feat(ui): unify retained window mounts
This commit is contained in:
parent
6e9e10367f
commit
a8e9503d2e
20 changed files with 823 additions and 366 deletions
|
|
@ -2,65 +2,73 @@ using System;
|
|||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>How a production retained window obtains its outer chrome.</summary>
|
||||
public enum RetailWindowChrome
|
||||
{
|
||||
/// <summary>The imported root already is the complete retail outer frame.</summary>
|
||||
Imported,
|
||||
|
||||
/// <summary>Wrap imported content in the shared retail nine-slice frame.</summary>
|
||||
NineSlice,
|
||||
|
||||
/// <summary>Use the shared frame variant with the toolbar's two height stops.</summary>
|
||||
CollapsibleNineSlice,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mounts an imported retail window's content into the shared 8-piece beveled
|
||||
/// chrome (<see cref="UiNineSlicePanel"/>) and optionally registers it with
|
||||
/// the <see cref="UiRoot"/> window manager. This is THE mount recipe every
|
||||
/// retail window follows: frame sized content + 2×border, content inset by
|
||||
/// the border with Draggable/Resizable off (the frame owns move/resize).
|
||||
///
|
||||
/// <para>The vitals/chat/toolbar/inventory windows still inline this recipe
|
||||
/// in <c>GameWindow</c> (pre-extraction); new windows must mount through this
|
||||
/// helper instead of copying the block again — see
|
||||
/// docs/research/2026-07-02-ui-architecture-review.md (theme T1).</para>
|
||||
/// The single mount path for production retained windows. It owns the distinction
|
||||
/// between DAT roots that already contain chrome and content roots that require the
|
||||
/// shared nine-slice wrapper, applies exact resize/opacity/visibility policy, mounts
|
||||
/// the outer frame, and returns its registered typed handle.
|
||||
/// </summary>
|
||||
public static class RetailWindowFrame
|
||||
{
|
||||
/// <summary>Per-window placement + behavior knobs. Null-valued knobs keep
|
||||
/// the widget defaults (<see cref="UiElement"/> / <see cref="UiNineSlicePanel"/>).</summary>
|
||||
public sealed record Options
|
||||
{
|
||||
/// <summary>Initial window position (screen px).</summary>
|
||||
public required string WindowName { get; init; }
|
||||
public RetailWindowChrome Chrome { get; init; } = RetailWindowChrome.NineSlice;
|
||||
|
||||
/// <summary>Initial outer-frame position in screen pixels.</summary>
|
||||
public float Left { get; init; }
|
||||
public float Top { get; init; }
|
||||
|
||||
/// <summary>Minimum frame size; null = lock to content + 2×border (non-shrinkable).</summary>
|
||||
/// <summary>
|
||||
/// Optional mounted content extent. Used when a LayoutDesc root shares a
|
||||
/// larger auxiliary coordinate space (the main chat root is cropped to 490px).
|
||||
/// </summary>
|
||||
public float? ContentWidth { get; init; }
|
||||
public float? ContentHeight { get; init; }
|
||||
public bool RebaseContentLayout { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional DAT element whose effective 0x3C..0x3F properties provide
|
||||
/// max-height, max-width, min-height, and min-width respectively. Explicit
|
||||
/// option values win; wrapper chrome is added to DAT content constraints.
|
||||
/// </summary>
|
||||
public ElementInfo? DatConstraintSource { get; init; }
|
||||
public float? MinWidth { get; init; }
|
||||
public float? MinHeight { get; init; }
|
||||
|
||||
/// <summary>Maximum frame height while resizing; null = unbounded.</summary>
|
||||
public float? MaxWidth { get; init; }
|
||||
public float? MaxHeight { get; init; }
|
||||
|
||||
/// <summary>Allow horizontal resize (frame default: true).</summary>
|
||||
public bool Draggable { get; init; } = true;
|
||||
public bool Resizable { get; init; } = true;
|
||||
public bool ResizeX { get; init; } = true;
|
||||
public bool ResizeY { get; init; } = true;
|
||||
public ResizeEdges ResizableEdges { get; init; } =
|
||||
ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Top | ResizeEdges.Bottom;
|
||||
public bool ConstrainDragToParent { get; init; }
|
||||
|
||||
/// <summary>Restrict which edges start a resize; null = all edges.</summary>
|
||||
public ResizeEdges? ResizableEdges { get; init; }
|
||||
|
||||
/// <summary>Window translucency (retail SetDefaultOpacity analog).</summary>
|
||||
public float Opacity { get; init; } = 1f;
|
||||
|
||||
/// <summary>Start shown or hidden (hidden windows toggle via the window manager).</summary>
|
||||
public bool Visible { get; init; } = true;
|
||||
|
||||
/// <summary>How the content tracks the frame when it resizes.</summary>
|
||||
public AnchorEdges ContentAnchors { get; init; } =
|
||||
AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom;
|
||||
|
||||
/// <summary>Force the content's ClickThrough; null = leave as imported.</summary>
|
||||
public bool? ContentClickThrough { get; init; }
|
||||
|
||||
/// <summary>Register the frame under this name in the UiRoot window
|
||||
/// manager (<see cref="WindowNames"/>); null = unmanaged window.</summary>
|
||||
public string? WindowName { get; init; }
|
||||
public IRetainedPanelController? Controller { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap <paramref name="content"/> (sized by the importer) in the beveled
|
||||
/// chrome, add it to <paramref name="root"/>, and register it when
|
||||
/// <see cref="Options.WindowName"/> is set. Returns the frame.
|
||||
/// </summary>
|
||||
public static UiNineSlicePanel Mount(
|
||||
public static RetailWindowHandle Mount(
|
||||
UiRoot root,
|
||||
UiElement content,
|
||||
Func<uint, (uint handle, int w, int h)> resolveChrome,
|
||||
|
|
@ -70,39 +78,103 @@ public static class RetailWindowFrame
|
|||
ArgumentNullException.ThrowIfNull(content);
|
||||
ArgumentNullException.ThrowIfNull(resolveChrome);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.WindowName);
|
||||
|
||||
const int border = RetailChromeSprites.Border;
|
||||
float contentW = content.Width;
|
||||
float contentH = content.Height;
|
||||
float contentWidth = options.ContentWidth ?? content.Width;
|
||||
float contentHeight = options.ContentHeight ?? content.Height;
|
||||
bool wrapped = options.Chrome != RetailWindowChrome.Imported;
|
||||
int inset = wrapped ? 2 * RetailChromeSprites.Border : 0;
|
||||
float outerWidth = contentWidth + inset;
|
||||
float outerHeight = contentHeight + inset;
|
||||
|
||||
var frame = new UiNineSlicePanel(resolveChrome)
|
||||
UiElement outerFrame;
|
||||
if (wrapped)
|
||||
{
|
||||
Left = options.Left,
|
||||
Top = options.Top,
|
||||
Width = contentW + 2 * border,
|
||||
Height = contentH + 2 * border,
|
||||
MinWidth = options.MinWidth ?? contentW + 2 * border,
|
||||
MinHeight = options.MinHeight ?? contentH + 2 * border,
|
||||
ResizeX = options.ResizeX,
|
||||
Opacity = options.Opacity,
|
||||
Visible = options.Visible,
|
||||
};
|
||||
if (options.MaxHeight is { } maxHeight) frame.MaxHeight = maxHeight;
|
||||
if (options.ResizableEdges is { } edges) frame.ResizableEdges = edges;
|
||||
outerFrame = options.Chrome switch
|
||||
{
|
||||
RetailWindowChrome.NineSlice => new UiNineSlicePanel(resolveChrome),
|
||||
RetailWindowChrome.CollapsibleNineSlice => new UiCollapsibleFrame(resolveChrome),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(options.Chrome)),
|
||||
};
|
||||
|
||||
content.Left = border;
|
||||
content.Top = border;
|
||||
content.Width = contentW;
|
||||
content.Height = contentH;
|
||||
content.Anchors = options.ContentAnchors;
|
||||
if (options.ContentClickThrough is { } clickThrough) content.ClickThrough = clickThrough;
|
||||
content.Draggable = false; // the frame owns window move/resize
|
||||
content.Resizable = false;
|
||||
const int border = RetailChromeSprites.Border;
|
||||
content.Left = border;
|
||||
content.Top = border;
|
||||
content.Width = contentWidth;
|
||||
content.Height = contentHeight;
|
||||
content.Anchors = options.ContentAnchors;
|
||||
content.Draggable = false;
|
||||
content.Resizable = false;
|
||||
if (options.ContentClickThrough is { } clickThrough)
|
||||
content.ClickThrough = clickThrough;
|
||||
if (options.RebaseContentLayout)
|
||||
content.RebaseChildLayoutBaselines();
|
||||
outerFrame.AddChild(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
outerFrame = content;
|
||||
content.Width = contentWidth;
|
||||
content.Height = contentHeight;
|
||||
content.Anchors = AnchorEdges.None;
|
||||
if (options.ContentClickThrough is { } clickThrough)
|
||||
content.ClickThrough = clickThrough;
|
||||
if (options.RebaseContentLayout)
|
||||
content.RebaseChildLayoutBaselines();
|
||||
}
|
||||
|
||||
frame.AddChild(content);
|
||||
root.AddChild(frame);
|
||||
if (options.WindowName is { } name)
|
||||
root.RegisterWindow(name, frame);
|
||||
return frame;
|
||||
outerFrame.Left = options.Left;
|
||||
outerFrame.Top = options.Top;
|
||||
outerFrame.Width = outerWidth;
|
||||
outerFrame.Height = outerHeight;
|
||||
outerFrame.Anchors = AnchorEdges.None;
|
||||
outerFrame.Draggable = options.Draggable;
|
||||
outerFrame.Resizable = options.Resizable;
|
||||
outerFrame.ResizeX = options.ResizeX;
|
||||
outerFrame.ResizeY = options.ResizeY;
|
||||
outerFrame.ResizableEdges = options.ResizableEdges;
|
||||
outerFrame.ConstrainDragToParent = options.ConstrainDragToParent;
|
||||
outerFrame.Opacity = Math.Clamp(options.Opacity, 0f, 1f);
|
||||
outerFrame.Visible = options.Visible;
|
||||
|
||||
outerFrame.MinWidth = ResolveConstraint(
|
||||
options.MinWidth, options.DatConstraintSource, 0x3Fu, outerWidth, inset);
|
||||
outerFrame.MinHeight = ResolveConstraint(
|
||||
options.MinHeight, options.DatConstraintSource, 0x3Eu, outerHeight, inset);
|
||||
outerFrame.MaxWidth = Math.Max(
|
||||
outerFrame.MinWidth,
|
||||
ResolveConstraint(options.MaxWidth, options.DatConstraintSource, 0x3Du, float.MaxValue, inset));
|
||||
outerFrame.MaxHeight = Math.Max(
|
||||
outerFrame.MinHeight,
|
||||
ResolveConstraint(options.MaxHeight, options.DatConstraintSource, 0x3Cu, float.MaxValue, inset));
|
||||
|
||||
// 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
|
||||
// resize after rendering one frame.
|
||||
if (wrapped)
|
||||
content.ApplyAnchor(outerWidth, outerHeight);
|
||||
|
||||
root.AddChild(outerFrame);
|
||||
return root.RegisterWindow(
|
||||
options.WindowName,
|
||||
outerFrame,
|
||||
content,
|
||||
options.Controller);
|
||||
}
|
||||
|
||||
private static float ResolveConstraint(
|
||||
float? explicitValue,
|
||||
ElementInfo? datSource,
|
||||
uint propertyId,
|
||||
float fallback,
|
||||
int chromeInset)
|
||||
{
|
||||
if (explicitValue is { } value)
|
||||
return value;
|
||||
if (datSource is not null
|
||||
&& datSource.TryGetEffectiveInteger(propertyId, out int datValue)
|
||||
&& datValue >= 0)
|
||||
return datValue + chromeInset;
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue