fix(ui): share primary panel placement

Port gmPanelUI's persistent parent placement semantics across Inventory, Character, and Magic while preserving each imported child's content and resize policy. Synchronize typed retained-window handles so drag, switching, close/reopen, and layout persistence all observe one canonical location; keep effect panels independent.

User-verified in normal Release. Release build and all 5,770 tests passed with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 09:13:31 +02:00
parent 124e046976
commit 3f3cfdac30
7 changed files with 460 additions and 9 deletions

View file

@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
/// deferred child; this owner applies the same lifecycle to registered retained
/// windows without making the toolbar authoritative for non-toolbar panels.
/// </summary>
public sealed class RetailPanelUiController
public sealed class RetailPanelUiController : IDisposable
{
public const uint RestorePreviousPropertyId = 0x10000049u;
@ -21,6 +21,9 @@ public sealed class RetailPanelUiController
private uint? _activePanel;
private uint? _deferredPanel;
private bool _applying;
private bool _synchronizingPlacement;
private PanelPlacement? _mainPanelPlacement;
private bool _disposed;
public RetailPanelUiController(
Func<string, bool> isVisible,
@ -34,21 +37,72 @@ public sealed class RetailPanelUiController
public uint? ActivePanelId => _activePanel;
/// <summary>
/// Registers one of retail <c>gmPanelUI</c>'s primary toolbar children.
/// Inventory, Character, and Magic retain panel-specific content/resize
/// policy but share the one parent placement. A drag of any registered
/// child updates every hidden sibling through typed window handles, so
/// persistence observes the same canonical position too.
/// </summary>
public void RegisterMainPanel(
uint panelId,
string windowName,
RetailWindowHandle window,
bool restorePrevious = false)
{
ArgumentNullException.ThrowIfNull(window);
if (!string.Equals(windowName, window.Name, StringComparison.Ordinal))
throw new ArgumentException(
$"Panel window name '{windowName}' does not match handle '{window.Name}'.",
nameof(windowName));
RegisterCore(
panelId,
windowName,
restorePrevious,
window,
sharesMainPanelPlacement: true);
}
/// <param name="restorePrevious">
/// Effective DAT bool property <c>0x10000049</c>. Retail retains the previous
/// panel only when the newly shown panel has this property and the previous
/// panel does not.
/// </param>
public void Register(uint panelId, string windowName, bool restorePrevious = false)
=> RegisterCore(
panelId,
windowName,
restorePrevious,
window: null,
sharesMainPanelPlacement: false);
private void RegisterCore(
uint panelId,
string windowName,
bool restorePrevious,
RetailWindowHandle? window,
bool sharesMainPanelPlacement)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (panelId == 0) throw new ArgumentOutOfRangeException(nameof(panelId));
ArgumentException.ThrowIfNullOrWhiteSpace(windowName);
if (_byPanel.ContainsKey(panelId) || _byWindow.ContainsKey(windowName))
throw new InvalidOperationException(
$"Panel {panelId} or retained window '{windowName}' is already registered.");
_byPanel.Add(panelId, new PanelEntry(windowName, restorePrevious));
var entry = new PanelEntry(
windowName,
restorePrevious,
window,
sharesMainPanelPlacement);
_byPanel.Add(panelId, entry);
_byWindow.Add(windowName, panelId);
if (window is not null)
window.Moved += OnWindowMoved;
if (sharesMainPanelPlacement && _mainPanelPlacement is { } placement)
MoveWindowTo(entry, placement);
if (_isVisible(windowName))
ObserveWindowVisibility(windowName, visible: true);
}
@ -89,7 +143,10 @@ public sealed class RetailPanelUiController
if (visible)
{
if (_activePanel == panelId)
{
PrepareMainPanelPlacement(requested);
return _show(requested.WindowName);
}
uint? previousId = _activePanel;
PanelEntry? previous = previousId is uint id
@ -98,6 +155,9 @@ public sealed class RetailPanelUiController
? found
: null;
if (previous is { } previousEntry)
CaptureMainPanelPlacement(previousEntry, synchronizeSiblings: true);
_deferredPanel = requested.RestorePrevious
&& previous is { RestorePrevious: false }
? previousId
@ -105,6 +165,7 @@ public sealed class RetailPanelUiController
_activePanel = panelId;
if (previous is not null)
_hide(previous.Value.WindowName);
PrepareMainPanelPlacement(requested);
return _show(requested.WindowName);
}
@ -114,6 +175,7 @@ public sealed class RetailPanelUiController
return _hide(requested.WindowName);
}
CaptureMainPanelPlacement(requested, synchronizeSiblings: true);
bool hidden = _hide(requested.WindowName);
_activePanel = null;
if (_deferredPanel is not uint deferredId
@ -122,6 +184,7 @@ public sealed class RetailPanelUiController
_deferredPanel = null;
_activePanel = deferredId;
PrepareMainPanelPlacement(deferred);
return _show(deferred.WindowName) || hidden;
}
finally
@ -140,5 +203,94 @@ public sealed class RetailPanelUiController
SetPanelVisibility(panelId, visible);
}
private readonly record struct PanelEntry(string WindowName, bool RestorePrevious);
private void OnWindowMoved(RetailWindowHandle window)
{
if (_disposed || _synchronizingPlacement) return;
foreach (PanelEntry entry in _byPanel.Values)
{
if (!entry.SharesMainPanelPlacement
|| !ReferenceEquals(entry.Window, window))
continue;
_mainPanelPlacement = new PanelPlacement(window.Left, window.Top);
SynchronizeMainPanelSiblings(window);
return;
}
}
private void CaptureMainPanelPlacement(
PanelEntry entry,
bool synchronizeSiblings)
{
if (!entry.SharesMainPanelPlacement || entry.Window is not { } window)
return;
_mainPanelPlacement = new PanelPlacement(window.Left, window.Top);
if (synchronizeSiblings)
SynchronizeMainPanelSiblings(window);
}
private void PrepareMainPanelPlacement(PanelEntry entry)
{
if (!entry.SharesMainPanelPlacement || entry.Window is not { } window)
return;
if (_mainPanelPlacement is not { } placement)
{
_mainPanelPlacement = new PanelPlacement(window.Left, window.Top);
SynchronizeMainPanelSiblings(window);
return;
}
MoveWindowTo(entry, placement);
}
private void SynchronizeMainPanelSiblings(RetailWindowHandle source)
{
if (_mainPanelPlacement is not { } placement) return;
_synchronizingPlacement = true;
try
{
foreach (PanelEntry sibling in _byPanel.Values)
{
if (!sibling.SharesMainPanelPlacement
|| sibling.Window is not { } window
|| ReferenceEquals(window, source))
continue;
MoveWindowTo(sibling, placement);
}
}
finally
{
_synchronizingPlacement = false;
}
}
private void MoveWindowTo(PanelEntry entry, PanelPlacement placement)
{
if (entry.Window is not { } window
|| (window.Left == placement.Left && window.Top == placement.Top))
return;
window.MoveTo(placement.Left, placement.Top);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
foreach (PanelEntry entry in _byPanel.Values)
if (entry.Window is { } window)
window.Moved -= OnWindowMoved;
}
private readonly record struct PanelPlacement(float Left, float Top);
private readonly record struct PanelEntry(
string WindowName,
bool RestorePrevious,
RetailWindowHandle? Window,
bool SharesMainPanelPlacement);
}