diff --git a/docs/ISSUES.md b/docs/ISSUES.md index d672e01d..502a0686 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -26,7 +26,44 @@ What does NOT go here: ## #372 — Options panel: Character/Chat/Config tabs render BLANK on screen and most Gameplay buttons do nothing (connected-gate failure) -**Status:** OPEN — filed 2026-08-11 at Campaign OP's first connected gate +**Status:** BLANK-TABS ROOT-CAUSED + FIXED (`c3ed32fb` probe, fix this +commit); the Gameplay-buttons observation still needs a re-gate. Filed +2026-08-11 at Campaign OP's first connected gate. + +**ROOT CAUSE (blank tabs) — found + fixed.** `UiTemplateListBox` creates its +row viewport lazily (post-Build, during a page controller's `Bind`) at +**0×0** with `Left|Top|Right|Bottom` fill-anchors. The anchor system captures +its baseline from that 0-size rect (`mR = parentW − (Left+Width) = parentW`), +and `UiElement.ComputeAnchoredRect`'s left+right branch then yields +`w = parentW − mR − mL = 0` (and `h = 0`) **permanently**. A 0-tall viewport +makes `UiScrollablePanel.LayoutScrollableChildren` cull every row +(`top + height ≤ Height` is false for any real row at Height 0), so every +ListBox-backed tab (Character/Chat/Config) draws empty. Gameplay works +because it has no viewport — its buttons are authored static children sized +at Build, so they never hit the lazy-0×0 path. **Fix:** seed the viewport to +the ListBox's current extent at creation (`UiTemplateListBox.cs` Viewport +getter), so the fill-anchor baseline is `mR = parentW − parentW = 0` and the +viewport tracks the ListBox. Regressed by +`tests/AcDream.App.Tests/UI/UiTemplateListBoxViewportTests.cs` (RED→GREEN) +and the live-DAT mount probe. Every fixture conformance test stayed green +throughout — the false-negative class this issue documents — so the +regression test drives the anchor+cull layout path the suite never did. + +**Still owed — the Gameplay-buttons half is NOT root-caused.** Of the seven +buttons the user found only Exit Game acting. Two (Configure Keyboard, +In-Game Help) are correctly INERT. The other four (Exit-to-CharSel confirm +dialog, Use-Mouse-Turning chat lines, Urgent Assistance / Report Abuse +failure text) have effects that may have gone unnoticed rather than failed — +no evidence either way yet. Needs a re-gate observation (per-button: does a +click produce ANY visible response) before investigating; do not guess-fix. +Separately, the live log showed all 13 `ID_ChatOption_TextFilter_*` Chat-tab +filter labels failing to resolve from string table `0x23000003` (rows render +blank-captioned by the honest-fallback path; masks/behaviour unaffected) — a +minor label-resolution bug to fix (wrong table id or key spelling for that +family), tracked here until split out. + + +**(filed OPEN)** — 2026-08-11 at Campaign OP's first connected gate (`ACDREAM_RETAIL_UI=1`, live ACE). The user found: opening the Options panel (F11/toolbar) shows the Gameplay tab, but switching to Character/Chat/Config shows a BLANK page, and of the seven Gameplay buttons only **Exit Game** diff --git a/src/AcDream.App/UI/UiTemplateListBox.cs b/src/AcDream.App/UI/UiTemplateListBox.cs index 4091d143..5968899d 100644 --- a/src/AcDream.App/UI/UiTemplateListBox.cs +++ b/src/AcDream.App/UI/UiTemplateListBox.cs @@ -144,16 +144,37 @@ public sealed class UiTemplateListBox : UiDatElement /// Retail ListBox rows never come from static dat children — see class doc. public override bool ConsumesDatChildren => true; + /// The lazily-created row viewport, or null before the first row is added. + /// Exposed for #372's regression test (the viewport must fill this ListBox, not + /// collapse to 0×0). + internal UiScrollablePanel? ViewportForTest => _viewport; + private UiScrollablePanel Viewport { get { if (_viewport is null) { + // #372: the viewport MUST start filling this ListBox. It is created + // lazily (post-Build, during a page controller's Bind), so it misses + // the Build-time sizing that authored static children get. With + // Left|Top|Right|Bottom fill-anchors but a 0×0 initial rect, the + // anchor system captures a degenerate baseline (mR = parentW - (0+0) + // = parentW) and ComputeAnchoredRect then keeps it 0×0 forever + // (w = parentW - mR - mL = 0) — a 0-tall viewport makes + // UiScrollablePanel.LayoutScrollableChildren cull every row, so every + // ListBox-backed Options tab renders BLANK (Gameplay, which has no + // viewport, was the only tab that worked). Seeding the viewport to + // this ListBox's current size makes the fill-anchor baseline correct + // (mR = parentW - (0 + parentW) = 0) and self-maintaining as the + // ListBox re-anchors each frame. The ListBox is a static dat child + // sized at Build, so its extent is already authored here. _viewport = new UiScrollablePanel { Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom, LineHeight = _pendingLineHeight, + Width = Width, + Height = Height, }; base.AddChild(_viewport); } diff --git a/tests/AcDream.App.Tests/UI/UiTemplateListBoxViewportTests.cs b/tests/AcDream.App.Tests/UI/UiTemplateListBoxViewportTests.cs new file mode 100644 index 00000000..98ceeba6 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/UiTemplateListBoxViewportTests.cs @@ -0,0 +1,79 @@ +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"); + } +}