fix(CT6): fix round — chrome-inclusive host clamp (BLOCKER B1) + 372px mount default
Opus dual-lens review of CT6 (ec50455a) found 1 blocker, 4 should-fix, 5
notes. All applied:
BLOCKER B1 — the shared gmPanelUI host (0x100005FE) IS retail's own
outer window frame, not a content element: its authored 310/372/310/1000
already include the 5px bevel on every side. RetailWindowFrame.Mount was
adding the NineSlice wrapper's OWN 10px chrome inset on top of that
already-chrome-inclusive source, clamping MinWidth to 320 while the
window's actual mounted outer width stayed 310 — silently below its own
minimum until RetailWindowManager.ResizeTo forcibly widened it despite
ResizeX=false. Fixed with a new
RetailWindowFrame.Options.DatConstraintSourceIsOuterFrame opt-out
(chrome inset = 0 for constraint resolution only, value stays
DAT-sourced); MountCharacter sets it true. Mounted clamp is now exactly
the host's four raw values: width fixed 310, height 372..1000. Added a
mount-time invariant (throws if the mounted outer extent falls outside
its own just-computed clamp) that would have caught this at the first
test run.
S4 (campaign-lead ruling) — the window must MOUNT at retail's authored
default, outer 372 (content 362, matching the host's own content parent
0x10000180), not 0x2100002E's own 300x600 content-authoring canvas
(which produced a stale 610px default pre-fix: 600 + 10 chrome inset).
372 is exactly the host's own authored MinHeight — retail opens at its
resize floor and can only be dragged taller. MountCharacter now sets
ContentHeight=362f explicitly. At this default the 9 attribute/vital
rows (180px) overflow the 160px list immediately — retail-correct, not
a regression.
S2 — 0x1000023E and 0x10000533 both author property 0x79
(HideWhenDisabled) TRUE (fixture-verified: BoolValue=true on both). A
fitting list HIDES the scrollbar entirely; it does not draw a full-track
"disabled" thumb. The code was already correct; four wrong descriptions
(plan ledger, CharacterStatController comment, CT7 script, test comment)
are corrected, plus a new IsPresentationVisible assertion pair in the
resize test.
S3 — CharacterTitlesController's `if (listBox.LayoutPolicy is null)`
Anchors fallback was unreachable on both the real DAT and the fixture
(0x10000532/0x10000539 both author HasOriginalParentSize=true, so
LayoutPolicy is always assigned). Deleted; added an InstalledDatFact pin
guarding the deletion against DAT drift.
N4 — renamed NineSlice_ChatShapedConstraints_... to
NineSlice_ContentShapedConstraints_InsetArithmeticClampsProgrammaticResize
(it tested inset arithmetic on a content-shaped source, not chat's real
contract) and added a true chat-contract pin mounting Chrome=Imported
with chat's real 300/100/2000/2000 constraints, asserting no inset
applies.
N5 — corrected the "nothing inferred, no register row" sentences in the
ground-truth doc and plan ledger: they were false pre-fix (the mounted
clamp WAS an inferred double-counted composition); true now that B1
removes the composition.
CT7 script §4 rewritten with exact clamps (no "≈"), the corrected
default-overflow scrollbar behavior, and an absolute starting-height
statement.
Verified: full hermetic solution suite green (15,441 tests, Release,
Lane exclusions per the release gate), InstalledDat lane green across
the whole solution (414 tests, ACDREAM_RUN_INSTALLED_DAT_TESTS=1,
Status!=KnownFailure) including two new pins
(TitlesListAndPage_AuthorHasOriginalParentSize,
Imported_ChatContract_ClampsAtAuthoredBoundsWithNoChromeInset).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ec50455a63
commit
996cd73675
11 changed files with 552 additions and 120 deletions
|
|
@ -810,13 +810,18 @@ public static class CharacterStatController
|
|||
allRaise1, allRaise10, SetFooterSelected, iconDidResolve);
|
||||
}
|
||||
|
||||
// Always bound + visible for whichever tab is active — retail's
|
||||
// authored scrollbar gutter (0x1000023E, 281..297 within the
|
||||
// 300px list) is reserved regardless of content, and UiScrollbar
|
||||
// itself already draws the correct full-track "disabled" thumb
|
||||
// when Model.HasOverflow is false (HideWhenDisabled defaults to
|
||||
// false — see UiScrollbar.IsPresentationVisible), so no
|
||||
// per-tab visibility toggle is needed here any more.
|
||||
// Always BOUND for whichever tab is active — no per-tab visibility
|
||||
// toggle is needed here. CT6 fix round (S2 correction): the
|
||||
// scrollbar's own .Visible=true just keeps it in the tree; what
|
||||
// actually shows or hides it on screen is UiScrollbar's
|
||||
// IsPresentationVisible, and 0x1000023E authors 0x79
|
||||
// (HideWhenDisabled) TRUE (fixture-verified — the 121/0x79
|
||||
// property on the fixture's scrollbar element carries
|
||||
// BoolValue=true). That means a fitting list HIDES the bar
|
||||
// entirely, not a full-track "disabled" thumb left visible — the
|
||||
// previous comment here had the authored default backwards.
|
||||
// Content that overflows still makes the bar visible and
|
||||
// interactive, same as always.
|
||||
if (skillScrollbar is not null)
|
||||
{
|
||||
skillScrollbar.Model = viewport.Scroll;
|
||||
|
|
|
|||
|
|
@ -183,17 +183,23 @@ public sealed class CharacterTitlesController : IDisposable
|
|||
// the same way for the same reason).
|
||||
listBox.LineHeight = 24;
|
||||
// CT6 (2026-08-25): the Titles list (authored H=455 inside the
|
||||
// 575px page — CT1 ground truth §3) must shrink/grow with the
|
||||
// window the same way CharacterStatController's attribute/skill
|
||||
// list does — same compatibility-anchor pattern, only applied when
|
||||
// the imported element carries no authored edge policy of its own
|
||||
// (an authored LayoutPolicy always wins). UiTemplateListBox's own
|
||||
// 575px page — CT1 ground truth §3) shrinks/grows with the window
|
||||
// the same way CharacterStatController's attribute/skill list does.
|
||||
// CT6 fix round (S3 correction): the former Anchors fallback here
|
||||
// ("if LayoutPolicy is null") was DEAD CODE — both 0x10000532 (this
|
||||
// ListBox) and its page container 0x10000539 author
|
||||
// HasOriginalParentSize=true in the real DAT (pinned by
|
||||
// CharacterPanelLiveDatTests.TitlesListAndPage_AuthorHasOriginalParentSize),
|
||||
// so LayoutImporter/DatWidgetFactory ALWAYS assigns a real
|
||||
// LayoutPolicy to this element and the fallback branch never ran on
|
||||
// either the installed DAT or the committed fixture. The actual
|
||||
// reflow mechanism is that authored LayoutPolicy stretching with the
|
||||
// mounted content's height — deleted rather than left as
|
||||
// unreachable/misleading compatibility code. UiTemplateListBox's own
|
||||
// internal viewport (created lazily inside AddItemFromTemplateList)
|
||||
// already carries the #372-class eager-baseline-capture fix, so
|
||||
// once the ListBox itself reflows, its scrollbar (bound below) picks
|
||||
// up the new content/view relationship for free.
|
||||
if (listBox.LayoutPolicy is null)
|
||||
listBox.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom;
|
||||
// already carries the #372-class eager-baseline-capture fix, so once
|
||||
// the ListBox itself reflows, its scrollbar (bound below) picks up
|
||||
// the new content/view relationship for free.
|
||||
|
||||
uint scrollbarElementId = listBox.ScrollbarElementId;
|
||||
UiElement? scrollbarElement = scrollbarElementId == 0
|
||||
|
|
|
|||
|
|
@ -43,9 +43,30 @@ public static class RetailWindowFrame
|
|||
/// <summary>
|
||||
/// Optional DAT element whose effective 0x3C..0x3F properties provide
|
||||
/// max-height, max-width, min-height, and min-width respectively. Explicit
|
||||
/// option values win; wrapper chrome is added to DAT content constraints.
|
||||
/// option values win; wrapper chrome is added to DAT content constraints
|
||||
/// UNLESS <see cref="DatConstraintSourceIsOuterFrame"/> is set.
|
||||
/// </summary>
|
||||
public ElementInfo? DatConstraintSource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// True when <see cref="DatConstraintSource"/> is itself retail's own
|
||||
/// OUTER window frame — chrome already included in its authored
|
||||
/// 0x3C..0x3F values — rather than a content element the wrapper's own
|
||||
/// chrome gets layered on top of. CT6 fix-round BLOCKER B1: the
|
||||
/// Character window's resize clamp is authored on the shared
|
||||
/// <c>gmPanelUI</c> host (<c>0x100005FE</c>, LayoutDesc
|
||||
/// <c>0x2100006E</c>) — that host IS retail's outer frame (5px bevel +
|
||||
/// 300x362 content + 5px = 310x372), so its authored MinWidth/MinHeight/
|
||||
/// MaxWidth/MaxHeight ALREADY include the chrome our NineSlice wrapper
|
||||
/// draws. Adding the wrapper's own chrome inset on top double-counts
|
||||
/// the bevel and clamps the mounted window BELOW its own authored
|
||||
/// minimum. Set true only when the DAT source element genuinely already
|
||||
/// represents the complete outer frame, not a content root — the
|
||||
/// default (false) preserves the normal content-plus-chrome behavior
|
||||
/// every other window relies on.
|
||||
/// </summary>
|
||||
public bool DatConstraintSourceIsOuterFrame { get; init; }
|
||||
|
||||
public float? MinWidth { get; init; }
|
||||
public float? MinHeight { get; init; }
|
||||
public float? MaxWidth { get; init; }
|
||||
|
|
@ -157,16 +178,36 @@ public static class RetailWindowFrame
|
|||
outerFrame.Opacity = Math.Clamp(options.Opacity, 0f, 1f);
|
||||
outerFrame.Visible = options.Visible;
|
||||
|
||||
int constraintInset = options.DatConstraintSourceIsOuterFrame ? 0 : inset;
|
||||
outerFrame.MinWidth = ResolveConstraint(
|
||||
options.MinWidth, options.DatConstraintSource, 0x3Fu, outerWidth, inset);
|
||||
options.MinWidth, options.DatConstraintSource, 0x3Fu, outerWidth, constraintInset);
|
||||
outerFrame.MinHeight = ResolveConstraint(
|
||||
options.MinHeight, options.DatConstraintSource, 0x3Eu, outerHeight, inset);
|
||||
options.MinHeight, options.DatConstraintSource, 0x3Eu, outerHeight, constraintInset);
|
||||
outerFrame.MaxWidth = Math.Max(
|
||||
outerFrame.MinWidth,
|
||||
ResolveConstraint(options.MaxWidth, options.DatConstraintSource, 0x3Du, float.MaxValue, inset));
|
||||
ResolveConstraint(options.MaxWidth, options.DatConstraintSource, 0x3Du, float.MaxValue, constraintInset));
|
||||
outerFrame.MaxHeight = Math.Max(
|
||||
outerFrame.MinHeight,
|
||||
ResolveConstraint(options.MaxHeight, options.DatConstraintSource, 0x3Cu, float.MaxValue, inset));
|
||||
ResolveConstraint(options.MaxHeight, options.DatConstraintSource, 0x3Cu, float.MaxValue, constraintInset));
|
||||
|
||||
// Mount-time invariant (CT6 fix-round BLOCKER B1): a window must never
|
||||
// open already violating its own clamp. This would have caught B1 at
|
||||
// the first test run — the pre-fix Character mount computed outer
|
||||
// bounds of 320/382/1010 (300x362 content + 10px chrome inset added
|
||||
// TWICE) while its own MinWidth clamped to 320 but its actual outer
|
||||
// WIDTH was only 310, silently sitting below its own minimum until
|
||||
// RetailWindowManager.ResizeTo forcibly widened it despite ResizeX
|
||||
// being false.
|
||||
if (outerWidth < outerFrame.MinWidth || outerWidth > outerFrame.MaxWidth)
|
||||
throw new InvalidOperationException(
|
||||
$"RetailWindowFrame.Mount(\"{options.WindowName}\"): mounted outer width " +
|
||||
$"{outerWidth} is outside its own clamp [{outerFrame.MinWidth}, {outerFrame.MaxWidth}] " +
|
||||
"— the window would open already violating its authored resize bounds.");
|
||||
if (outerHeight < outerFrame.MinHeight || outerHeight > outerFrame.MaxHeight)
|
||||
throw new InvalidOperationException(
|
||||
$"RetailWindowFrame.Mount(\"{options.WindowName}\"): mounted outer height " +
|
||||
$"{outerHeight} is outside its own clamp [{outerFrame.MinHeight}, {outerFrame.MaxHeight}] " +
|
||||
"— the window would open already violating its authored resize bounds.");
|
||||
|
||||
// Capture the wrapper/content baseline at the mounted design extent now,
|
||||
// so a resize that occurs before the first draw uses the same margins as a
|
||||
|
|
|
|||
|
|
@ -4122,6 +4122,17 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
// host 0x100005FE authors MinWidth=310 MinHeight=372 MaxWidth=310
|
||||
// MaxHeight=1000 (Min==Max width: no horizontal Resizebar is
|
||||
// authored, matching ResizeX=false below).
|
||||
//
|
||||
// CT6 fix round (2026-08-25, BLOCKER B1): host 0x100005FE is not a
|
||||
// content element our NineSlice wrapper adds chrome to — it IS
|
||||
// retail's own outer window frame (5px bevel + 300x362 content parent
|
||||
// 0x10000180 + 5px = 310x372, verified by PanelHost_
|
||||
// AuthorsFixedWidthAndBottomOnlyResizeContract). Its authored
|
||||
// 0x3C..0x3F values already include that bevel. Without
|
||||
// DatConstraintSourceIsOuterFrame, RetailWindowFrame.ResolveConstraint
|
||||
// added the wrapper's OWN 10px chrome inset on top — double-counting
|
||||
// the bevel and clamping the mounted window's MinWidth to 320 while
|
||||
// its actual outer width stayed 310, silently below its own minimum.
|
||||
ElementInfo? hostConstraint;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
hostConstraint = LayoutImporter.ImportInfos(
|
||||
|
|
@ -4137,11 +4148,25 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Chrome = RetailWindowChrome.NineSlice,
|
||||
Left = 540f,
|
||||
Top = 18f,
|
||||
// CT6 fix round (2026-08-25, S4 ruling): mount at retail's
|
||||
// authored DEFAULT — the host's own content parent
|
||||
// (0x10000180) is 300x362, not 0x2100002E's own 300x600
|
||||
// authoring canvas (which retail scroll-clips into that
|
||||
// slot). 362 + the 10px NineSlice inset = 372, exactly the
|
||||
// host's own authored MinHeight — matching the ground-truth
|
||||
// finding that retail's Character/Skills window opens AT its
|
||||
// resize floor and can only be dragged taller. The 9
|
||||
// attribute/vital rows (180px) overflow the resulting 160px
|
||||
// list at this default; that is retail-correct, not a bug —
|
||||
// the list's own scrollbar (0x1000023E, hide-when-disabled)
|
||||
// activates immediately.
|
||||
ContentHeight = 362f,
|
||||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
ConstrainResizeToParent = true,
|
||||
DatConstraintSource = hostConstraint,
|
||||
DatConstraintSourceIsOuterFrame = true,
|
||||
Visible = false,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
ContentClickThrough = false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue