fix(ui): #372 — Options tabs no longer blank; UiTemplateListBox viewport fills its ListBox
ROOT CAUSE (proven, not guessed): the lazily-created row viewport was constructed 0x0 with Left|Top|Right|Bottom fill-anchors. Its first ApplyAnchor captured mR = parentW - (0+0) = parentW, so ComputeAnchoredRect's l&&r branch (w = parentW - mR - mL) kept it 0x0 forever. A 0-tall viewport makes UiScrollablePanel.LayoutScrollableChildren cull every row, so Character/Chat/Config rendered blank while Gameplay (no viewport — authored static children sized at Build) worked. This is the exact Gameplay-vs-rest split the user's first connected gate found. FIX: seed the viewport to the ListBox's current extent at creation, so the fill-anchor baseline is mR = parentW - parentW = 0 and the viewport tracks the parent. The ListBox is a static dat child sized at Build, so its extent is authored by the time the viewport is lazily created during Bind. Dormancy preserved — the viewport is still created only on the first row. Reproduced RED then GREEN by UiTemplateListBoxViewportTests (viewport fills; rows stay visible after the anchor+cull layout pass) — the layout path the whole fixture conformance suite structurally never drove, which is why every OP2-OP6 test was green over a live-only blank-tab failure. Full Release suite 13,131 / 4 skips / 0 failed. Still owed (NOT fixed here, no evidence yet): the 'only Exit Game worked' Gameplay-buttons observation needs a re-gate (two buttons are correctly INERT; the other four have dialog/chat effects that may have gone unnoticed); and the 13 ID_ChatOption_TextFilter_* labels fail to resolve (blank captions, behaviour unaffected). See #372. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c3ed32fb5a
commit
057d8cd703
3 changed files with 138 additions and 1 deletions
|
|
@ -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.
|
||||
|
||||
<!-- original filing -->
|
||||
**(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**
|
||||
|
|
|
|||
|
|
@ -144,16 +144,37 @@ public sealed class UiTemplateListBox : UiDatElement
|
|||
/// <summary>Retail ListBox rows never come from static dat children — see class doc.</summary>
|
||||
public override bool ConsumesDatChildren => true;
|
||||
|
||||
/// <summary>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).</summary>
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
79
tests/AcDream.App.Tests/UI/UiTemplateListBoxViewportTests.cs
Normal file
79
tests/AcDream.App.Tests/UI/UiTemplateListBoxViewportTests.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
/// <summary>
|
||||
/// #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: <see cref="UiTemplateListBox"/> creates its row
|
||||
/// viewport at 0×0 with Left|Top|Right|Bottom fill-anchors; the anchor
|
||||
/// baseline captured from that 0-size rect makes
|
||||
/// <c>ComputeAnchoredRect</c> keep it 0×0 forever (mR=parentW, so
|
||||
/// w=parentW-mR-mL=0), and <see cref="UiScrollablePanel.LayoutScrollableChildren"/>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue