feat(ui): unify retained window mounts
This commit is contained in:
parent
6e9e10367f
commit
a8e9503d2e
20 changed files with 823 additions and 366 deletions
|
|
@ -172,8 +172,8 @@ public static class CharacterStatController
|
|||
private const uint ScrollThumbSprite = 0x06004C63u;
|
||||
private const uint ScrollThumbTop = 0x06004C60u;
|
||||
private const uint ScrollThumbBot = 0x06004C66u;
|
||||
private const uint ScrollUpSprite = 0x06004C6Cu;
|
||||
private const uint ScrollDownSprite = 0x06004C69u;
|
||||
private const uint ScrollUpSprite = 0x06004C69u;
|
||||
private const uint ScrollDownSprite = 0x06004C6Cu;
|
||||
|
||||
private enum CharacterStatTab
|
||||
{
|
||||
|
|
@ -634,7 +634,11 @@ public static class CharacterStatController
|
|||
|
||||
if (source is UiScrollbar existingBar)
|
||||
{
|
||||
ConfigureSkillScrollbar(existingBar, spriteResolve);
|
||||
existingBar.SpriteResolve ??= id =>
|
||||
{
|
||||
var (handle, width, height) = spriteResolve(id);
|
||||
return (handle, width, height);
|
||||
};
|
||||
existingBar.Visible = false;
|
||||
return existingBar;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,14 +40,6 @@ public sealed class ChatWindowController
|
|||
private const uint SendId = 0x10000019u;
|
||||
private const uint MaxMinId = 0x1000046Fu;
|
||||
|
||||
// Scrollbar sprite ids from base layout 0x2100003E (confirmed in Task D).
|
||||
private const uint TrackSprite = 0x06004C5Fu;
|
||||
private const uint ThumbSprite = 0x06004C63u; // 3-slice middle tile
|
||||
private const uint ThumbTopSprite = 0x06004C60u; // 3-slice top cap
|
||||
private const uint ThumbBotSprite = 0x06004C66u; // 3-slice bottom cap
|
||||
private const uint UpSprite = 0x06004C6Cu; // up arrow (top button)
|
||||
private const uint DownSprite = 0x06004C69u; // down arrow (bottom button)
|
||||
|
||||
// Channel menu sprite ids (confirmed in chat element dump).
|
||||
private const uint MenuNormal = 0x06004D65u; // button face
|
||||
private const uint MenuPressed = 0x06004D66u; // button pressed
|
||||
|
|
@ -72,6 +64,12 @@ public sealed class ChatWindowController
|
|||
/// <summary>Channel-selector menu widget.</summary>
|
||||
public UiMenu Menu { get; private set; } = null!;
|
||||
|
||||
/// <summary>Resolved gmMainChatUI root metadata, including DAT size constraints.</summary>
|
||||
public ElementInfo DatWindowInfo { get; private set; } = null!;
|
||||
|
||||
public RetailWindowHandle? WindowHandle { get; private set; }
|
||||
public bool IsMaximized => _maximized;
|
||||
|
||||
// ── Private state ──────────────────────────────────────────────────────
|
||||
|
||||
private ChatChannelKind _activeChannel = ChatChannelKind.Say;
|
||||
|
|
@ -121,6 +119,7 @@ public sealed class ChatWindowController
|
|||
/// <summary>Window top before maximize.</summary>
|
||||
private float _normalTop;
|
||||
private bool _maximized;
|
||||
private UiButton? _maxMinButton;
|
||||
|
||||
// ── Factory ────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -181,7 +180,11 @@ public sealed class ChatWindowController
|
|||
// GameWindow adds this to the host, which re-parents it out of the synthetic wrapper,
|
||||
// orphaning the strays so they never draw.
|
||||
var window = layout.FindElement(RootId) ?? layout.Root;
|
||||
var c = new ChatWindowController { Root = window };
|
||||
var c = new ChatWindowController
|
||||
{
|
||||
Root = window,
|
||||
DatWindowInfo = FindInfo(rootInfo, RootId) ?? rootInfo,
|
||||
};
|
||||
|
||||
// Drop the dat top resize bar (0x1000000F): it is authored 800px wide and
|
||||
// juts out of the content-width window. The host wraps this content in the
|
||||
|
|
@ -234,13 +237,7 @@ public sealed class ChatWindowController
|
|||
bar.Height = bar.Height + oldTop;
|
||||
bar.ResetAnchorCapture();
|
||||
bar.Model = c.Transcript.Scroll;
|
||||
bar.SpriteResolve = resolve;
|
||||
bar.TrackSprite = TrackSprite;
|
||||
bar.ThumbSprite = ThumbSprite;
|
||||
bar.ThumbTopSprite = ThumbTopSprite;
|
||||
bar.ThumbBotSprite = ThumbBotSprite;
|
||||
bar.UpSprite = UpSprite;
|
||||
bar.DownSprite = DownSprite;
|
||||
bar.SpriteResolve ??= resolve;
|
||||
c.Scrollbar = bar;
|
||||
}
|
||||
|
||||
|
|
@ -301,7 +298,7 @@ public sealed class ChatWindowController
|
|||
ReflowInputRow();
|
||||
}
|
||||
|
||||
// ── Max/min toggle — simplified gmMainChatUI::HandleMaximizeButton ──
|
||||
// ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ──
|
||||
if (layout.FindElement(MaxMinId) is UiButton maxMinEl)
|
||||
{
|
||||
// The dat puts max/min and the scrollbar up-button at the SAME X (both
|
||||
|
|
@ -310,6 +307,7 @@ public sealed class ChatWindowController
|
|||
if (track is not null)
|
||||
maxMinEl.Left = track.Left - maxMinEl.Width;
|
||||
maxMinEl.ResetAnchorCapture();
|
||||
c._maxMinButton = maxMinEl;
|
||||
maxMinEl.OnClick = c.ToggleMaximize;
|
||||
}
|
||||
|
||||
|
|
@ -319,28 +317,76 @@ public sealed class ChatWindowController
|
|||
// ── Max/min implementation ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Toggle between the normal chat window height and an expanded 320px height.
|
||||
/// Simplified port of retail <c>gmMainChatUI::HandleMaximizeButton @0x4cddb0</c>:
|
||||
/// retail stores the pre-maximize height and restores it on a second click.
|
||||
/// The 320px expanded size is the approximate retail maximized chat height.
|
||||
/// Attach the typed outer-frame handle after the controller's imported content
|
||||
/// has been mounted. Maximize/restore must resize this frame, not the child root.
|
||||
/// </summary>
|
||||
public void AttachWindow(RetailWindowHandle handle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handle);
|
||||
if (!ReferenceEquals(handle.ContentRoot, Root))
|
||||
throw new ArgumentException("Chat handle content root does not match the bound layout.", nameof(handle));
|
||||
if (WindowHandle is not null && !ReferenceEquals(WindowHandle, handle))
|
||||
throw new InvalidOperationException("Chat controller is already attached to another window.");
|
||||
WindowHandle = handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exact control flow from <c>gmMainChatUI::HandleMaximizeButton @0x004CCE50</c>:
|
||||
/// save/restore Y+height, expand by half the parent, choose up/down growth from
|
||||
/// available space, clamp through the mounted frame's DAT constraints, and set
|
||||
/// the imported max/min button state.
|
||||
/// </summary>
|
||||
private void ToggleMaximize()
|
||||
{
|
||||
if (!_maximized)
|
||||
if (WindowHandle is not { IsRegistered: true } handle)
|
||||
return;
|
||||
|
||||
UiElement frame = handle.OuterFrame;
|
||||
float parentHeight = frame.Parent?.Height ?? 0f;
|
||||
if (parentHeight <= 0f)
|
||||
return;
|
||||
|
||||
if (_maximized)
|
||||
{
|
||||
_normalHeight = Root.Height;
|
||||
_normalTop = Root.Top;
|
||||
// Expand upward: move the top edge up so the bottom stays anchored.
|
||||
Root.Top = MathF.Max(0f, Root.Top + Root.Height - 320f);
|
||||
Root.Height = 320f;
|
||||
_maximized = true;
|
||||
float restoredHeight = Math.Clamp(_normalHeight, frame.MinHeight, frame.MaxHeight);
|
||||
float restoredTop = Math.Clamp(
|
||||
_normalTop,
|
||||
0f,
|
||||
MathF.Max(0f, parentHeight - frame.MinHeight));
|
||||
_maximized = false;
|
||||
_maxMinButton?.TrySetRetailState(RetailUiStateIds.Minimized);
|
||||
handle.ResizeTo(frame.Width, restoredHeight);
|
||||
handle.MoveTo(frame.Left, restoredTop);
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
_normalTop = frame.Top;
|
||||
_normalHeight = frame.Height;
|
||||
|
||||
float expansion = parentHeight / 2f;
|
||||
float targetTop = frame.Top;
|
||||
float targetHeight = frame.Height + expansion;
|
||||
if (frame.Top + targetHeight > parentHeight || frame.Top >= parentHeight / 2f)
|
||||
targetTop = MathF.Max(0f, frame.Top - expansion);
|
||||
|
||||
targetHeight = MathF.Min(targetHeight, parentHeight - targetTop);
|
||||
targetHeight = Math.Clamp(targetHeight, frame.MinHeight, frame.MaxHeight);
|
||||
|
||||
_maximized = true;
|
||||
_maxMinButton?.TrySetRetailState(RetailUiStateIds.Maximized);
|
||||
handle.ResizeTo(frame.Width, targetHeight);
|
||||
handle.MoveTo(frame.Left, targetTop);
|
||||
}
|
||||
|
||||
private static ElementInfo? FindInfo(ElementInfo node, uint id)
|
||||
{
|
||||
if (node.Id == id) return node;
|
||||
foreach (ElementInfo child in node.Children)
|
||||
{
|
||||
Root.Top = _normalTop;
|
||||
Root.Height = _normalHeight;
|
||||
_maximized = false;
|
||||
ElementInfo? found = FindInfo(child, id);
|
||||
if (found is not null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ public static class DatWidgetFactory
|
|||
6 => new UiMenu(), // UIElement_Menu (reg :120163)
|
||||
7 => BuildMeter(info, resolve, elementFont), // UIElement_Meter
|
||||
0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf
|
||||
11 => new UiScrollbar(), // UIElement_Scrollbar (reg :124137)
|
||||
11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar (reg :124137)
|
||||
12 => BuildText(info, resolve, elementFont), // UIElement_Text (reg :115655)
|
||||
0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
|
||||
_ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers)
|
||||
|
|
@ -129,6 +129,68 @@ public static class DatWidgetFactory
|
|||
return e;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bind inherited scrollbar media structurally. Property 0x77 names the
|
||||
/// increment button and 0x78 the decrement button; the remaining Type-1
|
||||
/// child is the thumb with ordered top/middle/bottom image slices.
|
||||
/// </summary>
|
||||
private static UiScrollbar BuildScrollbar(
|
||||
ElementInfo info,
|
||||
Func<uint, (uint tex, int w, int h)> resolve)
|
||||
{
|
||||
var bar = new UiScrollbar
|
||||
{
|
||||
SpriteResolve = resolve,
|
||||
TrackSprite = DefaultImage(info),
|
||||
};
|
||||
|
||||
uint incrementId = ReferencedElementId(info, 0x77u);
|
||||
uint decrementId = ReferencedElementId(info, 0x78u);
|
||||
ElementInfo? increment = info.Children.FirstOrDefault(child => child.Id == incrementId);
|
||||
ElementInfo? decrement = info.Children.FirstOrDefault(child => child.Id == decrementId);
|
||||
bar.UpSprite = decrement is null ? 0u : DefaultImage(decrement);
|
||||
bar.DownSprite = increment is null ? 0u : DefaultImage(increment);
|
||||
|
||||
ElementInfo? thumb = info.Children.FirstOrDefault(child =>
|
||||
child.Type == 1u && child.Id != incrementId && child.Id != decrementId);
|
||||
if (thumb is not null)
|
||||
{
|
||||
ElementInfo[] slices = thumb.Children
|
||||
.Where(child => DefaultImage(child) != 0u)
|
||||
.OrderBy(child => child.Y)
|
||||
.ThenBy(child => child.ReadOrder)
|
||||
.ToArray();
|
||||
if (slices.Length > 0) bar.ThumbTopSprite = DefaultImage(slices[0]);
|
||||
if (slices.Length > 1) bar.ThumbSprite = DefaultImage(slices[1]);
|
||||
if (slices.Length > 2) bar.ThumbBotSprite = DefaultImage(slices[^1]);
|
||||
}
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
private static uint ReferencedElementId(ElementInfo info, uint propertyId)
|
||||
{
|
||||
if (!info.TryGetEffectiveProperty(propertyId, out var property))
|
||||
return 0u;
|
||||
return property.Kind switch
|
||||
{
|
||||
UiPropertyKind.Enum or UiPropertyKind.DataId => (uint)property.UnsignedValue,
|
||||
UiPropertyKind.Integer when property.IntegerValue >= 0 => (uint)property.IntegerValue,
|
||||
_ => 0u,
|
||||
};
|
||||
}
|
||||
|
||||
private static uint DefaultImage(ElementInfo info)
|
||||
{
|
||||
uint stateId = info.EffectiveDefaultStateId();
|
||||
if (info.States.TryGetValue(stateId, out var state) && state.Image is { } image)
|
||||
return image.File;
|
||||
if (info.States.TryGetValue(UiStateInfo.DirectStateId, out var direct)
|
||||
&& direct.Image is { } directImage)
|
||||
return directImage.File;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
// ── Meter ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -25,15 +25,6 @@ public sealed class InventoryController : IItemListDragHandler
|
|||
public const uint TitleTextId = 0x100001D3u; // "Inventory of <name>"
|
||||
public const uint ContentsScrollbarId = 0x100001C7u; // gm3DItemsUI gutter scrollbar (right of the grid)
|
||||
|
||||
// Scrollbar chrome from base layout 0x2100003E (shared with the chat scrollbar; see
|
||||
// ChatWindowController). Both inventory + chat scrollbars inherit this base.
|
||||
private const uint ScrollTrackSprite = 0x06004C5Fu;
|
||||
private const uint ScrollThumbSprite = 0x06004C63u; // 3-slice middle tile
|
||||
private const uint ScrollThumbTop = 0x06004C60u; // 3-slice top cap
|
||||
private const uint ScrollThumbBot = 0x06004C66u; // 3-slice bottom cap
|
||||
private const uint ScrollUpSprite = 0x06004C6Cu; // up arrow (top button)
|
||||
private const uint ScrollDownSprite = 0x06004C69u; // down arrow (bottom button)
|
||||
|
||||
// 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037).
|
||||
private const int ContentsColumns = 6;
|
||||
private const float ContentsCellPx = 32f; // gm3DItemsUI grid (192x96 = 6x3 of 32px)
|
||||
|
|
@ -108,19 +99,13 @@ public sealed class InventoryController : IItemListDragHandler
|
|||
_contentsGrid.CellHeight = ContentsCellPx;
|
||||
}
|
||||
|
||||
// Bind the gutter scrollbar to the contents grid's scroll model (the factory built
|
||||
// 0x100001C7 as a bare Type-11 UiScrollbar; wire it like ChatWindowController does).
|
||||
// Bind the gutter scrollbar to the contents grid's scroll model. The factory
|
||||
// already imported its inherited track/thumb/arrow media from LayoutDesc.
|
||||
if (_contentsGrid is not null
|
||||
&& layout.FindElement(ContentsScrollbarId) is UiScrollbar bar)
|
||||
{
|
||||
bar.Model = _contentsGrid.Scroll;
|
||||
bar.SpriteResolve = _contentsGrid.SpriteResolve; // chrome resolve from the factory ctor
|
||||
bar.TrackSprite = ScrollTrackSprite;
|
||||
bar.ThumbSprite = ScrollThumbSprite;
|
||||
bar.ThumbTopSprite = ScrollThumbTop;
|
||||
bar.ThumbBotSprite = ScrollThumbBot;
|
||||
bar.UpSprite = ScrollUpSprite;
|
||||
bar.DownSprite = ScrollDownSprite;
|
||||
bar.Model = _contentsGrid.Scroll;
|
||||
bar.SpriteResolve ??= _contentsGrid.SpriteResolve;
|
||||
}
|
||||
if (_containerList is not null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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