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:
parent
124e046976
commit
3f3cfdac30
7 changed files with 460 additions and 9 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -784,7 +784,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
|
||||
SpellbookWindowController = controller;
|
||||
UiElement root = layout.Root;
|
||||
RetailWindowFrame.Mount(
|
||||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||
Host.Root,
|
||||
root,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
|
|
@ -805,7 +805,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ContentClickThrough = false,
|
||||
Controller = controller,
|
||||
});
|
||||
_panelUi.Register(RetailPanelCatalog.Magic, WindowNames.Spellbook);
|
||||
_panelUi.RegisterMainPanel(
|
||||
RetailPanelCatalog.Magic,
|
||||
WindowNames.Spellbook,
|
||||
handle);
|
||||
Console.WriteLine("[M3] retail spellbook/component book from LayoutDesc 0x21000034.");
|
||||
}
|
||||
|
||||
|
|
@ -1134,7 +1137,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Assets.ResolveSprite,
|
||||
(request, completed) => HandleCharacterRaise(provider, request, completed),
|
||||
() => CloseWindow(WindowNames.Character));
|
||||
RetailWindowFrame.Mount(Host.Root, layout.Root, _bindings.Assets.ResolveSprite,
|
||||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||
Host.Root,
|
||||
layout.Root,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Character,
|
||||
|
|
@ -1149,7 +1155,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
ContentClickThrough = false,
|
||||
});
|
||||
_panelUi.Register(RetailPanelCatalog.Character, WindowNames.Character);
|
||||
_panelUi.RegisterMainPanel(
|
||||
RetailPanelCatalog.Character,
|
||||
WindowNames.Character,
|
||||
handle);
|
||||
Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E).");
|
||||
}
|
||||
|
||||
|
|
@ -1231,7 +1240,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Host.Root.Height - root.Top),
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
});
|
||||
_panelUi.Register(RetailPanelCatalog.Inventory, WindowNames.Inventory);
|
||||
_panelUi.RegisterMainPanel(
|
||||
RetailPanelCatalog.Inventory,
|
||||
WindowNames.Inventory,
|
||||
handle);
|
||||
|
||||
uint contents, sideBag, mainPack;
|
||||
IReadOnlyDictionary<uint, uint> paperdollEmptySprites;
|
||||
|
|
@ -1280,6 +1292,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_itemConfirmationController?.Dispose();
|
||||
_gameplayConfirmationController?.Dispose();
|
||||
DialogFactory?.Dispose();
|
||||
_panelUi.Dispose();
|
||||
Host.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue