feat(CT6): character window Y-resize clamped at retail's authored host minimum + shrink-and-scroll list contract

CT6 (Campaign CT slice 6): the resize clamp source is the SHARED
gmPanelUI host (0x100005FE in LayoutDesc 0x2100006E), not 0x2100002E's
own root and not the Character/Skills slot 0x1000018E either — live
probe confirmed the host authors MinWidth=MaxWidth=310 (fixed — no
horizontal Resizebar authored), MinHeight=372, MaxHeight=1000, and that
the bottom Resizebar (0x10000660) and top Dragbar (0x1000065C) are
direct children of the host, not the content parent. Decomp chain:
UIElement_Resizebar::StartMouseResizing @0x0046B7E0 calls
UIElement::StartResizing(this->GetParent(), ...), stashing drag state on
that parent; UIElement::MouseResizeElement @0x00461130 then reads
GetAttribute_Int(this, 0x3C..0x3F) off that same element every
mouse-move.

RetailUiRuntime.MountCharacter now imports the host element and passes
it as RetailWindowFrame.Options.DatConstraintSource, matching the
existing MountSideVitals pattern.

CharacterStatController.RebuildActiveList now wraps BOTH the Attributes
and Skills tabs' rows in the same UiScrollablePanel viewport (previously
only Skills got one; Attributes rows had no clipping/scrolling and the
shared scrollbar was force-hidden — owner report item 2). The shared
scrollbar is now always bound + visible; UiScrollbar's own
IsPresentationVisible/IsModelDisabled already draw the correct
full-track "disabled" thumb when content fits. This surfaced and fixed
a real #372/#412-class anchor-baseline bug: the viewport's
Left|Top|Bottom anchor was capturing its baseline margins lazily on its
own first ApplyAnchor call, which happens AFTER the ListBox has already
grown from its raw DAT height to its mounted height, permanently
capping the viewport short on every later resize. Fixed with an eager
CaptureCurrentAnchorBaseline() call, mirroring UiTemplateListBox
.Viewport's own lazy getter.

CharacterTitlesController.Bind gained the same defensive
Anchors = Left|Top|Bottom fallback for the Titles ListBox that
CharacterStatController already had (a no-op on the real DAT — both the
Titles page and its ListBox already carry a real authored LayoutPolicy
that stretches correctly).

Standardization audit: UiElement.MinWidth/MinHeight/MaxWidth/MaxHeight,
set once at RetailWindowFrame.Mount, are the ONLY clamp fields — read
identically by interactive drag, RetailWindowManager.ResizeTo, and
RetailWindowLayoutPersistence's restore clamp. No gaps found; no
register row (every number is a live-probed authored DAT value or a
structural correctness fix, nothing inferred).

