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>
83 lines
3.5 KiB
C#
83 lines
3.5 KiB
C#
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
|
||
}
|
||
}
|