using System; namespace AcDream.App.UI.Layout; /// How a production retained window obtains its outer chrome. public enum RetailWindowChrome { /// The imported root already is the complete retail outer frame. Imported, /// Wrap imported content in the shared retail nine-slice frame. NineSlice, /// Use the shared frame variant with the toolbar's two height stops. CollapsibleNineSlice, } /// /// 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. /// public static class RetailWindowFrame { public sealed record Options { public required string WindowName { get; init; } public RetailWindowChrome Chrome { get; init; } = RetailWindowChrome.NineSlice; /// Initial outer-frame position in screen pixels. public float Left { get; init; } public float Top { get; init; } /// /// Optional mounted content extent. Used when a LayoutDesc root shares a /// larger auxiliary coordinate space (the main chat root is cropped to 490px). /// public float? ContentWidth { get; init; } public float? ContentHeight { get; init; } public bool RebaseContentLayout { get; init; } /// /// 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. /// 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 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; } }