fix(ui): Options panel Config tab content escapes the window frame — stale viewport anchor capture, not a missing clip
The Config tab's footer sat mid-panel with further rows drawing below the window's bottom edge. Live-DAT measured: the mounted tab-host root is authored 300x362 (retail's real default window size), but the Config page slot underneath keeps its own larger design geometry (298x575 against a 300x600 canvas) until retail's real four-edge UiLayoutPolicy (UIElement::UpdateForParentSizeChange @0x00462640) shrinks it on the first ApplyAnchor pass -- verified stable, this part already worked. The actual bug: UiTemplateListBox.Viewport (the UiScrollablePanel that hosts + clips every row) is a programmatic C# element seeded at Bind time, BEFORE the tree's first draw frame -- before the ListBox has ever shrunk. Its legacy anchor baseline is captured lazily on its own first ApplyAnchor call, which lands AFTER the ListBox has already shrunk earlier in that same frame (parent-before-child draw order). That capture measures a negative bottom margin the stretch math preserves forever: the viewport stayed locked at its original 560px design height, clipping rows to a bound retail never actually gave the window on screen. Fix: force the viewport's anchor capture to happen immediately after seeding it, while its Width/Height still exactly equal a zero-margin baseline against the CURRENT (pre-shrink) parent, instead of lazily on the first draw frame against an already-shrunk parent. This is #372's sequel -- #372 fixed the 0x0 collapse case; this is the "ListBox itself later shrinks" case #372's own fixture never exercised. Three new tests (UiTemplateListBoxViewportTests using the live-DAT-measured 298x575/276x560 numbers, plus two ConfigOptionsPageControllerTests against the real production Bind path and the committed host fixture) all fail pre-fix, confirmed by temporarily reverting the change. Scoped to UiTemplateListBox's own viewport; UiScrollablePanel/ApplyAnchor/ ComputeAnchoredRect are untouched, so chat's transcript scrolling and every other UiScrollablePanel/UiItemList consumer are unaffected. fix #412 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
97a7be12ee
commit
c623b57ad3
4 changed files with 310 additions and 0 deletions
|
|
@ -76,4 +76,75 @@ public sealed class UiTemplateListBoxViewportTests
|
|||
Assert.True(row1.Visible, "row 1 culled — the #372 blank-tab bug");
|
||||
Assert.True(row2.Visible, "row 2 culled — the #372 blank-tab bug");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #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
|
||||
/// <see cref="MakeListBox"/> 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 <see cref="UiElement"/>
|
||||
/// carrying a real <see cref="UiLayoutPolicy"/> 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.
|
||||
/// </summary>
|
||||
[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)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue