feat(ui): unify retained window mounts

This commit is contained in:
Erik 2026-07-10 22:22:25 +02:00
parent 6e9e10367f
commit a8e9503d2e
20 changed files with 823 additions and 366 deletions

View file

@ -2099,22 +2099,20 @@ public sealed class GameWindow : IDisposable
healthText: () => (_vitalsVm!.HealthCurrent, _vitalsVm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "",
staminaText: () => (_vitalsVm!.StaminaCurrent, _vitalsVm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "",
manaText: () => (_vitalsVm!.ManaCurrent, _vitalsVm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : "");
// Top-level retail window: user-positioned (Anchors.None so the per-frame
// anchor pass doesn't reset it), movable, and horizontally resizable like
// retail. On a width change the dat edge-anchors reflow the pieces
// (UIElement::UpdateForParentSizeChange @0x00462640): top/bottom edges +
// the three bars stretch, corners stay 5px, the right edge/corners track
// the right side. Vertical resize is off (the layout has no vertical stretch).
var vitalsRoot = imported.Root;
vitalsRoot.Left = 10; vitalsRoot.Top = 30;
vitalsRoot.ClickThrough = false;
vitalsRoot.Anchors = AcDream.App.UI.AnchorEdges.None;
vitalsRoot.Draggable = true;
vitalsRoot.Resizable = true;
vitalsRoot.ResizeX = true;
vitalsRoot.ResizeY = false;
vitalsRoot.MinWidth = 40f;
_uiHost.Root.AddChild(vitalsRoot);
AcDream.App.UI.Layout.RetailWindowFrame.Mount(
_uiHost.Root, vitalsRoot, ResolveChrome,
new AcDream.App.UI.Layout.RetailWindowFrame.Options
{
WindowName = AcDream.App.UI.WindowNames.Vitals,
Chrome = AcDream.App.UI.Layout.RetailWindowChrome.Imported,
Left = 10f,
Top = 30f,
ResizeX = true,
ResizeY = false,
MinWidth = 40f,
ContentClickThrough = false,
});
Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C).");
}
else
@ -2150,16 +2148,20 @@ public sealed class GameWindow : IDisposable
setUiLocked: SetRetailUiLocked,
datFont: vitalsDatFont);
radarRoot.Left = System.Math.Max(0f, _window!.Size.X - radarRoot.Width - 10f);
radarRoot.Top = 10f;
radarRoot.ClickThrough = false;
// User-positioned top-level window: Anchors.None prevents the
// per-frame anchor pass from undoing drag-handle movement.
radarRoot.Anchors = AcDream.App.UI.AnchorEdges.None;
radarRoot.Resizable = false;
radarRoot.ConstrainDragToParent = true;
_uiHost.Root.AddChild(radarRoot);
_uiHost.RegisterWindow(AcDream.App.UI.WindowNames.Radar, radarRoot);
AcDream.App.UI.Layout.RetailWindowFrame.Mount(
_uiHost.Root, radarRoot, ResolveChrome,
new AcDream.App.UI.Layout.RetailWindowFrame.Options
{
WindowName = AcDream.App.UI.WindowNames.Radar,
Chrome = AcDream.App.UI.Layout.RetailWindowChrome.Imported,
Left = System.Math.Max(0f, _window!.Size.X - radarRoot.Width - 10f),
Top = 10f,
Resizable = false,
ResizeX = false,
ResizeY = false,
ConstrainDragToParent = true,
ContentClickThrough = false,
});
ApplySavedRadarPosition();
Console.WriteLine("[D.6] retail radar/compass from LayoutDesc 0x21000074.");
}
@ -2204,32 +2206,30 @@ public sealed class GameWindow : IDisposable
// HEIGHT so the content's child anchors (input-bar-at-bottom, transcript-
// fills) capture correct margins on first layout; resizing the frame reflows
// them correctly from there.
const int chatBorder = AcDream.App.UI.RetailChromeSprites.Border;
var chatRoot = chatController.Root;
float contentW = 490f, contentH = chatRoot.Height; // dat-authored height
var chatFrame = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
{
Left = 10, Top = 440,
Width = contentW + 2 * chatBorder, Height = contentH + 2 * chatBorder,
MinWidth = 200f, MinHeight = 90f,
// Retail chat is translucent — fade the window's backgrounds/chrome
// (text stays opaque). Configurable opacity is a later step; 0.75 reads
// as see-through-but-readable. (retail SetDefaultOpacity ~0.5 / active 1.0)
Opacity = 0.75f,
};
chatRoot.Left = chatBorder; chatRoot.Top = chatBorder;
chatRoot.Width = contentW; chatRoot.Height = contentH;
chatRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Right | AcDream.App.UI.AnchorEdges.Bottom;
var chatHandle = AcDream.App.UI.Layout.RetailWindowFrame.Mount(
_uiHost.Root, chatRoot, ResolveChrome,
new AcDream.App.UI.Layout.RetailWindowFrame.Options
{
WindowName = AcDream.App.UI.WindowNames.Chat,
Chrome = AcDream.App.UI.Layout.RetailWindowChrome.NineSlice,
Left = 10f,
Top = 440f,
ContentWidth = 490f,
ContentHeight = chatRoot.Height,
RebaseContentLayout = true,
DatConstraintSource = chatController.DatWindowInfo,
MinWidth = 200f,
ResizeX = true,
ResizeY = true,
Opacity = 0.75f,
});
chatController.AttachWindow(chatHandle);
// The LayoutDesc root is 800px wide because it shares the display
// coordinate space with auxiliary/floaty chat elements. The docked
// gmMainChatUI content is the left 490px. Rebase its immediate children
// after that intentional crop so subsequent frame resizing grows the
// transcript/input across the whole mounted interior.
chatRoot.RebaseChildLayoutBaselines();
chatRoot.Draggable = false; chatRoot.Resizable = false;
chatFrame.AddChild(chatRoot);
_uiHost.Root.AddChild(chatFrame);
// Tab / Enter enters "write mode" by focusing this input (retail's chat
// activation); a focused input suppresses character movement (see the
// WantsKeyboard gate in the movement poll).
@ -2365,34 +2365,26 @@ public sealed class GameWindow : IDisposable
// thickness on every side, giving an outer window of 310×132.
const int toolbarBorder = AcDream.App.UI.RetailChromeSprites.Border;
float toolbarContentW = 300f, toolbarContentH = toolbarRoot.Height;
var toolbarFrame = new AcDream.App.UI.UiCollapsibleFrame(ResolveChrome)
{
Left = 10, Top = 300,
Width = toolbarContentW + 2 * toolbarBorder,
Height = toolbarContentH + 2 * toolbarBorder,
// The toolbar is fully opaque (not translucent like the chat window).
Opacity = 1.0f,
};
// Content is offset by the border so it sits inside the chrome.
toolbarRoot.Left = toolbarBorder;
toolbarRoot.Top = toolbarBorder;
toolbarRoot.Width = toolbarContentW;
toolbarRoot.Height = toolbarContentH;
// Anchor content to Left|Top|Right only — drop Bottom so the dat content
// keeps its full height when the frame collapses; row 2 hides via Visible,
// not reflow. (Width is fixed so the horizontal anchors are inert but harmless.)
toolbarRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Right;
// The frame is the draggable window; the content itself is not. ClickThrough so the
// content panel never CLAIMS a hit — its behavioral children (slots, indicators) are
// hit children-first, and its empty areas fall through to the frame (move). Critically,
// when collapsed the row-2 band is hidden, so below the collapsed frame the content has
// no visible/hit children and ClickThrough lets clicks fall through (no phantom
// window-drag from where row 2 used to be). Same pattern as the chat content panel.
toolbarRoot.ClickThrough = true;
toolbarRoot.Draggable = false;
toolbarRoot.Resizable = false;
toolbarFrame.AddChild(toolbarRoot);
var toolbarHandle = AcDream.App.UI.Layout.RetailWindowFrame.Mount(
_uiHost.Root, toolbarRoot, ResolveChrome,
new AcDream.App.UI.Layout.RetailWindowFrame.Options
{
WindowName = AcDream.App.UI.WindowNames.Toolbar,
Chrome = AcDream.App.UI.Layout.RetailWindowChrome.CollapsibleNineSlice,
Left = 10f,
Top = 300f,
ContentWidth = toolbarContentW,
ContentHeight = toolbarContentH,
Resizable = false,
ResizeX = false,
ResizeY = true,
ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom,
ContentAnchors = AcDream.App.UI.AnchorEdges.Left
| AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Right,
ContentClickThrough = true,
});
var toolbarFrame = (AcDream.App.UI.UiCollapsibleFrame)toolbarHandle.OuterFrame;
// Collapse-to-one-row: the frame's bottom edge snaps between a one-row (row 2 hidden)
// and two-row height. CollapsedHeight is computed from the layout (just above row 2),
@ -2431,8 +2423,6 @@ public sealed class GameWindow : IDisposable
Console.WriteLine($"[D.2b] toolbar collapse: collapsed={collapsedH:0} expanded={expandedH:0} (row2Top={minRow2Top:0}).");
}
_uiHost.Root.AddChild(toolbarFrame);
Console.WriteLine("[D.5.1] retail toolbar window from LayoutDesc importer (0x21000016).");
}
else Console.WriteLine("[D.5.1] toolbar: LayoutDesc 0x21000016 not found.");
@ -2462,16 +2452,18 @@ public sealed class GameWindow : IDisposable
_uiHost.Root, characterLayout.Root, ResolveChrome,
new AcDream.App.UI.Layout.RetailWindowFrame.Options
{
WindowName = AcDream.App.UI.WindowNames.Character,
Chrome = AcDream.App.UI.Layout.RetailWindowChrome.NineSlice,
Left = 540f,
Top = 18f,
MaxHeight = 760f,
ResizeX = false,
ResizeY = true,
ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom,
Visible = false,
ContentAnchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Bottom,
ContentClickThrough = false,
WindowName = AcDream.App.UI.WindowNames.Character,
});
Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E).");
@ -2515,36 +2507,27 @@ public sealed class GameWindow : IDisposable
// SAME UiNineSlicePanel the vitals/chat/toolbar windows use. The gmInventoryUI
// LayoutDesc (0x21000023) is 300×362; the frame adds one border thickness on
// every side. The frame is the draggable window; the dat content sits inside it.
const int invBorder = AcDream.App.UI.RetailChromeSprites.Border;
float invContentW = inventoryRoot.Width, invContentH = inventoryRoot.Height;
var inventoryFrame = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
{
Left = inventoryRoot.Left, Top = inventoryRoot.Top, // keep the dat window position
Width = invContentW + 2 * invBorder,
Height = invContentH + 2 * invBorder,
Opacity = 1.0f,
Visible = false, // F12 toggles it
Anchors = AcDream.App.UI.AnchorEdges.None, // user-positioned
Draggable = true,
};
// Content offset by the border so it sits inside the chrome.
inventoryRoot.Left = invBorder; inventoryRoot.Top = invBorder;
inventoryRoot.Width = invContentW; inventoryRoot.Height = invContentH;
// Content fills the frame vertically (Left|Top|Bottom = fixed width + height tracks
// the frame) so a vertical resize grows the inner content.
inventoryRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Bottom;
inventoryRoot.Draggable = false;
inventoryFrame.AddChild(inventoryRoot);
// Vertical-only resize: drag the BOTTOM edge to expand and see more of the pack.
// ResizeX=false + ResizableEdges=Bottom blocks horizontal resize (user request).
// MinHeight = the dat default (no shrink below it); MaxHeight = toward full screen.
inventoryFrame.Resizable = true;
inventoryFrame.ResizeX = false;
inventoryFrame.ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom;
inventoryFrame.MinHeight = invContentH + 2 * invBorder;
inventoryFrame.MaxHeight = 560f;
var inventoryHandle = AcDream.App.UI.Layout.RetailWindowFrame.Mount(
_uiHost.Root, inventoryRoot, ResolveChrome,
new AcDream.App.UI.Layout.RetailWindowFrame.Options
{
WindowName = AcDream.App.UI.WindowNames.Inventory,
Chrome = AcDream.App.UI.Layout.RetailWindowChrome.NineSlice,
Left = inventoryRoot.Left,
Top = inventoryRoot.Top,
ContentWidth = invContentW,
ContentHeight = invContentH,
Visible = false,
ResizeX = false,
ResizeY = true,
ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom,
MaxHeight = 560f,
ContentAnchors = AcDream.App.UI.AnchorEdges.Left
| AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Bottom,
});
var inventoryFrame = (AcDream.App.UI.UiNineSlicePanel)inventoryHandle.OuterFrame;
// Stretch the contents region with the window: the gm3DItemsUI sub-window + its grid
// + scrollbar + the full-window backdrop grow vertically (Left|Top|Bottom); the
@ -2568,9 +2551,6 @@ public sealed class GameWindow : IDisposable
PinTopLeft(0x100001CDu); // paperdoll (fixed at top)
PinTopLeft(0x100001CEu); // side-bag column (fixed at top)
_uiHost.Root.AddChild(inventoryFrame);
_uiHost.RegisterWindow(AcDream.App.UI.WindowNames.Inventory, inventoryFrame);
// Phase D.2b-B — populate the inventory from ClientObjectTable: the
// "Contents of Backpack" grid + the pack-selector strip + the burden meter +
// captions. Analogue of gmInventoryUI/gmBackpackUI/gm3DItemsUI::PostInit.