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
|
|
@ -412,21 +412,74 @@ MaxHeight=1000; its bottom Resizebar (`0x10000660`) and top Dragbar
|
||||||
`GetAttribute_Int(this, 0x3C..0x3F)` reads off that same parent. The
|
`GetAttribute_Int(this, 0x3C..0x3F)` reads off that same parent. The
|
||||||
Character/Skills slot `0x1000018E` itself authors no constraints of its
|
Character/Skills slot `0x1000018E` itself authors no constraints of its
|
||||||
own (confirmed, same pin). `RetailUiRuntime.MountCharacter` now imports
|
own (confirmed, same pin). `RetailUiRuntime.MountCharacter` now imports
|
||||||
that host element and passes it as `DatConstraintSource`; the mounted
|
that host element and passes it as `DatConstraintSource`. **CORRECTED
|
||||||
outer frame clamps at MinWidth=MaxWidth≈320, MinHeight≈382,
|
(CT6 fix round, BLOCKER B1):** this paragraph originally claimed the
|
||||||
MaxHeight≈1010 after the NineSlice chrome inset. Full derivation +
|
mounted outer frame clamped at "MinWidth=MaxWidth≈320, MinHeight≈382,
|
||||||
decomp anchors: `docs/research/2026-08-24-campaign-ct-dat-ground-truth.md`
|
MaxHeight≈1010 after the NineSlice chrome inset" — that was WRONG. Host
|
||||||
§CT6.
|
`0x100005FE` is not a content element our wrapper adds chrome to; it IS
|
||||||
|
retail's own outer window frame (5px bevel + 300×362 content parent
|
||||||
|
`0x10000180` + 5px = 310×372), so its authored 0x3C..0x3F values are
|
||||||
|
already chrome-INCLUSIVE. Adding the NineSlice wrapper's own 10px inset
|
||||||
|
on top double-counted the bevel, 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 when set — the value stays
|
||||||
|
DAT-sourced, only the redundant inset is skipped); `MountCharacter` sets
|
||||||
|
it true. The mounted outer clamps are now EXACTLY what the host authors:
|
||||||
|
width fixed **310**, height **372..1000** — no composed arithmetic. A
|
||||||
|
new mount-time invariant in `RetailWindowFrame.Mount` (throws if the
|
||||||
|
just-mounted outer extent falls outside its own just-computed clamp)
|
||||||
|
would have caught B1 at the very first test run; it is now permanent for
|
||||||
|
every window this path mounts.
|
||||||
|
|
||||||
|
**S4 (2026-08-25, campaign-lead ruling — which number governs the
|
||||||
|
default mount size):** `0x2100002E`'s own root is authored 300×600 (the
|
||||||
|
"Size tension" the earlier ground-truth doc left unresolved — CT3's
|
||||||
|
Titles page alone is 300×575, plus the 25px tab bar). That 600 is a real
|
||||||
|
authored canvas, but it is the CONTENT's own design surface, not the
|
||||||
|
mounted default — retail scroll-clips it into the shared host's much
|
||||||
|
smaller 300×362 content parent (`0x10000180`). Pre-fix, `MountCharacter`
|
||||||
|
left `ContentHeight` unset, so it fell back to the raw 600px canvas,
|
||||||
|
producing a stale 610px mounted default (600 + 10px chrome inset) that
|
||||||
|
was never retail's actual opening size. **372 (the host's own outer
|
||||||
|
frame, 362 content + 10 chrome) is the number that governs the mount
|
||||||
|
default** — it is also exactly the host's own authored MinHeight, so
|
||||||
|
retail's Character/Skills window opens AT its resize floor and can only
|
||||||
|
be dragged taller, never shorter. `MountCharacter` now sets
|
||||||
|
`Options.ContentHeight = 362f` explicitly to realize this. The authored
|
||||||
|
page composition (header 112 + list 160 + divider + footer) IS the
|
||||||
|
362px design; at that default the 9 attribute/vital rows (180px content)
|
||||||
|
OVERFLOW the 160px list, so the stat list's scrollbar is active
|
||||||
|
immediately on open — retail-correct, not a regression (see S2 below for
|
||||||
|
what "active" actually looks like). Persistence still restores a
|
||||||
|
user-chosen size within the 372..1000 clamp on top of this default. Full
|
||||||
|
derivation + decomp anchors:
|
||||||
|
`docs/research/2026-08-24-campaign-ct-dat-ground-truth.md` §CT6.
|
||||||
`CharacterStatController.RebuildActiveList` now wraps BOTH the
|
`CharacterStatController.RebuildActiveList` now wraps BOTH the
|
||||||
Attributes and Skills tabs' rows in the same `UiScrollablePanel`
|
Attributes and Skills tabs' rows in the same `UiScrollablePanel`
|
||||||
viewport (previously only Skills got one; Attributes rows were added
|
viewport (previously only Skills got one; Attributes rows were added
|
||||||
directly to the ListBox with no clipping/scrolling and the shared
|
directly to the ListBox with no clipping/scrolling and the shared
|
||||||
scrollbar was force-hidden — the owner's item 2). The shared scrollbar
|
scrollbar was force-hidden — the owner's item 2). The shared scrollbar
|
||||||
is now always bound + visible; `UiScrollbar`'s own
|
is now always BOUND (`.Model`/`.Visible = true`); no per-tab visibility
|
||||||
`IsPresentationVisible`/`IsModelDisabled` already draw the correct
|
toggle is needed. **CORRECTED (CT6 fix round, S2):** this paragraph
|
||||||
full-track "disabled" thumb when content fits (`HideWhenDisabled`
|
originally claimed `UiScrollbar`'s own `IsPresentationVisible`/
|
||||||
defaults false), so no per-tab visibility toggle is needed any more.
|
`IsModelDisabled` "draw the correct full-track 'disabled' thumb when
|
||||||
This surfaced and fixed a real, previously-unexercised `#372`/
|
content fits (`HideWhenDisabled` defaults false)" — that had the
|
||||||
|
authored default BACKWARDS. `0x1000023E` (this scrollbar) and
|
||||||
|
`0x10000533` (the Titles list's own scrollbar) both author property
|
||||||
|
`0x79` (`HideWhenDisabled`) **TRUE**, fixture-verified (`BoolValue: true`
|
||||||
|
on both elements' property 121/0x79 in the committed fixture). A fitting
|
||||||
|
list HIDES the bar entirely; it does not leave a full-track disabled
|
||||||
|
thumb visible. The code was already correct — `.Visible = true` only
|
||||||
|
keeps the bar in the tree, `IsPresentationVisible` does the actual
|
||||||
|
show/hide — only this description was wrong; fixed here, in
|
||||||
|
`CharacterStatController.RebuildActiveList`'s own comment, in the CT7
|
||||||
|
script, and in `CharacterStatControllerTests`' comment, plus a new
|
||||||
|
`IsPresentationVisible` assertion pair added to the resize test (hidden
|
||||||
|
once growing makes the content fit, visible+interactive while
|
||||||
|
overflowing). This surfaced and fixed a real, previously-unexercised `#372`/
|
||||||
`#412`-class anchor-baseline bug: the viewport's `Left|Top|Bottom`
|
`#412`-class anchor-baseline bug: the viewport's `Left|Top|Bottom`
|
||||||
anchor was capturing its baseline margins lazily on its OWN first
|
anchor was capturing its baseline margins lazily on its OWN first
|
||||||
`ApplyAnchor` call, which happens AFTER the ListBox has already grown
|
`ApplyAnchor` call, which happens AFTER the ListBox has already grown
|
||||||
|
|
@ -435,13 +488,19 @@ bogus non-zero margin that permanently capped the viewport short on
|
||||||
every later resize. Fixed with an eager
|
every later resize. Fixed with an eager
|
||||||
`viewport.CaptureCurrentAnchorBaseline()` call right after
|
`viewport.CaptureCurrentAnchorBaseline()` call right after
|
||||||
`AddChild`, mirroring the identical fix already shipped in
|
`AddChild`, mirroring the identical fix already shipped in
|
||||||
`UiTemplateListBox.Viewport`'s own lazy getter. `CharacterTitlesController.Bind`
|
`UiTemplateListBox.Viewport`'s own lazy getter. **CORRECTED (CT6 fix
|
||||||
gained the same defensive `Anchors = Left|Top|Bottom` fallback for the
|
round, S3):** `CharacterTitlesController.Bind` originally gained the
|
||||||
Titles ListBox (`0x10000532`) that `CharacterStatController` already
|
same defensive `if (listBox.LayoutPolicy is null) Anchors =
|
||||||
had for its own list — live-verified as a no-op on the real DAT (both
|
Left|Top|Bottom` fallback for the Titles ListBox (`0x10000532`) that
|
||||||
the Titles page container and its ListBox already carry a real authored
|
`CharacterStatController` already had for its own list. Both
|
||||||
`LayoutPolicy` that stretches correctly on its own), but matching the
|
`0x10000532` and the Titles page container `0x10000539` author
|
||||||
established pattern for synthetic/test layouts.
|
`HasOriginalParentSize=true` in the real DAT AND the committed fixture,
|
||||||
|
which makes `LayoutImporter`/`DatWidgetFactory` always assign a real
|
||||||
|
`LayoutPolicy` — the fallback branch was therefore UNREACHABLE, not a
|
||||||
|
harmless no-op "matching the established pattern for synthetic/test
|
||||||
|
layouts" as originally described. Deleted rather than left as dead code;
|
||||||
|
a new `CharacterPanelLiveDatTests` pin asserts `HasOriginalParentSize`
|
||||||
|
on both elements to guard the deletion against future DAT drift.
|
||||||
STANDARDIZATION AUDIT (no gaps found, no follow-up filed): `UiElement
|
STANDARDIZATION AUDIT (no gaps found, no follow-up filed): `UiElement
|
||||||
.MinWidth/MinHeight/MaxWidth/MaxHeight`, set once at
|
.MinWidth/MinHeight/MaxWidth/MaxHeight`, set once at
|
||||||
`RetailWindowFrame.Mount` from `Options.DatConstraintSource`/explicit
|
`RetailWindowFrame.Mount` from `Options.DatConstraintSource`/explicit
|
||||||
|
|
@ -451,10 +510,20 @@ path (`RetailWindowManager.ResizeTo`, which both `RetailPanelUiController`'s
|
||||||
main-panel geometry sync and this slice's tests exercise), and the
|
main-panel geometry sync and this slice's tests exercise), and the
|
||||||
persisted-geometry restore clamp (`RetailWindowLayoutPersistence.Apply`).
|
persisted-geometry restore clamp (`RetailWindowLayoutPersistence.Apply`).
|
||||||
`RetailWindowFrame.Mount` remains the single production mount path (no
|
`RetailWindowFrame.Mount` remains the single production mount path (no
|
||||||
window bypasses it). New regression pin:
|
window bypasses it). New regression pin (**renamed, CT6 fix round N4**:
|
||||||
`RetailWindowFrameTests.NineSlice_ChatShapedConstraints_ClampProgrammaticResizeAtAuthoredBounds`
|
the original name `NineSlice_ChatShapedConstraints_
|
||||||
|
ClampProgrammaticResizeAtAuthoredBounds` overclaimed — it exercises
|
||||||
|
NineSlice inset ARITHMETIC on a content-shaped source (490×100,
|
||||||
|
height-only synthetic constraints) and never actually pinned chat's real
|
||||||
|
DAT contract, since no width constraints were even set):
|
||||||
|
`RetailWindowFrameTests.NineSlice_ContentShapedConstraints_InsetArithmeticClampsProgrammaticResize`
|
||||||
proves the same mechanism still clamps chat-shaped constraints after
|
proves the same mechanism still clamps chat-shaped constraints after
|
||||||
Character was wired onto it. Tests: `CharacterStatControllerTests
|
Character was wired onto it. A new companion test,
|
||||||
|
`Imported_ChatContract_ClampsAtAuthoredBoundsWithNoChromeInset`, mounts
|
||||||
|
with `Chrome=Imported` and chat's real 300/100/2000/2000 constraints
|
||||||
|
(matching production's actual `MountChat` wiring) and asserts no inset
|
||||||
|
applies — the true chat-contract pin the renamed test's name no longer
|
||||||
|
claims to be. Tests: `CharacterStatControllerTests
|
||||||
.CharacterWindow_ResizesYWithinAuthoredHostClamp_AndReflowsListAndScrollbar`
|
.CharacterWindow_ResizesYWithinAuthoredHostClamp_AndReflowsListAndScrollbar`
|
||||||
(window-level: clamp at authored min/max, list shrink, scrollbar
|
(window-level: clamp at authored min/max, list shrink, scrollbar
|
||||||
overflow flip, footer stays bottom-docked, grow-back restore) and
|
overflow flip, footer stays bottom-docked, grow-back restore) and
|
||||||
|
|
@ -462,9 +531,16 @@ overflow flip, footer stays bottom-docked, grow-back restore) and
|
||||||
(same contract for the Titles list) plus the pre-existing 126+22-test
|
(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
|
suites, all updated where the new nested-viewport DOM shape required it
|
||||||
(`Descendants(list)` instead of `list.Children` — the shape Skills rows
|
(`Descendants(list)` instead of `list.Children` — the shape Skills rows
|
||||||
already needed). No register row: every number is either a live-probed
|
already needed). No register row: **CORRECTED (CT6 fix round, N5)** —
|
||||||
authored DAT value or a structural anchor-capture-correctness fix,
|
before the B1 fix this sentence ("every number is either a live-probed
|
||||||
nothing inferred.
|
authored DAT value ... nothing inferred") was not actually true: the
|
||||||
|
mounted 320/382/1010 clamp WAS an inference (the host's chrome-inclusive
|
||||||
|
values plus a second, redundant chrome inset composed on top). After B1
|
||||||
|
removes that composition, the mounted clamp is now literally the host's
|
||||||
|
own four probed values with zero arithmetic applied — the sentence holds
|
||||||
|
for real. No register row for the S4 content-height default either: 362
|
||||||
|
is the same host content-parent width/height CT6 already probed and
|
||||||
|
cited (`0x10000180`, 300×362), not a new number.
|
||||||
|
|
||||||
**CT7 — Connected gate.** Test script
|
**CT7 — Connected gate.** Test script
|
||||||
(`docs/research/2026-08-25-campaign-ct-test-script.md`), owner drive:
|
(`docs/research/2026-08-25-campaign-ct-test-script.md`), owner drive:
|
||||||
|
|
|
||||||
|
|
@ -641,26 +641,122 @@ root or the slot.
|
||||||
`MountCharacter` now imports `ElementInfo? hostConstraint =
|
`MountCharacter` now imports `ElementInfo? hostConstraint =
|
||||||
LayoutImporter.ImportInfos(dats, 0x2100006Eu, 0x100005FEu)` (the same
|
LayoutImporter.ImportInfos(dats, 0x2100006Eu, 0x100005FEu)` (the same
|
||||||
targeted single-root overload CT1 established for row templates) and
|
targeted single-root overload CT1 established for row templates) and
|
||||||
passes it as `RetailWindowFrame.Options.DatConstraintSource`. Chrome
|
passes it as `RetailWindowFrame.Options.DatConstraintSource`.
|
||||||
inset (`2 * RetailChromeSprites.Border` = 10px, NineSlice) is added
|
|
||||||
automatically by `RetailWindowFrame.ResolveConstraint`, giving the
|
**CORRECTED (CT6 fix round, BLOCKER B1, 2026-08-25):** this section
|
||||||
mounted outer frame MinWidth=MaxWidth=320, MinHeight=382, MaxHeight=1010.
|
originally claimed the NineSlice chrome inset (`2 *
|
||||||
|
RetailChromeSprites.Border` = 10px) "is added automatically by
|
||||||
|
`RetailWindowFrame.ResolveConstraint`, giving the mounted outer frame
|
||||||
|
MinWidth=MaxWidth=320, MinHeight=382, MaxHeight=1010." That was WRONG.
|
||||||
|
Host `0x100005FE` is not a bare content element our own NineSlice
|
||||||
|
wrapper adds chrome to — per the geometry dumped above, it IS retail's
|
||||||
|
own complete outer window frame: 5px bevel + the 300×362 content parent
|
||||||
|
(`0x10000180`) + 5px = 310×372 exactly. Its authored 0x3C..0x3F values
|
||||||
|
are therefore already CHROME-INCLUSIVE. Composing the wrapper's own 10px
|
||||||
|
inset on top of an already chrome-inclusive source double-counted the
|
||||||
|
bevel: the mounted window's clamp said MinWidth=320 while its actual
|
||||||
|
mounted outer width was only 310 — the window opened already violating
|
||||||
|
its own minimum, silently "fixed" at runtime only because
|
||||||
|
`RetailWindowManager.ResizeTo`'s main-panel geometry sync forcibly
|
||||||
|
widened it to 320 despite `ResizeX=false`, which would have produced a
|
||||||
|
visible 15px right-bevel seam against the other eight main panels
|
||||||
|
sharing that sync.
|
||||||
|
|
||||||
|
**Fix:** a new opt-out,
|
||||||
|
`RetailWindowFrame.Options.DatConstraintSourceIsOuterFrame` (default
|
||||||
|
`false`, preserving every other window's existing content-plus-chrome
|
||||||
|
behavior), tells `ResolveConstraint` to apply a chrome inset of 0 when
|
||||||
|
the DAT source is itself already the outer frame. `MountCharacter` sets
|
||||||
|
it `true` for `hostConstraint`. The mounted outer clamps are now EXACTLY
|
||||||
|
the host's four raw values, no composed arithmetic: **MinWidth=
|
||||||
|
MaxWidth=310, MinHeight=372, MaxHeight=1000.** A new mount-time
|
||||||
|
invariant in `RetailWindowFrame.Mount` (throws if the just-mounted outer
|
||||||
|
extent falls outside its own just-computed clamp) guards against this
|
||||||
|
class of bug recurring for any window this path mounts.
|
||||||
`ResizeX=false`/`ResizableEdges=Bottom` (already correct in the
|
`ResizeX=false`/`ResizableEdges=Bottom` (already correct in the
|
||||||
pre-CT6 code) match the fixed-width/bottom-only-Resizebar authoring
|
pre-CT6 code) match the fixed-width/bottom-only-Resizebar authoring
|
||||||
exactly — no change needed there.
|
exactly — no change needed there.
|
||||||
|
|
||||||
|
### Which size governs the mount default (S4, campaign-lead ruling, 2026-08-25)
|
||||||
|
|
||||||
|
The "Size tension" flagged earlier in this doc (§4, "Correction to the
|
||||||
|
plan") — `0x2100002E`'s own root authored 300×600 vs. the host slot's
|
||||||
|
300×362 — is now resolved: **372px outer (362px content) is the number
|
||||||
|
that governs the MOUNTED DEFAULT**, and 600 is the content's own
|
||||||
|
authored design canvas that retail scroll-clips into the much smaller
|
||||||
|
host slot, never the size the window actually opens at. Concretely:
|
||||||
|
`0x2100002E`'s 300×600 root exists because its Titles page alone is
|
||||||
|
authored 300×575 (Y=25 offset + the 25px tab bar — §3 above) — that is
|
||||||
|
real, authored geometry, and the Titles page correctly stretches to fill
|
||||||
|
whatever height the mounted window offers via its own `LayoutPolicy`
|
||||||
|
(confirmed below). But retail never displays that full 600px canvas at
|
||||||
|
once outside of the Titles tab's own internal scroll: the shared
|
||||||
|
`gmPanelUI` host's content parent (`0x10000180`) is fixed at 300×362,
|
||||||
|
and 372 (362 + the 10px chrome bevel) is exactly the host's own
|
||||||
|
authored MinHeight — i.e., retail's Character/Skills window OPENS at
|
||||||
|
its own resize floor and can only be dragged taller, never shorter.
|
||||||
|
Pre-fix, `MountCharacter` left `Options.ContentHeight` unset, so it fell
|
||||||
|
back to `content.Width`/`content.Height` — the raw 600px canvas — giving
|
||||||
|
a stale 610px (600 + 10 chrome) mounted default that was never retail's
|
||||||
|
actual opening size and was 238px taller than the true floor. Fixed by
|
||||||
|
setting `Options.ContentHeight = 362f` explicitly (the same host
|
||||||
|
content-parent height this section already probed and cited, not a new
|
||||||
|
number). At the corrected 372px default, the authored page composition
|
||||||
|
(header 112px + list 160px + divider + footer) is the true 362px design;
|
||||||
|
the 9 attribute/vital rows (9 × 20 = 180px content) OVERFLOW the 160px
|
||||||
|
list immediately, so the stat list's scrollbar is active from the moment
|
||||||
|
the window opens — this is retail-correct (see §S2 below for what
|
||||||
|
"active" means given the corrected `HideWhenDisabled` finding), not a
|
||||||
|
regression introduced by the fix.
|
||||||
|
|
||||||
### Titles-page list reflow
|
### Titles-page list reflow
|
||||||
|
|
||||||
The Titles page's own container (`0x10000539`) and its ListBox
|
The Titles page's own container (`0x10000539`) and its ListBox
|
||||||
(`0x10000532`) both already carry a REAL authored `LayoutPolicy` in the
|
(`0x10000532`) both already carry a REAL authored `LayoutPolicy` in the
|
||||||
committed/installed layout (live-verified: `LayoutPolicy is not null`
|
committed/installed layout (live-verified: `LayoutPolicy is not null`
|
||||||
for both) that correctly stretches with the mounted content's height —
|
for both) that correctly stretches with the mounted content's height —
|
||||||
no code was needed to make the PAGE itself reflow. The only missing
|
no code was needed to make the PAGE itself reflow.
|
||||||
piece was the ListBox's own compatibility fallback (`Anchors =
|
|
||||||
Left|Top|Bottom`, engaged only when `LayoutPolicy is null` — a no-op on
|
**CORRECTED (CT6 fix round, S3, 2026-08-25):** this section originally
|
||||||
the real DAT, but needed for synthetic/test layouts and matches the
|
went on to claim "the only missing piece was the ListBox's own
|
||||||
identical pattern already used for `CharacterStatController`'s
|
compatibility fallback (`Anchors = Left|Top|Bottom`, engaged only when
|
||||||
`statList`), added in `CharacterTitlesController.Bind`.
|
`LayoutPolicy is null` — a no-op on the real DAT, but needed for
|
||||||
|
synthetic/test layouts...), added in `CharacterTitlesController.Bind`."
|
||||||
|
That framing was wrong: both `0x10000532` and `0x10000539` author
|
||||||
|
`HasOriginalParentSize=true` (the field `DatWidgetFactory` gates
|
||||||
|
`LayoutPolicy` assignment on), confirmed both on the real installed DAT
|
||||||
|
and the committed fixture — `LayoutImporter`/`DatWidgetFactory` ALWAYS
|
||||||
|
assigns a real `LayoutPolicy` to these elements, so the `if
|
||||||
|
(listBox.LayoutPolicy is null)` branch never ran anywhere, not even in
|
||||||
|
the "synthetic/test layouts" case it was written to cover. It was dead
|
||||||
|
code, not a harmless no-op fallback. Deleted from
|
||||||
|
`CharacterTitlesController.Bind`; a new
|
||||||
|
`CharacterPanelLiveDatTests.TitlesListAndPage_AuthorHasOriginalParentSize`
|
||||||
|
pin asserts `HasOriginalParentSize` on both `0x10000532` and `0x10000539`
|
||||||
|
to guard the deletion against future DAT drift.
|
||||||
|
|
||||||
|
### Correction to the scrollbar-visibility finding (S2, 2026-08-25)
|
||||||
|
|
||||||
|
The CT6 landing notes (in the campaign plan ledger) claimed
|
||||||
|
`UiScrollbar`'s own `IsPresentationVisible`/`IsModelDisabled` "already
|
||||||
|
draw the correct full-track 'disabled' thumb when content fits
|
||||||
|
(`HideWhenDisabled` defaults false)." That had the authored default
|
||||||
|
BACKWARDS. Both `0x1000023E` (the shared Attributes/Skills list
|
||||||
|
scrollbar) and `0x10000533` (the Titles list's own scrollbar) author
|
||||||
|
property `0x79` (`HideWhenDisabled`) **TRUE** in the committed fixture —
|
||||||
|
verified directly against `tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json`,
|
||||||
|
property `"121"` (=0x79) carries `"BoolValue": true` on both elements. A
|
||||||
|
fitting list HIDES the bar entirely (`IsPresentationVisible` false); it
|
||||||
|
does not leave a full-track "disabled" thumb on screen. The CODE in
|
||||||
|
`CharacterStatController.RebuildActiveList` was already correct —
|
||||||
|
`.Visible = true` only keeps the bar bound in the tree, and
|
||||||
|
`IsPresentationVisible` is the actual show/hide computation — only the
|
||||||
|
description was wrong. Given the S4 correction above (mount default is
|
||||||
|
now the compact 372px floor), the practical consequence is: the stat
|
||||||
|
list's scrollbar is VISIBLE and interactive from the moment the window
|
||||||
|
opens (rows overflow at the default), and DISAPPEARS once the window is
|
||||||
|
grown enough that all rows fit — the opposite of what the uncorrected
|
||||||
|
description implied.
|
||||||
|
|
||||||
### A latent anchor-baseline bug this slice surfaced and fixed
|
### A latent anchor-baseline bug this slice surfaced and fixed
|
||||||
|
|
||||||
|
|
@ -687,3 +783,15 @@ by `CharacterStatControllerTests
|
||||||
Every number in this section is either a live-probed authored DAT value
|
Every number in this section is either a live-probed authored DAT value
|
||||||
or a structural wiring/anchor-capture-correctness fix — nothing here is
|
or a structural wiring/anchor-capture-correctness fix — nothing here is
|
||||||
inferred or approximated.
|
inferred or approximated.
|
||||||
|
|
||||||
|
**CORRECTED (CT6 fix round, N5, 2026-08-25):** at initial landing, this
|
||||||
|
sentence was not actually true — the mounted 320/382/1010 clamp WAS an
|
||||||
|
inference (BLOCKER B1: the host's already chrome-inclusive values with a
|
||||||
|
second, redundant chrome inset composed on top). After the B1 fix
|
||||||
|
removes that composition, the mounted clamp is now literally the host's
|
||||||
|
own four probed values (310/310/372/1000) with zero arithmetic applied,
|
||||||
|
so the sentence holds for real. The S4 content-height default (362) is
|
||||||
|
likewise not a new inferred number — it is the same host content-parent
|
||||||
|
width/height (`0x10000180`, 300×362) this section already probed and
|
||||||
|
cited above, applied to `Options.ContentHeight` instead of being left
|
||||||
|
unset.
|
||||||
|
|
|
||||||
|
|
@ -75,48 +75,61 @@ Useful ACE console helpers: title grants come from quests/admin — check
|
||||||
|
|
||||||
Ground truth (2026-08-25 live probe against layout `0x2100006E`, host
|
Ground truth (2026-08-25 live probe against layout `0x2100006E`, host
|
||||||
`0x100005FE` — `docs/research/2026-08-24-campaign-ct-dat-ground-truth.md`
|
`0x100005FE` — `docs/research/2026-08-24-campaign-ct-dat-ground-truth.md`
|
||||||
§CT6): the resize clamp is authored on the SHARED `gmPanelUI` host, not
|
§CT6, corrected by the CT6 fix round's BLOCKER B1): the resize clamp is
|
||||||
the character content itself. Host authors **MinWidth=MaxWidth=310**
|
authored on the SHARED `gmPanelUI` host, not the character content
|
||||||
(fixed width — no horizontal Resizebar) and **MinHeight=372,
|
itself, and the host IS retail's own outer window frame — its authored
|
||||||
MaxHeight=1000**. With the NineSlice chrome's 10px inset, the MOUNTED
|
values are chrome-INCLUSIVE, not a content size our own chrome adds on
|
||||||
window's outer bounds are MinHeight≈382px, MaxHeight≈1010px, width
|
top of. Host authors **MinWidth=MaxWidth=310** (fixed width — no
|
||||||
fixed ≈320px.
|
horizontal Resizebar) and **MinHeight=372, MaxHeight=1000**. The
|
||||||
|
MOUNTED window's outer bounds are EXACTLY those same numbers: width
|
||||||
|
fixed **310px**, floor **372px**, ceiling **1000px** — no inset is
|
||||||
|
added on top (`RetailWindowFrame.Options.DatConstraintSourceIsOuterFrame`
|
||||||
|
now tells the mount path this source already includes the bevel).
|
||||||
|
**Starting height:** the window OPENS at retail's authored default,
|
||||||
|
**372px** — its own resize floor. It cannot open any shorter; it can
|
||||||
|
only be dragged taller.
|
||||||
|
|
||||||
1. **Grab the bottom edge and drag up (shrink).** PASS: the window
|
1. **Grab the bottom edge and drag up (shrink).** PASS: the window
|
||||||
stops shrinking at its authored floor (≈382px outer / the point where
|
stops shrinking at its authored floor (372px outer / the point where
|
||||||
further dragging has no visible effect) — it does NOT collapse
|
further dragging has no visible effect) — since the window already
|
||||||
arbitrarily small, and it does NOT refuse to shrink at all (the
|
OPENS at that floor, this step should show no shrink at all (there
|
||||||
pre-CT6 bug). Retail comparison: drag retail's own Character/Skills
|
is no room below the default to shrink into). It does NOT collapse
|
||||||
window to its floor side-by-side; both should bottom out at
|
arbitrarily small. Retail comparison: drag retail's own Character/
|
||||||
proportionally the same point relative to their starting size.
|
Skills window down from its own default; it should likewise refuse
|
||||||
|
to shrink further immediately.
|
||||||
2. **Keep dragging down (grow).** PASS: the window keeps growing until
|
2. **Keep dragging down (grow).** PASS: the window keeps growing until
|
||||||
its authored ceiling (≈1010px outer) — same side-by-side comparison
|
its authored ceiling (1000px outer) — same side-by-side comparison
|
||||||
against retail's own ceiling.
|
against retail's own ceiling.
|
||||||
3. **Left/right edges do not resize.** Only the bottom edge (and top
|
3. **Left/right edges do not resize.** Only the bottom edge (and top
|
||||||
Dragbar for moving, not resizing) responds — matches retail's
|
Dragbar for moving, not resizing) responds — matches retail's
|
||||||
fixed-width authoring (no horizontal Resizebar).
|
fixed-width authoring (no horizontal Resizebar).
|
||||||
4. **Scrollbar hand-off, Attributes tab.** At the default window size
|
4. **Scrollbar hand-off, Attributes tab.** At the default window size
|
||||||
the 9 attribute/vital rows fit without a visible scroll gap (bar
|
(372px) the 9 attribute/vital rows (180px content) OVERFLOW the
|
||||||
shows retail's full-track "disabled" thumb — present, not hidden,
|
160px list — the scrollbar is ACTIVE (visible + interactive)
|
||||||
per the 2026-08-24 scrollbar work's contract). Shrink the window
|
IMMEDIATELY on open, not after shrinking. PASS: this is the owner's
|
||||||
until the rows overflow: the bar's thumb shrinks proportionally and
|
item 2 fix — previously the scrollbar never appeared on Attributes
|
||||||
becomes interactive (drag, click-to-page, wheel-scroll all move the
|
at all. Grow the window until the rows fit without scrolling: the
|
||||||
list). PASS: this is the owner's item 2 fix — previously the
|
bar DISAPPEARS entirely (0x1000023E authors 0x79 hide-when-disabled
|
||||||
scrollbar never appeared on Attributes at all.
|
TRUE — a fitting list hides the bar, it does not leave a full-track
|
||||||
5. **Scrollbar hand-off, Skills tab.** Same shrink/overflow check —
|
"disabled" thumb visible). Shrink back down and the bar reappears.
|
||||||
should already have worked pre-CT6; confirm no regression.
|
5. **Scrollbar hand-off, Skills tab.** Same immediate-overflow-at-
|
||||||
6. **Scrollbar hand-off, Titles tab.** With several earned titles,
|
default check (a longer skills list only makes the overflow more
|
||||||
shrink the window until the Titles list (authored 455px, inside the
|
obvious); grow until it fits and confirm the bar disappears the same
|
||||||
575px page) overflows its available space — the list's own scrollbar
|
way.
|
||||||
(`0x10000533`) takes over the same way.
|
6. **Scrollbar hand-off, Titles tab.** With several earned titles, the
|
||||||
|
Titles list (authored 455px, inside the 575px page) is scroll-clipped
|
||||||
|
into the same 372px-default window and its own scrollbar
|
||||||
|
(`0x10000533`, also hide-when-disabled — fixture-verified) takes over
|
||||||
|
the same way: active when titles overflow, hidden when the window is
|
||||||
|
grown enough that they all fit.
|
||||||
7. **Footer stays bottom-docked.** While shrinking/growing on the
|
7. **Footer stays bottom-docked.** While shrinking/growing on the
|
||||||
Attributes/Skills tabs, the footer (raise buttons / selected-stat
|
Attributes/Skills tabs, the footer (raise buttons / selected-stat
|
||||||
info) stays pinned to the bottom edge — it does not float mid-window
|
info) stays pinned to the bottom edge — it does not float mid-window
|
||||||
or get clipped early.
|
or get clipped early.
|
||||||
8. **Grow back restores.** Drag back to the original size: the lists
|
8. **Grow back restores.** Drag back down to the original default
|
||||||
return to showing all rows without scrolling (scrollbar reverts to
|
(372px): the lists return to their default OVERFLOWING state
|
||||||
the full-track disabled state) and the window returns to its
|
(scrollbar reactivates — this is the default, not "all rows fit")
|
||||||
original proportions.
|
and the window returns to its original proportions.
|
||||||
9. Other windows (chat, social) still clamp at their own authored
|
9. Other windows (chat, social) still clamp at their own authored
|
||||||
minimums — regression check (chat: min 300×100, max 2000×2000 per
|
minimums — regression check (chat: min 300×100, max 2000×2000 per
|
||||||
`CharacterPanelLiveDatTests.ChatWindowRoot_AuthorsExplicitSizeConstraints`).
|
`CharacterPanelLiveDatTests.ChatWindowRoot_AuthorsExplicitSizeConstraints`).
|
||||||
|
|
|
||||||
|
|
@ -810,13 +810,18 @@ public static class CharacterStatController
|
||||||
allRaise1, allRaise10, SetFooterSelected, iconDidResolve);
|
allRaise1, allRaise10, SetFooterSelected, iconDidResolve);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always bound + visible for whichever tab is active — retail's
|
// Always BOUND for whichever tab is active — no per-tab visibility
|
||||||
// authored scrollbar gutter (0x1000023E, 281..297 within the
|
// toggle is needed here. CT6 fix round (S2 correction): the
|
||||||
// 300px list) is reserved regardless of content, and UiScrollbar
|
// scrollbar's own .Visible=true just keeps it in the tree; what
|
||||||
// itself already draws the correct full-track "disabled" thumb
|
// actually shows or hides it on screen is UiScrollbar's
|
||||||
// when Model.HasOverflow is false (HideWhenDisabled defaults to
|
// IsPresentationVisible, and 0x1000023E authors 0x79
|
||||||
// false — see UiScrollbar.IsPresentationVisible), so no
|
// (HideWhenDisabled) TRUE (fixture-verified — the 121/0x79
|
||||||
// per-tab visibility toggle is needed here any more.
|
// 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)
|
if (skillScrollbar is not null)
|
||||||
{
|
{
|
||||||
skillScrollbar.Model = viewport.Scroll;
|
skillScrollbar.Model = viewport.Scroll;
|
||||||
|
|
|
||||||
|
|
@ -183,17 +183,23 @@ public sealed class CharacterTitlesController : IDisposable
|
||||||
// the same way for the same reason).
|
// the same way for the same reason).
|
||||||
listBox.LineHeight = 24;
|
listBox.LineHeight = 24;
|
||||||
// CT6 (2026-08-25): the Titles list (authored H=455 inside the
|
// CT6 (2026-08-25): the Titles list (authored H=455 inside the
|
||||||
// 575px page — CT1 ground truth §3) must shrink/grow with the
|
// 575px page — CT1 ground truth §3) shrinks/grows with the window
|
||||||
// window the same way CharacterStatController's attribute/skill
|
// the same way CharacterStatController's attribute/skill list does.
|
||||||
// list does — same compatibility-anchor pattern, only applied when
|
// CT6 fix round (S3 correction): the former Anchors fallback here
|
||||||
// the imported element carries no authored edge policy of its own
|
// ("if LayoutPolicy is null") was DEAD CODE — both 0x10000532 (this
|
||||||
// (an authored LayoutPolicy always wins). UiTemplateListBox's own
|
// 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)
|
// internal viewport (created lazily inside AddItemFromTemplateList)
|
||||||
// already carries the #372-class eager-baseline-capture fix, so
|
// already carries the #372-class eager-baseline-capture fix, so once
|
||||||
// once the ListBox itself reflows, its scrollbar (bound below) picks
|
// the ListBox itself reflows, its scrollbar (bound below) picks up
|
||||||
// up the new content/view relationship for free.
|
// the new content/view relationship for free.
|
||||||
if (listBox.LayoutPolicy is null)
|
|
||||||
listBox.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom;
|
|
||||||
|
|
||||||
uint scrollbarElementId = listBox.ScrollbarElementId;
|
uint scrollbarElementId = listBox.ScrollbarElementId;
|
||||||
UiElement? scrollbarElement = scrollbarElementId == 0
|
UiElement? scrollbarElement = scrollbarElementId == 0
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,30 @@ public static class RetailWindowFrame
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional DAT element whose effective 0x3C..0x3F properties provide
|
/// Optional DAT element whose effective 0x3C..0x3F properties provide
|
||||||
/// max-height, max-width, min-height, and min-width respectively. Explicit
|
/// 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>
|
/// </summary>
|
||||||
public ElementInfo? DatConstraintSource { get; init; }
|
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? MinWidth { get; init; }
|
||||||
public float? MinHeight { get; init; }
|
public float? MinHeight { get; init; }
|
||||||
public float? MaxWidth { get; init; }
|
public float? MaxWidth { get; init; }
|
||||||
|
|
@ -157,16 +178,36 @@ public static class RetailWindowFrame
|
||||||
outerFrame.Opacity = Math.Clamp(options.Opacity, 0f, 1f);
|
outerFrame.Opacity = Math.Clamp(options.Opacity, 0f, 1f);
|
||||||
outerFrame.Visible = options.Visible;
|
outerFrame.Visible = options.Visible;
|
||||||
|
|
||||||
|
int constraintInset = options.DatConstraintSourceIsOuterFrame ? 0 : inset;
|
||||||
outerFrame.MinWidth = ResolveConstraint(
|
outerFrame.MinWidth = ResolveConstraint(
|
||||||
options.MinWidth, options.DatConstraintSource, 0x3Fu, outerWidth, inset);
|
options.MinWidth, options.DatConstraintSource, 0x3Fu, outerWidth, constraintInset);
|
||||||
outerFrame.MinHeight = ResolveConstraint(
|
outerFrame.MinHeight = ResolveConstraint(
|
||||||
options.MinHeight, options.DatConstraintSource, 0x3Eu, outerHeight, inset);
|
options.MinHeight, options.DatConstraintSource, 0x3Eu, outerHeight, constraintInset);
|
||||||
outerFrame.MaxWidth = Math.Max(
|
outerFrame.MaxWidth = Math.Max(
|
||||||
outerFrame.MinWidth,
|
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.MaxHeight = Math.Max(
|
||||||
outerFrame.MinHeight,
|
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,
|
// 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
|
// 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
|
// host 0x100005FE authors MinWidth=310 MinHeight=372 MaxWidth=310
|
||||||
// MaxHeight=1000 (Min==Max width: no horizontal Resizebar is
|
// MaxHeight=1000 (Min==Max width: no horizontal Resizebar is
|
||||||
// authored, matching ResizeX=false below).
|
// 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;
|
ElementInfo? hostConstraint;
|
||||||
lock (_bindings.Assets.DatLock)
|
lock (_bindings.Assets.DatLock)
|
||||||
hostConstraint = LayoutImporter.ImportInfos(
|
hostConstraint = LayoutImporter.ImportInfos(
|
||||||
|
|
@ -4137,11 +4148,25 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
Chrome = RetailWindowChrome.NineSlice,
|
Chrome = RetailWindowChrome.NineSlice,
|
||||||
Left = 540f,
|
Left = 540f,
|
||||||
Top = 18f,
|
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,
|
ResizeX = false,
|
||||||
ResizeY = true,
|
ResizeY = true,
|
||||||
ResizableEdges = ResizeEdges.Bottom,
|
ResizableEdges = ResizeEdges.Bottom,
|
||||||
ConstrainResizeToParent = true,
|
ConstrainResizeToParent = true,
|
||||||
DatConstraintSource = hostConstraint,
|
DatConstraintSource = hostConstraint,
|
||||||
|
DatConstraintSourceIsOuterFrame = true,
|
||||||
Visible = false,
|
Visible = false,
|
||||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||||
ContentClickThrough = false,
|
ContentClickThrough = false,
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,38 @@ public sealed class CharacterPanelLiveDatTests
|
||||||
Assert.Equal("Ghosted", setDisplayButton.DefaultStateName);
|
Assert.Equal("Ghosted", setDisplayButton.DefaultStateName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CT6 fix round (S3, 2026-08-25): guards the deletion of
|
||||||
|
/// <c>CharacterTitlesController.Bind</c>'s Anchors fallback
|
||||||
|
/// (<c>if (listBox.LayoutPolicy is null) listBox.Anchors = ...</c>)
|
||||||
|
/// against DAT drift. That fallback was proven UNREACHABLE — both the
|
||||||
|
/// Titles page container (<c>0x10000539</c>) and its ListBox
|
||||||
|
/// (<c>0x10000532</c>) author <c>HasOriginalParentSize=true</c>, the
|
||||||
|
/// field <c>DatWidgetFactory</c> gates real <c>LayoutPolicy</c>
|
||||||
|
/// assignment on (<c>DatWidgetFactory.cs</c>: <c>if
|
||||||
|
/// (info.HasOriginalParentSize) e.LayoutPolicy =
|
||||||
|
/// CreateLayoutPolicy(info)</c>), so <c>LayoutPolicy</c> is never
|
||||||
|
/// null for either element and the fallback branch never ran, on
|
||||||
|
/// either the installed DAT or the committed fixture. If a future
|
||||||
|
/// DAT revision ever authors either element WITHOUT
|
||||||
|
/// OriginalParentSize, this pin fails loudly — the signal to restore
|
||||||
|
/// a real fallback rather than silently losing Titles-list resize
|
||||||
|
/// behavior.
|
||||||
|
/// </summary>
|
||||||
|
[InstalledDatFact]
|
||||||
|
public void TitlesListAndPage_AuthorHasOriginalParentSize()
|
||||||
|
{
|
||||||
|
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
|
||||||
|
ElementInfo? tree = LayoutImporter.ImportInfos(dats, 0x2100002Eu);
|
||||||
|
Assert.NotNull(tree);
|
||||||
|
|
||||||
|
ElementInfo page = Assert.Single(Flatten(tree!), e => e.Id == 0x10000539u);
|
||||||
|
Assert.True(page.HasOriginalParentSize);
|
||||||
|
|
||||||
|
ElementInfo listBox = Assert.Single(page.Children, c => c.Id == 0x10000532u);
|
||||||
|
Assert.True(listBox.HasOriginalParentSize);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The title row template (<c>0x10000536</c> in LayoutDesc
|
/// The title row template (<c>0x10000536</c> in LayoutDesc
|
||||||
/// <c>0x2100005E</c>) — a single-line text row, no icon column, 24px
|
/// <c>0x2100005E</c>) — a single-line text row, no icon column, 24px
|
||||||
|
|
|
||||||
|
|
@ -2310,15 +2310,21 @@ public class CharacterStatControllerTests
|
||||||
Assert.Equal((510f, 7f), (divider.Top, divider.Height));
|
Assert.Equal((510f, 7f), (divider.Top, divider.Height));
|
||||||
Assert.Equal(3, footers.Count);
|
Assert.Equal(3, footers.Count);
|
||||||
Assert.All(footers, footer => Assert.Equal((520f, 55f), (footer.Top, footer.Height)));
|
Assert.All(footers, footer => Assert.Equal((520f, 55f), (footer.Top, footer.Height)));
|
||||||
// CT6 (2026-08-25): the shared scrollbar is now always bound + visible,
|
// CT6 (2026-08-25): the shared scrollbar is now always BOUND
|
||||||
// on Attributes as well as Skills — retail's authored gutter
|
// (.Model set, .Visible = true), on Attributes as well as Skills —
|
||||||
// (0x1000023E) is reserved regardless of content, and UiScrollbar's
|
// retail's authored gutter (0x1000023E) is reserved regardless of
|
||||||
// own IsPresentationVisible/IsModelDisabled already draw the correct
|
// content, so no per-tab visibility toggle is needed. CT6 fix round
|
||||||
// full-track "disabled" thumb when content fits (HideWhenDisabled
|
// (S2 correction): 0x1000023E authors 0x79 (HideWhenDisabled) TRUE
|
||||||
// defaults to false). The 9 attribute/vital rows (180px content) fit
|
// (fixture-verified), so a FITTING list actually HIDES the bar
|
||||||
// comfortably inside this fixture's 398px view, so HasOverflow is
|
// entirely via IsPresentationVisible — it does not draw a full-track
|
||||||
// false here — a shrunk window is what flips it true (see the
|
// "disabled" thumb the way the previous wording here implied. This
|
||||||
// window-level resize test).
|
// fixture mounts the raw 600px-tall content directly (no
|
||||||
|
// RetailWindowFrame.Mount, no S4 372px default), so the 9
|
||||||
|
// attribute/vital rows (180px content) fit comfortably inside its
|
||||||
|
// 398px view and HasOverflow is false here — a shrunk window (or the
|
||||||
|
// real S4-corrected 372px mount default) is what flips it true and
|
||||||
|
// makes the bar presentation-visible (see the window-level resize
|
||||||
|
// test, which asserts IsPresentationVisible both ways).
|
||||||
Assert.True(
|
Assert.True(
|
||||||
scrollbar.Visible,
|
scrollbar.Visible,
|
||||||
$"attributes-tab scrollbar: model={scrollbar.Model is not null}, " +
|
$"attributes-tab scrollbar: model={scrollbar.Model is not null}, " +
|
||||||
|
|
@ -2355,6 +2361,14 @@ public class CharacterStatControllerTests
|
||||||
/// pattern <see cref="RetailWindowFrameTests"/> already uses), matching
|
/// pattern <see cref="RetailWindowFrameTests"/> already uses), matching
|
||||||
/// production's actual wire-up in <c>RetailUiRuntime.MountCharacter</c>
|
/// production's actual wire-up in <c>RetailUiRuntime.MountCharacter</c>
|
||||||
/// (which sources the SAME host element live from the DAT).
|
/// (which sources the SAME host element live from the DAT).
|
||||||
|
///
|
||||||
|
/// <para>CT6 fix round (2026-08-25, BLOCKER B1): the host source IS
|
||||||
|
/// retail's own outer frame — its authored 310/372/310/1000 are already
|
||||||
|
/// chrome-inclusive (5px bevel + 300x362 content + 5px = 310x372) — so
|
||||||
|
/// this test now sets <see cref="RetailWindowFrame.Options.DatConstraintSourceIsOuterFrame"/>
|
||||||
|
/// the same way production's <c>MountCharacter</c> does, and the mounted
|
||||||
|
/// clamp equals the host's four raw values EXACTLY, with no chrome inset
|
||||||
|
/// composed on top.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CharacterWindow_ResizesYWithinAuthoredHostClamp_AndReflowsListAndScrollbar()
|
public void CharacterWindow_ResizesYWithinAuthoredHostClamp_AndReflowsListAndScrollbar()
|
||||||
|
|
@ -2384,6 +2398,7 @@ public class CharacterStatControllerTests
|
||||||
ResizableEdges = ResizeEdges.Bottom,
|
ResizableEdges = ResizeEdges.Bottom,
|
||||||
ConstrainResizeToParent = true,
|
ConstrainResizeToParent = true,
|
||||||
DatConstraintSource = HostConstraints(),
|
DatConstraintSource = HostConstraints(),
|
||||||
|
DatConstraintSourceIsOuterFrame = true,
|
||||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2400,13 +2415,14 @@ public class CharacterStatControllerTests
|
||||||
e => e.DatElementId == CharacterStatController.FooterStateAId);
|
e => e.DatElementId == CharacterStatController.FooterStateAId);
|
||||||
var viewport = Assert.IsType<UiScrollablePanel>(list.Children.Single());
|
var viewport = Assert.IsType<UiScrollablePanel>(list.Children.Single());
|
||||||
|
|
||||||
// Chrome inset = 2 * RetailChromeSprites.Border (5px) = 10; the DAT's
|
// CT6 fix round (BLOCKER B1): the host source IS the outer frame
|
||||||
// 310/372/310/1000 host values become 320/382/320/1010 on the
|
// already (DatConstraintSourceIsOuterFrame = true above), so no
|
||||||
// NineSlice-wrapped outer frame.
|
// chrome inset is composed on top — the mounted clamp is EXACTLY the
|
||||||
Assert.Equal(320f, handle.OuterFrame.MinWidth);
|
// DAT's own 310/372/310/1000.
|
||||||
Assert.Equal(320f, handle.OuterFrame.MaxWidth);
|
Assert.Equal(310f, handle.OuterFrame.MinWidth);
|
||||||
Assert.Equal(382f, handle.OuterFrame.MinHeight);
|
Assert.Equal(310f, handle.OuterFrame.MaxWidth);
|
||||||
Assert.Equal(1010f, handle.OuterFrame.MaxHeight);
|
Assert.Equal(372f, handle.OuterFrame.MinHeight);
|
||||||
|
Assert.Equal(1000f, handle.OuterFrame.MaxHeight);
|
||||||
|
|
||||||
float originalOuterHeight = handle.Height;
|
float originalOuterHeight = handle.Height;
|
||||||
float originalListHeight = list.Height;
|
float originalListHeight = list.Height;
|
||||||
|
|
@ -2418,9 +2434,9 @@ public class CharacterStatControllerTests
|
||||||
"the fixture's authored default height fits all 9 rows without scrolling");
|
"the fixture's authored default height fits all 9 rows without scrolling");
|
||||||
|
|
||||||
// Request far below the authored minimum — the clamp must hold at
|
// Request far below the authored minimum — the clamp must hold at
|
||||||
// 382, not the requested value.
|
// 372, not the requested value.
|
||||||
handle.ResizeTo(handle.Width, 50f);
|
handle.ResizeTo(handle.Width, 50f);
|
||||||
Assert.Equal(382f, handle.Height);
|
Assert.Equal(372f, handle.Height);
|
||||||
|
|
||||||
ApplyLayoutPass(handle.OuterFrame);
|
ApplyLayoutPass(handle.OuterFrame);
|
||||||
viewport.LayoutScrollableChildren();
|
viewport.LayoutScrollableChildren();
|
||||||
|
|
@ -2430,6 +2446,12 @@ public class CharacterStatControllerTests
|
||||||
Assert.True(
|
Assert.True(
|
||||||
scrollbar.Model!.HasOverflow,
|
scrollbar.Model!.HasOverflow,
|
||||||
"9 rows (180px content) must overflow the shrunk view");
|
"9 rows (180px content) must overflow the shrunk view");
|
||||||
|
// S2 correction: 0x1000023E authors 0x79 (HideWhenDisabled) TRUE —
|
||||||
|
// an overflowing list must show its scrollbar VISIBLE and
|
||||||
|
// interactive, not merely bound-but-invisible.
|
||||||
|
Assert.True(
|
||||||
|
scrollbar.IsPresentationVisible,
|
||||||
|
"an overflowing list's scrollbar must be presentation-visible (interactive), not hidden");
|
||||||
|
|
||||||
// The footer stays bottom-docked: same distance from the stat
|
// The footer stays bottom-docked: same distance from the stat
|
||||||
// layout's own bottom edge before and after the shrink.
|
// layout's own bottom edge before and after the shrink.
|
||||||
|
|
@ -2437,15 +2459,21 @@ public class CharacterStatControllerTests
|
||||||
Assert.Equal(originalFooterBottomGap, shrunkFooterBottomGap, precision: 2);
|
Assert.Equal(originalFooterBottomGap, shrunkFooterBottomGap, precision: 2);
|
||||||
|
|
||||||
// Request far above the authored maximum — the clamp must hold at
|
// Request far above the authored maximum — the clamp must hold at
|
||||||
// 1010, not the requested value.
|
// 1000, not the requested value.
|
||||||
handle.ResizeTo(handle.Width, 5000f);
|
handle.ResizeTo(handle.Width, 5000f);
|
||||||
Assert.Equal(1010f, handle.Height);
|
Assert.Equal(1000f, handle.Height);
|
||||||
|
|
||||||
ApplyLayoutPass(handle.OuterFrame);
|
ApplyLayoutPass(handle.OuterFrame);
|
||||||
viewport.LayoutScrollableChildren();
|
viewport.LayoutScrollableChildren();
|
||||||
Assert.False(
|
Assert.False(
|
||||||
scrollbar.Model!.HasOverflow,
|
scrollbar.Model!.HasOverflow,
|
||||||
"growing well past the content height restores no-overflow");
|
"growing well past the content height restores no-overflow");
|
||||||
|
// S2 correction: HideWhenDisabled TRUE means a fitting list HIDES its
|
||||||
|
// scrollbar entirely — it does NOT leave a full-track "disabled"
|
||||||
|
// thumb visible.
|
||||||
|
Assert.False(
|
||||||
|
scrollbar.IsPresentationVisible,
|
||||||
|
"content that fits after growing large must HIDE the scrollbar (0x79 HideWhenDisabled), not show a disabled thumb");
|
||||||
|
|
||||||
// Growing back to the ORIGINAL authored size restores the original
|
// Growing back to the ORIGINAL authored size restores the original
|
||||||
// list height and the no-overflow state.
|
// list height and the no-overflow state.
|
||||||
|
|
|
||||||
|
|
@ -576,11 +576,15 @@ public sealed class CharacterTitlesControllerTests
|
||||||
"the fixture's authored default height fits all 15 rows without scrolling");
|
"the fixture's authored default height fits all 15 rows without scrolling");
|
||||||
|
|
||||||
// Shrink the window well below the list's own authored 455px height.
|
// Shrink the window well below the list's own authored 455px height.
|
||||||
// (The Titles page container 0x10000539 carries its own authored
|
// (CT6 fix round, S3 correction: BOTH the Titles page container
|
||||||
// LayoutPolicy that already stretches with the mounted content's
|
// 0x10000539 AND its own ListBox 0x10000532 carry a real authored
|
||||||
// height — verified live during this test's development — so only
|
// LayoutPolicy — HasOriginalParentSize=true on both, pinned by
|
||||||
// the ListBox's OWN Anchors, set above by CharacterTitlesController,
|
// CharacterPanelLiveDatTests.TitlesListAndPage_AuthorHasOriginalParentSize
|
||||||
// were the missing piece.)
|
// — that already stretches with the mounted content's height. The
|
||||||
|
// reflow this test proves comes entirely from that authored
|
||||||
|
// LayoutPolicy; CharacterTitlesController.Bind sets no Anchors of
|
||||||
|
// its own on the ListBox, since the "if LayoutPolicy is null"
|
||||||
|
// fallback it used to carry was dead code and has been deleted.)
|
||||||
handle.ResizeTo(handle.Width, 200f);
|
handle.ResizeTo(handle.Width, 200f);
|
||||||
ApplyAnchors(handle.OuterFrame);
|
ApplyAnchors(handle.OuterFrame);
|
||||||
h.ListBox.ViewportForTest!.LayoutScrollableChildren();
|
h.ListBox.ViewportForTest!.LayoutScrollableChildren();
|
||||||
|
|
|
||||||
|
|
@ -146,17 +146,25 @@ public sealed class RetailWindowFrameTests
|
||||||
/// enforced identically by <c>RetailWindowManager.ResizeTo</c> (this
|
/// enforced identically by <c>RetailWindowManager.ResizeTo</c> (this
|
||||||
/// test), the interactive drag path in <c>UiRoot</c>, and the persisted-
|
/// test), the interactive drag path in <c>UiRoot</c>, and the persisted-
|
||||||
/// geometry restore clamp in <c>RetailWindowLayoutPersistence.Apply</c>.
|
/// geometry restore clamp in <c>RetailWindowLayoutPersistence.Apply</c>.
|
||||||
/// Chat (<c>0x2100006F</c>) was the FIRST window to author real DAT
|
///
|
||||||
/// min/max (min 300/100, max 2000/2000 per
|
/// <para>CT6 fix round (2026-08-25, N4 rename): this test's ORIGINAL name
|
||||||
/// <c>CharacterPanelLiveDatTests.ChatWindowRoot_AuthorsExplicitSizeConstraints</c>);
|
/// (<c>NineSlice_ChatShapedConstraints_ClampProgrammaticResizeAtAuthoredBounds</c>)
|
||||||
/// this test proves an interactive-shaped <c>ResizeTo</c> call against a
|
/// overclaimed. It mounts a content-shaped root (490×100, roughly chat's
|
||||||
/// chat-shaped mount still clamps at those authored bounds after CT6
|
/// own default extent) with a synthetic height-only constraint source
|
||||||
/// wired a SECOND window (Character) onto the same mechanism — a
|
/// (<see cref="Constraints"/> sets ONLY 0x3E/0x3C — no width bounds at
|
||||||
/// regression here would mean CT6 accidentally special-cased Character
|
/// all) — it exercises the NineSlice wrapper's chrome-inset ARITHMETIC on
|
||||||
/// instead of reusing the standard path.
|
/// a content-shaped source, not chat's actual authored DAT contract. Chat
|
||||||
|
/// itself mounts with <c>Chrome = RetailWindowChrome.Imported</c> (its
|
||||||
|
/// own root already IS the complete window, chrome included — see
|
||||||
|
/// <c>RetailUiRuntime.MountChat</c>), so the real chat contract needs
|
||||||
|
/// ZERO chrome inset, the opposite of what this test's NineSlice
|
||||||
|
/// wrapping exercises. See
|
||||||
|
/// <see cref="Imported_ChatContract_ClampsAtAuthoredBoundsWithNoChromeInset"/>
|
||||||
|
/// below for the genuine chat-shaped pin (Chrome=Imported, chat's real
|
||||||
|
/// 300/100/2000/2000 four-sided constraint).</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NineSlice_ChatShapedConstraints_ClampProgrammaticResizeAtAuthoredBounds()
|
public void NineSlice_ContentShapedConstraints_InsetArithmeticClampsProgrammaticResize()
|
||||||
{
|
{
|
||||||
var root = new UiRoot { Width = 1920, Height = 1080 };
|
var root = new UiRoot { Width = 1920, Height = 1080 };
|
||||||
var content = new UiPanel { Width = 490, Height = 100 };
|
var content = new UiPanel { Width = 490, Height = 100 };
|
||||||
|
|
@ -168,7 +176,7 @@ public sealed class RetailWindowFrameTests
|
||||||
NoTex,
|
NoTex,
|
||||||
new RetailWindowFrame.Options
|
new RetailWindowFrame.Options
|
||||||
{
|
{
|
||||||
WindowName = "chat",
|
WindowName = "chat-shaped",
|
||||||
Chrome = RetailWindowChrome.NineSlice,
|
Chrome = RetailWindowChrome.NineSlice,
|
||||||
Left = 10,
|
Left = 10,
|
||||||
Top = 20,
|
Top = 20,
|
||||||
|
|
@ -191,6 +199,59 @@ public sealed class RetailWindowFrameTests
|
||||||
Assert.Equal(500f, handle.Height);
|
Assert.Equal(500f, handle.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CT6 fix round (2026-08-25, N4): the true chat-contract pin the
|
||||||
|
/// renamed test above no longer claims to be. Chat's real LayoutDesc
|
||||||
|
/// (<c>0x2100006F</c>, root <c>0x10000600</c>) mounts with
|
||||||
|
/// <c>Chrome = RetailWindowChrome.Imported</c> — its own root already IS
|
||||||
|
/// the complete retail window, own dragbar/border/resize-grip children
|
||||||
|
/// included (<c>RetailUiRuntime.MountChat</c>) — and authors real
|
||||||
|
/// four-sided constraints, MinWidth=300, MinHeight=100, MaxWidth=2000,
|
||||||
|
/// MaxHeight=2000
|
||||||
|
/// (<c>CharacterPanelLiveDatTests.ChatWindowRoot_AuthorsExplicitSizeConstraints</c>).
|
||||||
|
/// Because Imported chrome applies NO wrapper (<c>inset = 0</c>), the
|
||||||
|
/// mounted clamp must equal those four DAT values EXACTLY — no chrome
|
||||||
|
/// inset composed on top, the same "authored value governs verbatim"
|
||||||
|
/// contract CT6's BLOCKER B1 fix restored for the Character window's
|
||||||
|
/// chrome-inclusive host source.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Imported_ChatContract_ClampsAtAuthoredBoundsWithNoChromeInset()
|
||||||
|
{
|
||||||
|
var root = new UiRoot { Width = 1920, Height = 1080 };
|
||||||
|
var content = new UiPanel { Width = 410, Height = 100 };
|
||||||
|
var constraints = ChatConstraints();
|
||||||
|
|
||||||
|
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||||
|
root,
|
||||||
|
content,
|
||||||
|
NoTex,
|
||||||
|
new RetailWindowFrame.Options
|
||||||
|
{
|
||||||
|
WindowName = "chat",
|
||||||
|
Chrome = RetailWindowChrome.Imported,
|
||||||
|
Left = 10,
|
||||||
|
Top = 440,
|
||||||
|
ResizeX = true,
|
||||||
|
ResizeY = true,
|
||||||
|
DatConstraintSource = constraints,
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(300f, handle.OuterFrame.MinWidth);
|
||||||
|
Assert.Equal(100f, handle.OuterFrame.MinHeight);
|
||||||
|
Assert.Equal(2000f, handle.OuterFrame.MaxWidth);
|
||||||
|
Assert.Equal(2000f, handle.OuterFrame.MaxHeight);
|
||||||
|
|
||||||
|
// No chrome inset: the clamp must hold at EXACTLY the authored
|
||||||
|
// 100/2000 bounds, not 110/2010 the way a NineSlice-wrapped source
|
||||||
|
// would.
|
||||||
|
Assert.True(handle.ResizeTo(handle.Width, 5f));
|
||||||
|
Assert.Equal(100f, handle.Height);
|
||||||
|
|
||||||
|
Assert.True(handle.ResizeTo(handle.Width, 50000f));
|
||||||
|
Assert.Equal(2000f, handle.Height);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NineSlice_CanSupplyBorderWithoutDuplicatingAuthoredCenter()
|
public void NineSlice_CanSupplyBorderWithoutDuplicatingAuthoredCenter()
|
||||||
{
|
{
|
||||||
|
|
@ -312,4 +373,37 @@ public sealed class RetailWindowFrameTests
|
||||||
info.States[UiStateInfo.DirectStateId] = direct;
|
info.States[UiStateInfo.DirectStateId] = direct;
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Synthetic four-sided constraint matching chat's real authored DAT
|
||||||
|
/// values (<c>CharacterPanelLiveDatTests.ChatWindowRoot_AuthorsExplicitSizeConstraints</c>):
|
||||||
|
/// MinWidth=300, MinHeight=100, MaxWidth=2000, MaxHeight=2000.
|
||||||
|
/// </summary>
|
||||||
|
private static ElementInfo ChatConstraints()
|
||||||
|
{
|
||||||
|
var info = new ElementInfo();
|
||||||
|
var direct = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||||
|
direct.Properties.Values[0x3Fu] = new UiPropertyValue
|
||||||
|
{
|
||||||
|
Kind = UiPropertyKind.Integer,
|
||||||
|
IntegerValue = 300,
|
||||||
|
};
|
||||||
|
direct.Properties.Values[0x3Eu] = new UiPropertyValue
|
||||||
|
{
|
||||||
|
Kind = UiPropertyKind.Integer,
|
||||||
|
IntegerValue = 100,
|
||||||
|
};
|
||||||
|
direct.Properties.Values[0x3Du] = new UiPropertyValue
|
||||||
|
{
|
||||||
|
Kind = UiPropertyKind.Integer,
|
||||||
|
IntegerValue = 2000,
|
||||||
|
};
|
||||||
|
direct.Properties.Values[0x3Cu] = new UiPropertyValue
|
||||||
|
{
|
||||||
|
Kind = UiPropertyKind.Integer,
|
||||||
|
IntegerValue = 2000,
|
||||||
|
};
|
||||||
|
info.States[UiStateInfo.DirectStateId] = direct;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue