using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI; /// /// #372 regression: the Options panel's Character/Chat/Config tabs rendered /// BLANK at the first connected gate while Gameplay (which has no ListBox) /// worked. Root cause: creates its row /// viewport at 0×0 with Left|Top|Right|Bottom fill-anchors; the anchor /// baseline captured from that 0-size rect makes /// ComputeAnchoredRect keep it 0×0 forever (mR=parentW, so /// w=parentW-mR-mL=0), and /// then culls every row (top+height ≤ 0 is false). These tests pin that the /// viewport fills its ListBox and the rows stay visible after a layout pass. /// public sealed class UiTemplateListBoxViewportTests { private static UiTemplateListBox MakeListBox(float width, float height) { // A ListBox element authoring one template entry, sized like the // Options-panel Character ListBox (0x100001FA, 276×560 authored). var info = new ElementInfo { Id = 0x100001FAu, Type = 5u }; var box = new UiTemplateListBox( info, _ => (0u, 0, 0), new[] { new UiTemplateListEntry(0x2100002Bu, 0x10000218u) }, scrollbarElementId: 0x100001FBu) { Width = width, Height = height, }; return box; } [Fact] public void Viewport_FillsTheListBox_NotCollapsedToZero() { var box = MakeListBox(276f, 560f); // Add a couple of rows via the prebuilt path (no DAT needed). box.AddPrebuiltRow(new UiText { Width = 260f, Height = 20f }); box.AddPrebuiltRow(new UiText { Width = 260f, Height = 20f }); UiScrollablePanel? viewport = box.ViewportForTest; Assert.NotNull(viewport); // Drive the per-frame layout the draw traversal runs: the parent // ListBox re-anchors its children (UiElement.DrawSelfAndChildren // line ~500), sizing the fill-anchored viewport. viewport!.ApplyAnchor(box.Width, box.Height); // The whole point of #372: the viewport must FILL the ListBox, not // collapse. Pre-fix this was 0×0. Assert.Equal(276f, viewport.Width, 3); Assert.Equal(560f, viewport.Height, 3); } [Fact] public void Rows_StayVisible_AfterLayout_WhenTheyFitTheViewport() { var box = MakeListBox(276f, 560f); var row0 = new UiText { Width = 260f, Height = 20f }; var row1 = new UiText { Width = 260f, Height = 20f }; var row2 = new UiText { Width = 260f, Height = 20f }; box.AddPrebuiltRow(row0); box.AddPrebuiltRow(row1); box.AddPrebuiltRow(row2); UiScrollablePanel viewport = box.ViewportForTest!; viewport.ApplyAnchor(box.Width, box.Height); // size the viewport viewport.LayoutScrollableChildren(); // the cull pass (runs each OnDraw) // All three rows sit at y 0/20/40 inside a 560px viewport → visible. // Pre-fix the viewport was 0px tall and every row was culled → blank tab. Assert.True(row0.Visible, "row 0 culled — the #372 blank-tab bug"); Assert.True(row1.Visible, "row 1 culled — the #372 blank-tab bug"); Assert.True(row2.Visible, "row 2 culled — the #372 blank-tab bug"); } /// /// #412-class regression (2026-08-16, overnight hover/UI round, Batch A bug /// 2): the Options panel's Config tab escaped past the window frame — the /// footer sitting mid-panel with further rows drawing below the window's /// bottom edge. #372's own fixture above never exercises this because /// gives the ListBox no parent — its own size /// never changes after the viewport is seeded. The real Options mount is /// different: this ListBox is itself a DAT-imported /// carrying a real from its authored parent /// (the Config page slot), and a page controller's Bind — which lazily /// creates this viewport — runs BEFORE the tree's first draw frame, i.e. /// before the ListBox has ever shrunk to fit its actual (smaller than /// authored) container. This reproduces that ordering with a real /// LayoutPolicy-driven parent standing in for the page slot. /// [Fact] public void Viewport_TracksTheListBox_WhenTheListBoxItselfShrinksOnFirstLayout() { // A stand-in for the Config page slot: authored 298×575 against an // authored 300×600 design canvas (live-DAT-measured values), but its // real mounted container is only 300×362 — exactly retail's // UIElement::UpdateForParentSizeChange four-edge policy (L=T=R=B=1, // "preserve original margin on every edge"). var slotPolicy = new UiLayoutPolicy( leftMode: 1, topMode: 1, rightMode: 1, bottomMode: 1, originalChild: UiPixelRect.FromPositionAndSize(2, 25, 298, 575), originalParent: UiPixelRect.FromPositionAndSize(0, 0, 300, 600)); var slot = new UiPanel { Left = 2, Top = 25, Width = 298, Height = 575, LayoutPolicy = slotPolicy, }; var root = new UiPanel { Width = 300, Height = 362 }; root.AddChild(slot); // The ListBox itself ALSO carries a real LayoutPolicy (live-DAT // measured: authored 276×560 against the slot's own 298×575 design // extent) — this is what shrinks it out from under the viewport. var box = MakeListBox(276f, 560f); var listBoxPolicy = new UiLayoutPolicy( leftMode: 1, topMode: 1, rightMode: 1, bottomMode: 1, originalChild: UiPixelRect.FromPositionAndSize(0, 0, 276, 560), originalParent: UiPixelRect.FromPositionAndSize(0, 0, 298, 575)); box.LayoutPolicy = listBoxPolicy; slot.AddChild(box); // Seed the viewport with rows BEFORE any layout pass has ever run — // exactly ConfigOptionsPageController.Bind's own ordering (it runs // before RetailWindowFrame.Mount's first draw frame). box.AddPrebuiltRow(new UiText { Width = 260f, Height = 20f }); UiScrollablePanel viewport = box.ViewportForTest!; // Drive ONE simulated draw-frame's top-down ApplyAnchor walk — the // SAME order DrawSelfAndChildren runs every frame: parent before // children, all the way down. slot.ApplyAnchor(root.Width, root.Height); // slot shrinks: 575 -> ~337 box.ApplyAnchor(slot.Width, slot.Height); // listbox shrinks: 560 -> ~297 (still ahead of the viewport) viewport.ApplyAnchor(box.Width, box.Height); // the viewport's FIRST EVER ApplyAnchor call // Pre-fix: the viewport's legacy anchor baseline captured a NEGATIVE // bottom margin against the ALREADY-SHRUNK ListBox (parentH(~297) - // (0+560) < 0), which ComputeAnchoredRect's stretch math preserves // forever — the viewport stayed locked at its original 560px height, // clipping rows to a bound retail never actually gave it on screen. Assert.Equal(box.Height, viewport.Height, 3); Assert.True( viewport.Height < 400f, $"viewport height {viewport.Height} did not shrink with its ListBox " + "(560 == the pre-fix stale-capture bug)"); } }