feat(ui): unify retained window mounts

This commit is contained in:
Erik 2026-07-10 22:22:25 +02:00
parent 6e9e10367f
commit a8e9503d2e
20 changed files with 823 additions and 366 deletions

View file

@ -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 ────────────────────────────────────────────────────────────