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>
409 lines
16 KiB
C#
409 lines
16 KiB
C#
using AcDream.App.UI;
|
||
using AcDream.App.UI.Layout;
|
||
|
||
namespace AcDream.App.Tests.UI.Layout;
|
||
|
||
public sealed class RetailWindowFrameTests
|
||
{
|
||
private static (uint, int, int) NoTex(uint _) => (0u, 0, 0);
|
||
|
||
[Theory]
|
||
[InlineData(9, 459f, 288f)]
|
||
[InlineData(13, 587f, 416f)]
|
||
[InlineData(18, 747f, 576f)]
|
||
public void CombatRoot_FitsRequestedFavoriteSlotCapacity(
|
||
int visibleSlots,
|
||
float expectedRootWidth,
|
||
float expectedListWidth)
|
||
{
|
||
ImportedLayout combat = LayoutImporter.Build(
|
||
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||
|
||
float width = RetailCombatLayout.FitFavoriteSlots(combat, visibleSlots);
|
||
|
||
Assert.Equal(expectedRootWidth, width);
|
||
Assert.Equal(expectedListWidth, combat.FindElement(
|
||
SpellcastingUiController.FavoriteListId)!.Width);
|
||
}
|
||
|
||
[Fact]
|
||
public void CombatRoot_EighteenSlotsKeepArrowsAndCastInsideFrame()
|
||
{
|
||
ImportedLayout combat = LayoutImporter.Build(
|
||
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||
|
||
RetailCombatLayout.FitFavoriteSlots(combat);
|
||
|
||
UiElement group = combat.FindElement(0x100000AAu)!;
|
||
UiElement scrollbar = combat.FindElement(SpellcastingUiController.FavoriteScrollbarId)!;
|
||
UiElement cast = combat.FindElement(SpellcastingUiController.CastButtonId)!;
|
||
Assert.Equal(747f, combat.Root.Width);
|
||
Assert.Equal((40f, 622f), (group.Left, group.Width));
|
||
Assert.Equal(622f, scrollbar.Width);
|
||
Assert.Equal((662f, 75f), (cast.Left, cast.Width));
|
||
Assert.Equal(737f, cast.Left + cast.Width);
|
||
}
|
||
|
||
[Fact]
|
||
public void ImportedChrome_MountsRootWithoutASecondFrame()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
var content = new UiPanel { Width = 220, Height = 90 };
|
||
|
||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||
root,
|
||
content,
|
||
NoTex,
|
||
new RetailWindowFrame.Options
|
||
{
|
||
WindowName = "vitals",
|
||
Chrome = RetailWindowChrome.Imported,
|
||
Left = 12,
|
||
Top = 18,
|
||
ResizeX = true,
|
||
ResizeY = false,
|
||
MinWidth = 40,
|
||
ContentClickThrough = false,
|
||
});
|
||
|
||
Assert.Same(content, handle.OuterFrame);
|
||
Assert.Same(content, handle.ContentRoot);
|
||
Assert.Same(root, content.Parent);
|
||
Assert.Equal((12f, 18f, 220f, 90f),
|
||
(content.Left, content.Top, content.Width, content.Height));
|
||
Assert.True(content.Draggable);
|
||
Assert.True(content.Resizable);
|
||
Assert.True(content.ResizeX);
|
||
Assert.False(content.ResizeY);
|
||
Assert.Equal(40f, content.MinWidth);
|
||
}
|
||
|
||
[Fact]
|
||
public void ImportedChrome_CanPreserveFixedHudDesktopAnchors()
|
||
{
|
||
var root = new UiRoot { Width = 1920, Height = 1080 };
|
||
var content = new UiPanel { Width = 1730, Height = 90 };
|
||
|
||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||
root,
|
||
content,
|
||
NoTex,
|
||
new RetailWindowFrame.Options
|
||
{
|
||
WindowName = "combat",
|
||
Chrome = RetailWindowChrome.Imported,
|
||
Left = 0f,
|
||
Top = 980f,
|
||
OuterAnchors = AnchorEdges.Left | AnchorEdges.Bottom,
|
||
});
|
||
|
||
Assert.Equal(
|
||
AnchorEdges.Left | AnchorEdges.Bottom,
|
||
handle.OuterFrame.Anchors);
|
||
}
|
||
|
||
[Fact]
|
||
public void NineSlice_UsesCroppedContentAndDatHeightConstraints()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
var content = new UiPanel { Width = 800, Height = 100 };
|
||
var constraints = Constraints(minHeight: 100, maxHeight: 360);
|
||
|
||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||
root,
|
||
content,
|
||
NoTex,
|
||
new RetailWindowFrame.Options
|
||
{
|
||
WindowName = "chat",
|
||
Chrome = RetailWindowChrome.NineSlice,
|
||
Left = 10,
|
||
Top = 440,
|
||
ContentWidth = 490,
|
||
DatConstraintSource = constraints,
|
||
MinWidth = 200,
|
||
Opacity = 0.75f,
|
||
});
|
||
|
||
var frame = Assert.IsType<UiNineSlicePanel>(handle.OuterFrame);
|
||
Assert.Same(content, handle.ContentRoot);
|
||
Assert.Same(frame, content.Parent);
|
||
Assert.Equal((500f, 110f), (frame.Width, frame.Height));
|
||
Assert.Equal((5f, 5f, 490f, 100f),
|
||
(content.Left, content.Top, content.Width, content.Height));
|
||
Assert.Equal(200f, frame.MinWidth);
|
||
Assert.Equal(110f, frame.MinHeight);
|
||
Assert.Equal(370f, frame.MaxHeight);
|
||
Assert.Equal(0.75f, frame.Opacity);
|
||
}
|
||
|
||
/// <summary>
|
||
/// CT6 shared-mechanism regression pin: the authored-min/max clamp exists
|
||
/// in exactly ONE place — <see cref="UiElement.MinWidth"/>/<see cref="UiElement.MaxWidth"/>/
|
||
/// <see cref="UiElement.MinHeight"/>/<see cref="UiElement.MaxHeight"/>,
|
||
/// set once at <see cref="RetailWindowFrame.Mount"/> from
|
||
/// <see cref="RetailWindowFrame.Options.DatConstraintSource"/> and then
|
||
/// enforced identically by <c>RetailWindowManager.ResizeTo</c> (this
|
||
/// test), the interactive drag path in <c>UiRoot</c>, and the persisted-
|
||
/// geometry restore clamp in <c>RetailWindowLayoutPersistence.Apply</c>.
|
||
///
|
||
/// <para>CT6 fix round (2026-08-25, N4 rename): this test's ORIGINAL name
|
||
/// (<c>NineSlice_ChatShapedConstraints_ClampProgrammaticResizeAtAuthoredBounds</c>)
|
||
/// overclaimed. It mounts a content-shaped root (490×100, roughly chat's
|
||
/// own default extent) with a synthetic height-only constraint source
|
||
/// (<see cref="Constraints"/> sets ONLY 0x3E/0x3C — no width bounds at
|
||
/// all) — it exercises the NineSlice wrapper's chrome-inset ARITHMETIC on
|
||
/// 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>
|
||
[Fact]
|
||
public void NineSlice_ContentShapedConstraints_InsetArithmeticClampsProgrammaticResize()
|
||
{
|
||
var root = new UiRoot { Width = 1920, Height = 1080 };
|
||
var content = new UiPanel { Width = 490, Height = 100 };
|
||
var constraints = Constraints(minHeight: 100, maxHeight: 2000);
|
||
|
||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||
root,
|
||
content,
|
||
NoTex,
|
||
new RetailWindowFrame.Options
|
||
{
|
||
WindowName = "chat-shaped",
|
||
Chrome = RetailWindowChrome.NineSlice,
|
||
Left = 10,
|
||
Top = 20,
|
||
ResizeX = false,
|
||
ResizeY = true,
|
||
DatConstraintSource = constraints,
|
||
});
|
||
|
||
// Request far below the authored minimum (100 + 10px chrome inset =
|
||
// 110) — the clamp must hold, not the requested value.
|
||
Assert.True(handle.ResizeTo(handle.Width, 5f));
|
||
Assert.Equal(110f, handle.Height);
|
||
|
||
// Request far above the authored maximum (2000 + 10 = 2010).
|
||
Assert.True(handle.ResizeTo(handle.Width, 50000f));
|
||
Assert.Equal(2010f, handle.Height);
|
||
|
||
// A request inside the bounds is honored exactly.
|
||
Assert.True(handle.ResizeTo(handle.Width, 500f));
|
||
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]
|
||
public void NineSlice_CanSupplyBorderWithoutDuplicatingAuthoredCenter()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
var content = new UiPanel { Width = 300, Height = 362 };
|
||
|
||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||
root, content, NoTex,
|
||
new RetailWindowFrame.Options
|
||
{
|
||
WindowName = "character-info",
|
||
DrawChromeCenter = false,
|
||
});
|
||
|
||
var frame = Assert.IsType<UiNineSlicePanel>(handle.OuterFrame);
|
||
Assert.False(frame.DrawCenterFill);
|
||
}
|
||
|
||
[Fact]
|
||
public void CollapsibleMount_ReturnsToolbarFrameAndIndependentAxes()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
var content = new UiPanel { Width = 300, Height = 122 };
|
||
|
||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||
root,
|
||
content,
|
||
NoTex,
|
||
new RetailWindowFrame.Options
|
||
{
|
||
WindowName = "toolbar",
|
||
Chrome = RetailWindowChrome.CollapsibleNineSlice,
|
||
ResizeX = false,
|
||
ResizeY = true,
|
||
ResizableEdges = ResizeEdges.Bottom,
|
||
ConstrainResizeToParent = true,
|
||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right,
|
||
ContentClickThrough = true,
|
||
});
|
||
|
||
var frame = Assert.IsType<UiCollapsibleFrame>(handle.OuterFrame);
|
||
Assert.False(frame.ResizeX);
|
||
Assert.True(frame.ResizeY);
|
||
Assert.Equal(ResizeEdges.Bottom, frame.ResizableEdges);
|
||
Assert.True(frame.ConstrainResizeToParent);
|
||
Assert.True(content.ClickThrough);
|
||
Assert.False(content.Draggable);
|
||
Assert.False(content.Resizable);
|
||
}
|
||
|
||
[Fact]
|
||
public void HiddenInventory_RestoredExpandedBeforeFirstDraw_GrowsContentsRows()
|
||
{
|
||
ImportedLayout layout = FixtureLoader.LoadInventory();
|
||
UiItemList grid = Assert.IsType<UiItemList>(
|
||
layout.FindElement(InventoryController.ContentsGridId));
|
||
grid.Columns = 6;
|
||
grid.CellWidth = 32f;
|
||
grid.CellHeight = 32f;
|
||
grid.Flush();
|
||
for (int i = 0; i < 102; i++)
|
||
grid.AddItem(new UiItemSlot());
|
||
|
||
var root = new UiRoot { Width = 1280, Height = 720 };
|
||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||
root,
|
||
layout.Root,
|
||
NoTex,
|
||
new RetailWindowFrame.Options
|
||
{
|
||
WindowName = WindowNames.Inventory,
|
||
Chrome = RetailWindowChrome.NineSlice,
|
||
ContentWidth = layout.Root.Width,
|
||
ContentHeight = layout.Root.Height,
|
||
Visible = false,
|
||
ResizeX = false,
|
||
ResizeY = true,
|
||
ResizableEdges = ResizeEdges.Bottom,
|
||
MaxHeight = 560f,
|
||
});
|
||
|
||
// Production installs the controller-owned policies after mounting, while
|
||
// inventory is still hidden, and captures their design baseline immediately.
|
||
InventoryController.ConfigureResizeLayout(layout);
|
||
|
||
// Persistence restores the hidden window before its first visible draw.
|
||
handle.ResizeTo(handle.Width, 527f);
|
||
ApplyAnchors(handle.OuterFrame);
|
||
grid.LayoutCells();
|
||
|
||
Assert.Equal(251f, grid.Height);
|
||
Assert.Equal(48, grid.Children.Count(child => child.Visible));
|
||
Assert.Equal(224f, grid.GetItem(42)!.Top); // final row intersects 251px view by 27px
|
||
}
|
||
|
||
private static void ApplyAnchors(UiElement parent)
|
||
{
|
||
foreach (UiElement child in parent.Children)
|
||
{
|
||
child.ApplyAnchor(parent.Width, parent.Height);
|
||
ApplyAnchors(child);
|
||
}
|
||
}
|
||
|
||
private static ElementInfo Constraints(int minHeight, int maxHeight)
|
||
{
|
||
var info = new ElementInfo();
|
||
var direct = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||
direct.Properties.Values[0x3Eu] = new UiPropertyValue
|
||
{
|
||
Kind = UiPropertyKind.Integer,
|
||
IntegerValue = minHeight,
|
||
};
|
||
direct.Properties.Values[0x3Cu] = new UiPropertyValue
|
||
{
|
||
Kind = UiPropertyKind.Integer,
|
||
IntegerValue = maxHeight,
|
||
};
|
||
info.States[UiStateInfo.DirectStateId] = direct;
|
||
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;
|
||
}
|
||
}
|