acdream/tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs
Erik 14ea938c7f fix(ui): D.2b inventory finish — grid cells exempt from the anchor pass
Root cause of the "grid escapes the window when scrolled" bug: UiElement.
DrawSelfAndChildren runs ApplyAnchor on every child AFTER OnDraw, which captured
each cell's scroll-0 position and reset Top to it every frame, fighting
LayoutCells' scroll offset. Cells are laid out procedurally by the list, so they
must be exempt — AddItem now sets cell.Anchors = None, making LayoutCells the
sole authority. Regression test reproduces the anchor-pass interaction (the unit
tests missed it by calling LayoutCells in isolation, never the draw traversal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:51:08 +02:00

83 lines
3.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AcDream.App.UI;
using Xunit;
namespace AcDream.App.Tests.UI;
public sealed class UiItemListScrollTests
{
private static UiItemList Grid(int items)
{
var list = new UiItemList { Columns = 6, CellWidth = 32, CellHeight = 32, Width = 192, Height = 96 };
list.Flush(); // drop the default single cell
for (int i = 0; i < items; i++) list.AddItem(new UiItemSlot());
list.LayoutCells(); // drive the scroll model + position cells
return list;
}
[Fact]
public void RowCount_ceils_to_whole_rows()
{
Assert.Equal(0, UiItemList.RowCount(0, 6));
Assert.Equal(1, UiItemList.RowCount(1, 6));
Assert.Equal(7, UiItemList.RowCount(41, 6));
}
[Fact]
public void Grid_drives_scroll_model_from_content()
{
var list = Grid(30); // 5 rows × 32 = 160 content, 96 view
Assert.Equal(160, list.Scroll.ContentHeight);
Assert.Equal(96, list.Scroll.ViewHeight);
Assert.Equal(64, list.Scroll.MaxScroll);
Assert.True(list.Scroll.HasOverflow);
}
[Fact]
public void At_top_first_three_rows_visible_rest_clipped()
{
var list = Grid(30);
Assert.True(list.GetItem(0)!.Visible); // row 0, top 0
Assert.True(list.GetItem(12)!.Visible); // row 2, top 64 (+32 == 96)
Assert.False(list.GetItem(18)!.Visible); // row 3, top 96 (off the bottom)
}
[Fact]
public void Scrolled_one_row_shifts_window_and_clips_top_row()
{
var list = Grid(30);
list.Scroll.SetScrollY(32);
list.LayoutCells();
Assert.False(list.GetItem(0)!.Visible); // row 0 scrolled above
Assert.Equal(0f, list.GetItem(6)!.Top); // row 1 now at the view top
Assert.True(list.GetItem(18)!.Visible); // row 3 now visible
}
[Fact]
public void Scroll_survives_the_per_frame_anchor_pass()
{
// Regression (the escaping-grid bug): UiElement.DrawSelfAndChildren runs ApplyAnchor
// on every child AFTER OnDraw/LayoutCells. Grid cells must be exempt (Anchors=None) so
// the anchor pass can't reset the scroll offset LayoutCells applied — otherwise the
// grid translates out of the view when scrolled.
var list = Grid(30); // cells added (Anchors=None) + laid out
for (int i = 0; i < list.GetNumUIItems(); i++) // first anchor pass (would capture base)
list.GetItem(i)!.ApplyAnchor(list.Width, list.Height);
list.Scroll.SetScrollY(32); // scroll one row
list.LayoutCells();
for (int i = 0; i < list.GetNumUIItems(); i++) // second anchor pass (the draw traversal)
list.GetItem(i)!.ApplyAnchor(list.Width, list.Height);
Assert.Equal(0f, list.GetItem(6)!.Top); // row 1 stays at the view top
Assert.False(list.GetItem(0)!.Visible); // row 0 stays clipped above
}
[Fact]
public void Wheel_down_scrolls_toward_the_bottom()
{
var list = Grid(30); // max scroll 64, LineHeight 32
// Silk wheel +Y = up/older; UiText negates Data0. Wheel DOWN (Data0 < 0) → +ScrollY.
var e = new UiEvent(0u, null, UiEventType.Scroll, Data0: -1);
bool handled = list.OnEvent(e);
Assert.True(handled);
Assert.Equal(32, list.Scroll.ScrollY); // one line (32px) down
}
}