Map synthetic move and resize affordances to the exact DAT cursors, make chat top chrome movable, and replace stale primary-panel height caps with a dynamic screen-edge constraint. This keeps the retained wrapper adaptation aligned with retail Dragbar/Resizebar behavior.
184 lines
7.5 KiB
C#
184 lines
7.5 KiB
C#
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>
|
|
/// 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
|
|
{
|
|
public sealed record Options
|
|
{
|
|
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>
|
|
/// 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; }
|
|
public float? MaxWidth { get; init; }
|
|
public float? MaxHeight { get; init; }
|
|
|
|
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; }
|
|
public bool ConstrainResizeToParent { get; init; }
|
|
|
|
public float Opacity { get; init; } = 1f;
|
|
public bool Visible { get; init; } = true;
|
|
|
|
public AnchorEdges ContentAnchors { get; init; } =
|
|
AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom;
|
|
public bool? ContentClickThrough { get; init; }
|
|
public IRetainedPanelController? Controller { get; init; }
|
|
public IRetainedWindowStateController? StateController { get; init; }
|
|
}
|
|
|
|
public static RetailWindowHandle Mount(
|
|
UiRoot root,
|
|
UiElement content,
|
|
Func<uint, (uint handle, int w, int h)> resolveChrome,
|
|
Options options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(root);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
ArgumentNullException.ThrowIfNull(resolveChrome);
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(options.WindowName);
|
|
|
|
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;
|
|
|
|
UiElement outerFrame;
|
|
if (wrapped)
|
|
{
|
|
outerFrame = options.Chrome switch
|
|
{
|
|
RetailWindowChrome.NineSlice => new UiNineSlicePanel(resolveChrome),
|
|
RetailWindowChrome.CollapsibleNineSlice => new UiCollapsibleFrame(resolveChrome),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(options.Chrome)),
|
|
};
|
|
|
|
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();
|
|
}
|
|
|
|
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.ConstrainResizeToParent = options.ConstrainResizeToParent;
|
|
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,
|
|
options.StateController);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|