using System; using System.Collections.Generic; namespace AcDream.App.UI.Layout; /// /// Retained-window port of retail gmPanelUI::RecvNotice_SetPanelVisibility /// at 0x004BC6F0. The original owns one active child panel and an optional /// deferred child; this owner applies the same lifecycle to registered retained /// windows without making the toolbar authoritative for non-toolbar panels. /// public sealed class RetailPanelUiController : IDisposable { public const uint RestorePreviousPropertyId = 0x10000049u; private readonly Func _isVisible; private readonly Func _show; private readonly Func _hide; private readonly Dictionary _byPanel = new(); private readonly Dictionary _byWindow = new(StringComparer.Ordinal); private uint? _activePanel; private uint? _deferredPanel; private bool _applying; private bool _synchronizingGeometry; private PanelGeometry? _mainPanelGeometry; private bool _disposed; public RetailPanelUiController( Func isVisible, Func show, Func hide) { _isVisible = isVisible ?? throw new ArgumentNullException(nameof(isVisible)); _show = show ?? throw new ArgumentNullException(nameof(show)); _hide = hide ?? throw new ArgumentNullException(nameof(hide)); } public uint? ActivePanelId => _activePanel; /// /// Registers one of retail gmPanelUI's toolbar or detail children. /// All primary and detail children retain panel-specific content but share /// one parent geometry. A move or resize of any registered child updates /// every hidden sibling through typed window handles, so persistence sees /// the same canonical rectangle too. /// 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, sharesMainPanelGeometry: true); } /// /// Effective DAT bool property 0x10000049. Retail retains the previous /// panel only when the newly shown panel has this property and the previous /// panel does not. /// public void Register(uint panelId, string windowName, bool restorePrevious = false) => RegisterCore( panelId, windowName, restorePrevious, window: null, sharesMainPanelGeometry: false); private void RegisterCore( uint panelId, string windowName, bool restorePrevious, RetailWindowHandle? window, bool sharesMainPanelGeometry) { 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."); var entry = new PanelEntry( windowName, restorePrevious, window, sharesMainPanelGeometry); _byPanel.Add(panelId, entry); _byWindow.Add(windowName, panelId); if (window is not null) { window.Moved += OnWindowMoved; window.Resized += OnWindowResized; } if (sharesMainPanelGeometry && _mainPanelGeometry is { } geometry) ApplyWindowGeometry(entry, geometry); if (_isVisible(windowName)) ObserveWindowVisibility(windowName, visible: true); } /// /// Registers a panel directly from its fully inherited retail root so the /// authored deferred-panel behavior cannot be dropped at the mount seam. /// public void Register(uint panelId, string windowName, ElementInfo authoredRoot) { ArgumentNullException.ThrowIfNull(authoredRoot); Register( panelId, windowName, authoredRoot.TryGetEffectiveBool( RestorePreviousPropertyId, out bool restorePrevious) && restorePrevious); } public bool IsPanelVisible(uint panelId) => _byPanel.TryGetValue(panelId, out PanelEntry entry) && _isVisible(entry.WindowName); public bool TogglePanel(uint panelId) { bool visible = !IsPanelVisible(panelId); return SetPanelVisibility(panelId, visible) && visible; } public bool SetPanelVisibility(uint panelId, bool visible) { if (!_byPanel.TryGetValue(panelId, out PanelEntry requested)) return false; _applying = true; try { if (visible) { if (_activePanel == panelId) { PrepareMainPanelGeometry(requested); return _show(requested.WindowName); } uint? previousId = _activePanel; PanelEntry? previous = previousId is uint id && _byPanel.TryGetValue(id, out PanelEntry found) && _isVisible(found.WindowName) ? found : null; if (previous is { } previousEntry) CaptureMainPanelGeometry(previousEntry, synchronizeSiblings: true); _deferredPanel = requested.RestorePrevious && previous is { RestorePrevious: false } ? previousId : null; _activePanel = panelId; if (previous is not null) _hide(previous.Value.WindowName); PrepareMainPanelGeometry(requested); return _show(requested.WindowName); } if (_activePanel != panelId) { if (_deferredPanel == panelId) _deferredPanel = null; return _hide(requested.WindowName); } CaptureMainPanelGeometry(requested, synchronizeSiblings: true); bool hidden = _hide(requested.WindowName); _activePanel = null; if (_deferredPanel is not uint deferredId || !_byPanel.TryGetValue(deferredId, out PanelEntry deferred)) return hidden; _deferredPanel = null; _activePanel = deferredId; PrepareMainPanelGeometry(deferred); return _show(deferred.WindowName) || hidden; } finally { _applying = false; } } /// /// Feeds visibility changes made by persistence or another retained-window /// owner back through the canonical panel lifecycle. /// public void ObserveWindowVisibility(string windowName, bool visible) { if (_applying || !_byWindow.TryGetValue(windowName, out uint panelId)) return; SetPanelVisibility(panelId, visible); } private void OnWindowMoved(RetailWindowHandle window) { if (_disposed || _synchronizingGeometry) return; foreach (PanelEntry entry in _byPanel.Values) { if (!entry.SharesMainPanelGeometry || !ReferenceEquals(entry.Window, window)) continue; CaptureAndSynchronizeMainPanelGeometry(window); return; } } private void OnWindowResized(RetailWindowHandle window) { if (_disposed || _synchronizingGeometry) return; foreach (PanelEntry entry in _byPanel.Values) { if (!entry.SharesMainPanelGeometry || !ReferenceEquals(entry.Window, window)) continue; CaptureAndSynchronizeMainPanelGeometry(window); return; } } private void CaptureAndSynchronizeMainPanelGeometry(RetailWindowHandle source) { _mainPanelGeometry = new PanelGeometry( source.Left, source.Top, source.Width, source.Height); SynchronizeMainPanelSiblings(source); } private void CaptureMainPanelGeometry( PanelEntry entry, bool synchronizeSiblings) { if (!entry.SharesMainPanelGeometry || entry.Window is not { } window) return; _mainPanelGeometry = new PanelGeometry( window.Left, window.Top, window.Width, window.Height); if (synchronizeSiblings) SynchronizeMainPanelSiblings(window); } private void PrepareMainPanelGeometry(PanelEntry entry) { if (!entry.SharesMainPanelGeometry || entry.Window is not { } window) return; if (_mainPanelGeometry is not { } geometry) { _mainPanelGeometry = new PanelGeometry( window.Left, window.Top, window.Width, window.Height); SynchronizeMainPanelSiblings(window); return; } ApplyWindowGeometry(entry, geometry); } private void SynchronizeMainPanelSiblings(RetailWindowHandle source) { if (_mainPanelGeometry is not { } geometry) return; _synchronizingGeometry = true; try { foreach (PanelEntry sibling in _byPanel.Values) { if (!sibling.SharesMainPanelGeometry || sibling.Window is not { } window || ReferenceEquals(window, source)) continue; ApplyWindowGeometry(sibling, geometry); } } finally { _synchronizingGeometry = false; } } private static void ApplyWindowGeometry(PanelEntry entry, PanelGeometry geometry) { if (entry.Window is not { } window) return; if (window.Left != geometry.Left || window.Top != geometry.Top) window.MoveTo(geometry.Left, geometry.Top); if (window.Width != geometry.Width || window.Height != geometry.Height) window.ResizeTo(geometry.Width, geometry.Height); } public void Dispose() { if (_disposed) return; _disposed = true; foreach (PanelEntry entry in _byPanel.Values) if (entry.Window is { } window) { window.Moved -= OnWindowMoved; window.Resized -= OnWindowResized; } } private readonly record struct PanelGeometry( float Left, float Top, float Width, float Height); private readonly record struct PanelEntry( string WindowName, bool RestorePrevious, RetailWindowHandle? Window, bool SharesMainPanelGeometry); }