Tests: CharacterStatControllerTests
.CharacterWindow_ResizesYWithinAuthoredHostClamp_AndReflowsListAndScrollbar,
CharacterTitlesControllerTests
.TitlesList_ReflowsWithWindowResize_AndScrollbarOverflowFlips,
RetailWindowFrameTests
.NineSlice_ChatShapedConstraints_ClampProgrammaticResizeAtAuthoredBounds
(shared-mechanism regression pin), CharacterPanelLiveDatTests
.PanelHost_AuthorsFixedWidthAndBottomOnlyResizeContract (InstalledDat
pin). Existing attribute-row tests updated from list.Children to
Descendants(list) for the new nested-viewport shape (the pattern skill
rows already needed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-25 02:21:56 +02:00
parent 4cbbdaf4bf
commit ec50455a63
10 changed files with 741 additions and 74 deletions

View file

@ -399,8 +399,75 @@ enforcement verified as the STANDARD path for every registered window
(one shared mechanism in `RetailWindowFrame`/`RetailWindowManager`,
no per-window special cases).
**CT6 landing notes (2026-08-25, implementation).** Live probe (dumped
+ deleted, pattern preserved by the new
`CharacterPanelLiveDatTests.PanelHost_AuthorsFixedWidthAndBottomOnlyResizeContract`
pin) confirmed the research lead's hypothesis exactly: the shared
`gmPanelUI` host `0x100005FE` (LayoutDesc `0x2100006E`) authors
MinWidth=MaxWidth=310 (fixed — no horizontal Resizebar), MinHeight=372,
MaxHeight=1000; its bottom Resizebar (`0x10000660`) and top Dragbar
(`0x1000065C`) are DIRECT CHILDREN of the host, not the content parent
— matching `UIElement_Resizebar::StartMouseResizing @0x0046B7E0`'s
`GetParent()` call and `UIElement::MouseResizeElement @0x00461130`'s
`GetAttribute_Int(this, 0x3C..0x3F)` reads off that same parent. The
Character/Skills slot `0x1000018E` itself authors no constraints of its
own (confirmed, same pin). `RetailUiRuntime.MountCharacter` now imports
that host element and passes it as `DatConstraintSource`; the mounted
outer frame clamps at MinWidth=MaxWidth≈320, MinHeight≈382,
MaxHeight≈1010 after the NineSlice chrome inset. Full derivation +
decomp anchors: `docs/research/2026-08-24-campaign-ct-dat-ground-truth.md`
§CT6.
`CharacterStatController.RebuildActiveList` now wraps BOTH the
Attributes and Skills tabs' rows in the same `UiScrollablePanel`
viewport (previously only Skills got one; Attributes rows were added
directly to the ListBox with no clipping/scrolling and the shared
scrollbar was force-hidden — the owner's item 2). The shared scrollbar
is now always bound + visible; `UiScrollbar`'s own
`IsPresentationVisible`/`IsModelDisabled` already draw the correct
full-track "disabled" thumb when content fits (`HideWhenDisabled`
defaults false), so no per-tab visibility toggle is needed any more.
This surfaced and fixed a real, previously-unexercised `#372`/
`#412`-class anchor-baseline bug: the viewport's `Left|Top|Bottom`
anchor was capturing its baseline margins lazily on its OWN first
`ApplyAnchor` call, which happens AFTER the ListBox has already grown
from its raw DAT height (160px) to its mounted height — measuring a
bogus non-zero margin that permanently capped the viewport short on
every later resize. Fixed with an eager
`viewport.CaptureCurrentAnchorBaseline()` call right after
`AddChild`, mirroring the identical fix already shipped in
`UiTemplateListBox.Viewport`'s own lazy getter. `CharacterTitlesController.Bind`
gained the same defensive `Anchors = Left|Top|Bottom` fallback for the
Titles ListBox (`0x10000532`) that `CharacterStatController` already
had for its own list — live-verified as a no-op on the real DAT (both
the Titles page container and its ListBox already carry a real authored
`LayoutPolicy` that stretches correctly on its own), but matching the
established pattern for synthetic/test layouts.
STANDARDIZATION AUDIT (no gaps found, no follow-up filed): `UiElement
.MinWidth/MinHeight/MaxWidth/MaxHeight`, set once at
`RetailWindowFrame.Mount` from `Options.DatConstraintSource`/explicit
overrides, are the ONLY clamp fields — read identically by the
interactive drag path (`UiRoot`'s resize handling), the programmatic
path (`RetailWindowManager.ResizeTo`, which both `RetailPanelUiController`'s
main-panel geometry sync and this slice's tests exercise), and the
persisted-geometry restore clamp (`RetailWindowLayoutPersistence.Apply`).
`RetailWindowFrame.Mount` remains the single production mount path (no
window bypasses it). New regression pin:
`RetailWindowFrameTests.NineSlice_ChatShapedConstraints_ClampProgrammaticResizeAtAuthoredBounds`
proves the same mechanism still clamps chat-shaped constraints after
Character was wired onto it. Tests: `CharacterStatControllerTests
.CharacterWindow_ResizesYWithinAuthoredHostClamp_AndReflowsListAndScrollbar`
(window-level: clamp at authored min/max, list shrink, scrollbar
overflow flip, footer stays bottom-docked, grow-back restore) and
`CharacterTitlesControllerTests.TitlesList_ReflowsWithWindowResize_AndScrollbarOverflowFlips`
(same contract for the Titles list) plus the pre-existing 126+22-test
suites, all updated where the new nested-viewport DOM shape required it
(`Descendants(list)` instead of `list.Children` — the shape Skills rows
already needed). No register row: every number is either a live-probed
authored DAT value or a structural anchor-capture-correctness fix,
nothing inferred.
**CT7 — Connected gate.** Test script
(`docs/research/2026-08-24-campaign-ct-test-script.md`), owner drive:
(`docs/research/2026-08-25-campaign-ct-test-script.md`), owner drive:
titles round trip against ACE (earn/set/display), header lines vs
retail side-by-side, resize behavior, row alignment screenshots.