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:
parent
4cbbdaf4bf
commit
ec50455a63
10 changed files with 741 additions and 74 deletions
|
|
@ -380,6 +380,63 @@ public sealed class CharacterPanelLiveDatTests
|
|||
Assert.Equal(2000, tree.MaxHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CT6 (2026-08-25 live probe): the ACTUAL resize clamp source for the
|
||||
/// Character/Skills window is the SHARED <c>gmPanelUI</c> host
|
||||
/// (<c>0x100005FE</c> in LayoutDesc <c>0x2100006E</c>) — not
|
||||
/// <c>0x2100002E</c>'s own root (see the pin above) and not the
|
||||
/// Character/Skills slot <c>0x1000018E</c> either (asserted below to
|
||||
/// author no constraints of its own). Decomp chain:
|
||||
/// <c>UIElement_Resizebar::StartMouseResizing @0x0046B7E0</c> calls
|
||||
/// <c>UIElement::StartResizing(this->GetParent(), ...)</c>, stashing
|
||||
/// the drag state on that PARENT; <c>UIElement::MouseResizeElement
|
||||
/// @0x00461130</c> then reads <c>GetAttribute_Int(this, 0x3C..0x3F)</c>
|
||||
/// off that SAME element every mouse-move. The bottom Resizebar
|
||||
/// (<c>0x10000660</c>) and top Dragbar (<c>0x1000065C</c>) are both
|
||||
/// DIRECT CHILDREN of the host (siblings of the content parent
|
||||
/// <c>0x10000180</c>), confirmed here — so the host's own authored
|
||||
/// 0x3C..0x3F values are what <c>RetailUiRuntime.MountCharacter</c>
|
||||
/// must plumb through as <see cref="RetailWindowFrame.Options.DatConstraintSource"/>.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void PanelHost_AuthorsFixedWidthAndBottomOnlyResizeContract()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
|
||||
ElementInfo? host = LayoutImporter.ImportInfos(dats, 0x2100006Eu, 0x100005FEu);
|
||||
Assert.NotNull(host);
|
||||
Assert.Equal(310f, host!.Width);
|
||||
Assert.Equal(372f, host.Height);
|
||||
|
||||
// MinWidth == MaxWidth: no horizontal Resizebar is authored on this
|
||||
// host (only the top Dragbar and bottom Resizebar — MountCharacter's
|
||||
// ResizeX=false matches this exactly). MinHeight == the host's own
|
||||
// authored default height (372) — retail's Character/Skills window
|
||||
// can only grow taller, never shrink below its own authored extent.
|
||||
Assert.Equal(310, host.MinWidth);
|
||||
Assert.Equal(310, host.MaxWidth);
|
||||
Assert.Equal(372, host.MinHeight);
|
||||
Assert.Equal(1000, host.MaxHeight);
|
||||
|
||||
// The Resizebar and Dragbar are DIRECT CHILDREN of the host, not the
|
||||
// content parent (0x10000180) — the load-bearing parentage fact
|
||||
// StartMouseResizing's GetParent() call depends on.
|
||||
Assert.Contains(host.Children, c => c.Id == 0x10000660u && c.Type == 9u); // Resizebar
|
||||
Assert.Contains(host.Children, c => c.Id == 0x1000065Cu && c.Type == 2u); // Dragbar
|
||||
Assert.Contains(host.Children, c => c.Id == 0x10000180u); // content parent, a SIBLING
|
||||
|
||||
// The Character/Skills slot itself authors no constraints of its
|
||||
// own — the clamp is exclusively the host's, not layered again on
|
||||
// the slot.
|
||||
ElementInfo? slot = LayoutImporter.ImportInfos(dats, 0x2100006Eu, 0x1000018Eu);
|
||||
Assert.NotNull(slot);
|
||||
Assert.Equal(300f, slot!.Width);
|
||||
Assert.Equal(362f, slot.Height);
|
||||
Assert.Null(slot.MinWidth);
|
||||
Assert.Null(slot.MinHeight);
|
||||
Assert.Null(slot.MaxWidth);
|
||||
Assert.Null(slot.MaxHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The title-string table chain
|
||||
/// (<c>CharacterTitleTable::GetCharacterTitleFromID @0x005c6ed0</c>):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue