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 /// UNLESS is set. /// public ElementInfo? DatConstraintSource { get; init; } /// /// True when is itself retail's own /// OUTER window frame — chrome already included in its authored /// 0x3C..0x3F values — rather than a content element the wrapper's own /// chrome gets layered on top of. CT6 fix-round BLOCKER B1: the /// Character window's resize clamp is authored on the shared /// gmPanelUI host (0x100005FE, LayoutDesc /// 0x2100006E) — that host IS retail's outer frame (5px bevel + /// 300x362 content + 5px = 310x372), so its authored MinWidth/MinHeight/ /// MaxWidth/MaxHeight ALREADY include the chrome our NineSlice wrapper /// draws. Adding the wrapper's own chrome inset on top double-counts /// the bevel and clamps the mounted window BELOW its own authored /// minimum. Set true only when the DAT source element genuinely already /// represents the complete outer frame, not a content root — the /// default (false) preserves the normal content-plus-chrome behavior /// every other window relies on. /// public bool DatConstraintSourceIsOuterFrame { 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 DrawChromeCenter { get; init; } = true; public bool Visible { get; init; } = true; /// /// Increment when the authored default extent changes incompatibly. /// Persistence preserves position and window state while resetting stale /// saved dimensions to this mount's current DAT-authored extent. /// public int AuthoredGeometryRevision { get; init; } /// /// Top-level desktop anchors. Most movable windows are unanchored; fixed /// retail HUD roots can preserve their authored screen-edge margins. /// public AnchorEdges OuterAnchors { get; init; } = AnchorEdges.None; 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)), }; if (outerFrame is UiNineSlicePanel nineSlice) nineSlice.DrawCenterFill = options.DrawChromeCenter; 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 = options.OuterAnchors; 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; int constraintInset = options.DatConstraintSourceIsOuterFrame ? 0 : inset; outerFrame.MinWidth = ResolveConstraint( options.MinWidth, options.DatConstraintSource, 0x3Fu, outerWidth, constraintInset); outerFrame.MinHeight = ResolveConstraint( options.MinHeight, options.DatConstraintSource, 0x3Eu, outerHeight, constraintInset); outerFrame.MaxWidth = Math.Max( outerFrame.MinWidth, ResolveConstraint(options.MaxWidth, options.DatConstraintSource, 0x3Du, float.MaxValue, constraintInset)); outerFrame.MaxHeight = Math.Max( outerFrame.MinHeight, ResolveConstraint(options.MaxHeight, options.DatConstraintSource, 0x3Cu, float.MaxValue, constraintInset)); // Mount-time invariant (CT6 fix-round BLOCKER B1): a window must never // open already violating its own clamp. This would have caught B1 at // the first test run — the pre-fix Character mount computed outer // bounds of 320/382/1010 (300x362 content + 10px chrome inset added // TWICE) while its own MinWidth clamped to 320 but its actual outer // WIDTH was only 310, silently sitting below its own minimum until // RetailWindowManager.ResizeTo forcibly widened it despite ResizeX // being false. if (outerWidth < outerFrame.MinWidth || outerWidth > outerFrame.MaxWidth) throw new InvalidOperationException( $"RetailWindowFrame.Mount(\"{options.WindowName}\"): mounted outer width " + $"{outerWidth} is outside its own clamp [{outerFrame.MinWidth}, {outerFrame.MaxWidth}] " + "— the window would open already violating its authored resize bounds."); if (outerHeight < outerFrame.MinHeight || outerHeight > outerFrame.MaxHeight) throw new InvalidOperationException( $"RetailWindowFrame.Mount(\"{options.WindowName}\"): mounted outer height " + $"{outerHeight} is outside its own clamp [{outerFrame.MinHeight}, {outerFrame.MaxHeight}] " + "— the window would open already violating its authored resize bounds."); // 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, options.AuthoredGeometryRevision); } 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; } }