From 14ea938c7f4a0f22f887045000e27951cd38ca5e Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 21 Jun 2026 20:51:08 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20D.2b=20inventory=20finish=20?= =?UTF-8?q?=E2=80=94=20grid=20cells=20exempt=20from=20the=20anchor=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.App/UI/UiItemList.cs | 6 ++++++ .../UI/UiItemListScrollTests.cs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/AcDream.App/UI/UiItemList.cs b/src/AcDream.App/UI/UiItemList.cs index b25e45cb..61c85d66 100644 --- a/src/AcDream.App/UI/UiItemList.cs +++ b/src/AcDream.App/UI/UiItemList.cs @@ -53,6 +53,12 @@ public sealed class UiItemList : UiElement public void AddItem(UiItemSlot cell) { cell.SpriteResolve ??= SpriteResolve; + // The list lays cells out procedurally (grid offset + scroll clip), so cells must be + // EXEMPT from the parent anchor pass — UiElement.DrawSelfAndChildren runs ApplyAnchor + // on every child after OnDraw, which would otherwise reset the scroll offset to each + // cell's captured base position (the escaping-grid bug). Anchors=None makes LayoutCells + // the sole authority over cell rects. + cell.Anchors = AnchorEdges.None; _cells.Add(cell); AddChild(cell); LayoutCells(); diff --git a/tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs b/tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs index d89c8be7..e8fa7917 100644 --- a/tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs +++ b/tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs @@ -52,6 +52,24 @@ public sealed class UiItemListScrollTests 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() {