feat(ui): persist retained window layouts
This commit is contained in:
parent
a8e9503d2e
commit
921c388e2c
22 changed files with 705 additions and 141 deletions
|
|
@ -764,7 +764,6 @@ public sealed class GameWindow : IDisposable
|
|||
// Core + the retained controller; GameWindow supplies live state only.
|
||||
private AcDream.App.UI.Layout.RadarController? _radarController;
|
||||
private AcDream.App.UI.Layout.RadarSnapshotProvider? _radarSnapshotProvider;
|
||||
private AcDream.App.UI.UiRadar? _radarRoot;
|
||||
// Phase D.2b-B — inventory controller (backpack grid + pack-selector + burden meter).
|
||||
private AcDream.App.UI.Layout.InventoryController? _inventoryController;
|
||||
// Phase D.2b Sub-phase C — paperdoll equip-slot controller (wield drag handler).
|
||||
|
|
@ -2036,7 +2035,6 @@ public sealed class GameWindow : IDisposable
|
|||
sendRaiseSkill: (statId, cost) => _liveSession?.SendRaiseSkill(statId, cost),
|
||||
sendTrainSkill: (statId, credits) => _liveSession?.SendTrainSkill(statId, credits));
|
||||
_uiHost.Root.DragReleasedOutsideUi += OnUiDragReleasedOutside;
|
||||
_uiHost.Root.WindowMoved += OnRetailWindowMoved;
|
||||
|
||||
// Feed Silk input to the UiRoot tree so windows drag / close / select.
|
||||
// UiRoot consumes UI events; the game InputDispatcher (subscribed to the
|
||||
|
|
@ -2130,7 +2128,6 @@ public sealed class GameWindow : IDisposable
|
|||
ResolveChrome, vitalsDatFont, ResolveDatFont);
|
||||
if (radarLayout is not null && radarLayout.Root is AcDream.App.UI.UiRadar radarRoot)
|
||||
{
|
||||
_radarRoot = radarRoot;
|
||||
_radarSnapshotProvider = new AcDream.App.UI.Layout.RadarSnapshotProvider(
|
||||
Objects,
|
||||
_entitiesByServerGuid,
|
||||
|
|
@ -2162,7 +2159,6 @@ public sealed class GameWindow : IDisposable
|
|||
ConstrainDragToParent = true,
|
||||
ContentClickThrough = false,
|
||||
});
|
||||
ApplySavedRadarPosition();
|
||||
Console.WriteLine("[D.6] retail radar/compass from LayoutDesc 0x21000074.");
|
||||
}
|
||||
else
|
||||
|
|
@ -2223,6 +2219,7 @@ public sealed class GameWindow : IDisposable
|
|||
ResizeX = true,
|
||||
ResizeY = true,
|
||||
Opacity = 0.75f,
|
||||
StateController = chatController,
|
||||
});
|
||||
chatController.AttachWindow(chatHandle);
|
||||
// The LayoutDesc root is 800px wide because it shares the display
|
||||
|
|
@ -2529,27 +2526,8 @@ public sealed class GameWindow : IDisposable
|
|||
});
|
||||
var inventoryFrame = (AcDream.App.UI.UiNineSlicePanel)inventoryHandle.OuterFrame;
|
||||
|
||||
// Stretch the contents region with the window: the gm3DItemsUI sub-window + its grid
|
||||
// + scrollbar + the full-window backdrop grow vertically (Left|Top|Bottom); the
|
||||
// paperdoll + side-bag column stay pinned at the top (Left|Top). The grid's ViewHeight
|
||||
// grows → more rows + the scrollbar thumb ratio (view/content) reflects it.
|
||||
void StretchV(uint id)
|
||||
{
|
||||
if (invLayout!.FindElement(id) is { } el)
|
||||
el.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
|
||||
| AcDream.App.UI.AnchorEdges.Bottom;
|
||||
}
|
||||
void PinTopLeft(uint id)
|
||||
{
|
||||
if (invLayout!.FindElement(id) is { } el)
|
||||
el.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top;
|
||||
}
|
||||
StretchV(0x100001D0u); // full-window backdrop
|
||||
StretchV(0x100001CFu); // gm3DItemsUI sub-window (contents-grid container)
|
||||
StretchV(0x100001C6u); // contents grid (UiItemList)
|
||||
StretchV(0x100001C7u); // contents scrollbar
|
||||
PinTopLeft(0x100001CDu); // paperdoll (fixed at top)
|
||||
PinTopLeft(0x100001CEu); // side-bag column (fixed at top)
|
||||
// InventoryController owns descendant resize/reflow policy and captures
|
||||
// its hidden-window baselines when it binds below.
|
||||
|
||||
// Phase D.2b-B — populate the inventory from ClientObjectTable: the
|
||||
// "Contents of Backpack" grid + the pack-selector strip + the burden meter +
|
||||
|
|
@ -2616,6 +2594,15 @@ public sealed class GameWindow : IDisposable
|
|||
toggleCharacter: () => ToggleRetailWindow(AcDream.App.UI.WindowNames.Character));
|
||||
SyncToolbarWindowButtons();
|
||||
|
||||
if (_settingsStore is not null)
|
||||
{
|
||||
_windowLayoutPersistence = new AcDream.App.UI.RetailWindowLayoutPersistence(
|
||||
_uiHost.WindowManager,
|
||||
_settingsStore,
|
||||
characterKey: () => _activeToonKey,
|
||||
screenSize: () => (_window!.Size.X, _window.Size.Y));
|
||||
}
|
||||
|
||||
if (_options.UiProbeEnabled)
|
||||
{
|
||||
void ProbeLog(string message) => Console.WriteLine("[UI-PROBE] " + message);
|
||||
|
|
@ -2910,7 +2897,8 @@ public sealed class GameWindow : IDisposable
|
|||
_liveSession.EnterWorld(user, characterIndex: 0);
|
||||
|
||||
_activeToonKey = chosen.Name;
|
||||
ApplySavedRadarPosition();
|
||||
_windowLayoutPersistence?.RestoreAll();
|
||||
SyncToolbarWindowButtons();
|
||||
if (_settingsStore is not null && _settingsVm is not null)
|
||||
{
|
||||
var toonBag = _settingsStore.LoadCharacter(_activeToonKey);
|
||||
|
|
@ -11799,6 +11787,7 @@ public sealed class GameWindow : IDisposable
|
|||
// "default" and gets swapped to the actual character name on the
|
||||
// first EnterWorld.
|
||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsStore? _settingsStore;
|
||||
private AcDream.App.UI.RetailWindowLayoutPersistence? _windowLayoutPersistence;
|
||||
private string _activeToonKey = "default";
|
||||
|
||||
private bool ToggleRetailWindow(string name)
|
||||
|
|
@ -11842,41 +11831,6 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void ApplySavedRadarPosition()
|
||||
{
|
||||
if (_radarRoot is null || _settingsStore is null || _window is null)
|
||||
return;
|
||||
|
||||
var saved = _settingsStore.LoadWindowPosition(
|
||||
_activeToonKey, AcDream.App.UI.WindowNames.Radar);
|
||||
if (saved is not { } position)
|
||||
return;
|
||||
|
||||
_radarRoot.Left = System.Math.Clamp(
|
||||
position.X, 0f, System.Math.Max(0f, _window.Size.X - _radarRoot.Width));
|
||||
_radarRoot.Top = System.Math.Clamp(
|
||||
position.Y, 0f, System.Math.Max(0f, _window.Size.Y - _radarRoot.Height));
|
||||
}
|
||||
|
||||
private void OnRetailWindowMoved(string name, AcDream.App.UI.UiElement window)
|
||||
{
|
||||
if (name != AcDream.App.UI.WindowNames.Radar || _settingsStore is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_settingsStore.SaveWindowPosition(
|
||||
_activeToonKey,
|
||||
name,
|
||||
new AcDream.UI.Abstractions.Panels.Settings.UiWindowPosition(
|
||||
window.Left, window.Top));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"settings: radar position save failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRetailCursorFeedback()
|
||||
{
|
||||
if (_uiHost is null || _cursorFeedbackController is null || _retailCursorManager is null || _input is null)
|
||||
|
|
@ -14097,6 +14051,8 @@ public sealed class GameWindow : IDisposable
|
|||
_paperdollViewportRenderer?.Dispose(); // Slice 2: the doll's off-screen FBO + textures
|
||||
_particleRenderer?.Dispose();
|
||||
_debugLines?.Dispose();
|
||||
_windowLayoutPersistence?.Dispose();
|
||||
_windowLayoutPersistence = null;
|
||||
_uiHost?.Dispose();
|
||||
_textRenderer?.Dispose();
|
||||
_debugFont?.Dispose();
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ internal static class MockupDesktop
|
|||
ResizeX = true,
|
||||
ResizeY = true,
|
||||
Opacity = 0.75f,
|
||||
StateController = controller,
|
||||
});
|
||||
controller.AttachWindow(handle);
|
||||
|
||||
|
|
|
|||
18
src/AcDream.App/UI/IRetainedWindowStateController.cs
Normal file
18
src/AcDream.App/UI/IRetainedWindowStateController.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Panel state that is not completely described by outer-frame bounds.</summary>
|
||||
public readonly record struct RetainedWindowState(
|
||||
bool Collapsed = false,
|
||||
bool Maximized = false,
|
||||
float? PersistedTop = null,
|
||||
float? PersistedHeight = null);
|
||||
|
||||
/// <summary>
|
||||
/// Optional state seam used by retained-window persistence. Bounds are restored
|
||||
/// first; the controller then reapplies collapsed/maximized presentation.
|
||||
/// </summary>
|
||||
public interface IRetainedWindowStateController
|
||||
{
|
||||
RetainedWindowState CaptureWindowState();
|
||||
void RestoreWindowState(RetainedWindowState state);
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// and bound in place.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ChatWindowController
|
||||
public sealed class ChatWindowController : IRetainedWindowStateController
|
||||
{
|
||||
public const uint LayoutId = 0x21000006u;
|
||||
|
||||
|
|
@ -364,13 +364,16 @@ public sealed class ChatWindowController
|
|||
_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);
|
||||
|
||||
float targetHeight = Math.Clamp(
|
||||
MathF.Min(frame.Height + expansion, parentHeight),
|
||||
frame.MinHeight,
|
||||
frame.MaxHeight);
|
||||
bool growUp = frame.Top + targetHeight > parentHeight
|
||||
|| frame.Top >= parentHeight / 2f;
|
||||
float targetTop = growUp
|
||||
? MathF.Max(0f, frame.Top - (targetHeight - frame.Height))
|
||||
: frame.Top;
|
||||
targetHeight = MathF.Min(targetHeight, parentHeight - targetTop);
|
||||
targetHeight = Math.Clamp(targetHeight, frame.MinHeight, frame.MaxHeight);
|
||||
|
||||
_maximized = true;
|
||||
_maxMinButton?.TrySetRetailState(RetailUiStateIds.Maximized);
|
||||
|
|
@ -378,6 +381,21 @@ public sealed class ChatWindowController
|
|||
handle.MoveTo(frame.Left, targetTop);
|
||||
}
|
||||
|
||||
public RetainedWindowState CaptureWindowState()
|
||||
=> new(
|
||||
Maximized: _maximized,
|
||||
PersistedTop: _maximized ? _normalTop : null,
|
||||
PersistedHeight: _maximized ? _normalHeight : null);
|
||||
|
||||
public void RestoreWindowState(RetainedWindowState state)
|
||||
{
|
||||
if (state.Maximized != _maximized)
|
||||
ToggleMaximize();
|
||||
else
|
||||
_maxMinButton?.TrySetRetailState(
|
||||
_maximized ? RetailUiStateIds.Maximized : RetailUiStateIds.Minimized);
|
||||
}
|
||||
|
||||
private static ElementInfo? FindInfo(ElementInfo node, uint id)
|
||||
{
|
||||
if (node.Id == id) return node;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ public sealed class InventoryController : IItemListDragHandler
|
|||
public const uint ContentsCaptionId = 0x100001C5u; // "Contents of Backpack"
|
||||
public const uint TitleTextId = 0x100001D3u; // "Inventory of <name>"
|
||||
public const uint ContentsScrollbarId = 0x100001C7u; // gm3DItemsUI gutter scrollbar (right of the grid)
|
||||
private const uint BackdropId = 0x100001D0u;
|
||||
private const uint ContentsWindowId = 0x100001CFu;
|
||||
private const uint PaperdollWindowId = 0x100001CDu;
|
||||
private const uint BackpackWindowId = 0x100001CEu;
|
||||
|
||||
// 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037).
|
||||
private const int ContentsColumns = 6;
|
||||
|
|
@ -92,6 +96,8 @@ public sealed class InventoryController : IItemListDragHandler
|
|||
_containerList = layout.FindElement(ContainerListId) as UiItemList;
|
||||
_topContainer = layout.FindElement(TopContainerId) as UiItemList;
|
||||
|
||||
ConfigureResizeLayout(layout);
|
||||
|
||||
if (_contentsGrid is not null)
|
||||
{
|
||||
_contentsGrid.Columns = ContentsColumns;
|
||||
|
|
@ -153,6 +159,24 @@ public sealed class InventoryController : IItemListDragHandler
|
|||
Populate();
|
||||
}
|
||||
|
||||
internal static void ConfigureResizeLayout(ImportedLayout layout)
|
||||
{
|
||||
static void Set(ImportedLayout imported, uint id, AnchorEdges anchors)
|
||||
{
|
||||
if (imported.FindElement(id) is not { } element) return;
|
||||
element.Anchors = anchors;
|
||||
element.CaptureCurrentAnchorBaseline();
|
||||
}
|
||||
|
||||
AnchorEdges stretch = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom;
|
||||
Set(layout, BackdropId, stretch);
|
||||
Set(layout, ContentsWindowId, stretch);
|
||||
Set(layout, ContentsGridId, stretch);
|
||||
Set(layout, ContentsScrollbarId, stretch);
|
||||
Set(layout, PaperdollWindowId, AnchorEdges.Left | AnchorEdges.Top);
|
||||
Set(layout, BackpackWindowId, AnchorEdges.Left | AnchorEdges.Top);
|
||||
}
|
||||
|
||||
public static InventoryController Bind(
|
||||
ImportedLayout layout,
|
||||
ClientObjectTable objects,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ public static class RetailWindowFrame
|
|||
AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom;
|
||||
public bool? ContentClickThrough { get; init; }
|
||||
public IRetainedPanelController? Controller { get; init; }
|
||||
public IRetainedWindowStateController? StateController { get; init; }
|
||||
}
|
||||
|
||||
public static RetailWindowHandle Mount(
|
||||
|
|
@ -159,7 +160,8 @@ public static class RetailWindowFrame
|
|||
options.WindowName,
|
||||
outerFrame,
|
||||
content,
|
||||
options.Controller);
|
||||
options.Controller,
|
||||
options.StateController);
|
||||
}
|
||||
|
||||
private static float ResolveConstraint(
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ synthesis + six deep-dive docs under
|
|||
geometry events, lock propagation, and controller teardown.
|
||||
- `IRetainedPanelController.cs` — disposable lifecycle contract for
|
||||
panel-specific show/hide and descendant-focus behavior.
|
||||
- `RetailWindowLayoutPersistence.cs` / `IRetainedWindowStateController.cs` —
|
||||
per-character/per-resolution bounds, visibility, collapse/maximize, legacy
|
||||
migration, and safe resolution-clamped restore.
|
||||
- `Layout/RetailWindowFrame.cs` — the single production/Studio mount
|
||||
contract for imported-chrome and shared-wrapper windows.
|
||||
- `UiHost.cs` — one-shot wrapper: owns the `UiRoot`, a `TextRenderer`,
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ public sealed class RetailWindowHandle
|
|||
string name,
|
||||
UiElement outerFrame,
|
||||
UiElement contentRoot,
|
||||
IRetainedPanelController? controller)
|
||||
IRetainedPanelController? controller,
|
||||
IRetainedWindowStateController? stateController)
|
||||
{
|
||||
_owner = owner;
|
||||
Name = name;
|
||||
OuterFrame = outerFrame;
|
||||
ContentRoot = contentRoot;
|
||||
Controller = controller;
|
||||
StateController = stateController;
|
||||
_notifiedVisible = outerFrame.Visible;
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +35,7 @@ public sealed class RetailWindowHandle
|
|||
public UiElement OuterFrame { get; }
|
||||
public UiElement ContentRoot { get; }
|
||||
public IRetainedPanelController? Controller { get; }
|
||||
public IRetainedWindowStateController? StateController { get; }
|
||||
public bool IsRegistered => !_disposed;
|
||||
public bool IsVisible => OuterFrame.Visible;
|
||||
public bool IsLocked => _owner.IsLocked;
|
||||
|
|
|
|||
176
src/AcDream.App/UI/RetailWindowLayoutPersistence.cs
Normal file
176
src/AcDream.App/UI/RetailWindowLayoutPersistence.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges typed retained-window lifecycle events to per-character,
|
||||
/// per-resolution settings. It deliberately ignores the temporary pre-login
|
||||
/// <c>default</c> character key so startup layout cannot overwrite a real
|
||||
/// character's state.
|
||||
/// </summary>
|
||||
public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||
{
|
||||
private readonly RetailWindowManager _manager;
|
||||
private readonly SettingsStore _store;
|
||||
private readonly Func<string> _characterKey;
|
||||
private readonly Func<(int Width, int Height)> _screenSize;
|
||||
private readonly List<RetailWindowHandle> _attached = new();
|
||||
private bool _restoring;
|
||||
private bool _disposed;
|
||||
|
||||
public RetailWindowLayoutPersistence(
|
||||
RetailWindowManager manager,
|
||||
SettingsStore store,
|
||||
Func<string> characterKey,
|
||||
Func<(int Width, int Height)> screenSize)
|
||||
{
|
||||
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_characterKey = characterKey ?? throw new ArgumentNullException(nameof(characterKey));
|
||||
_screenSize = screenSize ?? throw new ArgumentNullException(nameof(screenSize));
|
||||
|
||||
foreach (RetailWindowHandle handle in manager.Windows)
|
||||
Attach(handle);
|
||||
}
|
||||
|
||||
/// <summary>Restore all registered windows after character and screen are known.</summary>
|
||||
public void RestoreAll()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
string character = _characterKey();
|
||||
if (!CanPersist(character)) return;
|
||||
var screen = ValidScreenSize();
|
||||
string resolution = ResolutionKey(screen);
|
||||
|
||||
_restoring = true;
|
||||
try
|
||||
{
|
||||
foreach (RetailWindowHandle handle in _attached)
|
||||
{
|
||||
UiWindowLayout fallback = Capture(handle);
|
||||
UiWindowLayout? saved = _store.LoadWindowLayout(
|
||||
character, resolution, handle.Name, fallback);
|
||||
if (saved is not { } layout) continue;
|
||||
|
||||
Apply(handle, layout, screen);
|
||||
// Lazily migrate legacy position-only entries into the complete schema.
|
||||
_store.SaveWindowLayout(character, resolution, handle.Name, Capture(handle));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_restoring = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Attach(RetailWindowHandle handle)
|
||||
{
|
||||
_attached.Add(handle);
|
||||
handle.Moved += OnChanged;
|
||||
handle.Resized += OnChanged;
|
||||
handle.Shown += OnChanged;
|
||||
handle.Hidden += OnChanged;
|
||||
}
|
||||
|
||||
private void OnChanged(RetailWindowHandle handle)
|
||||
{
|
||||
if (_restoring || _disposed) return;
|
||||
string character = _characterKey();
|
||||
if (!CanPersist(character)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var screen = ValidScreenSize();
|
||||
_store.SaveWindowLayout(
|
||||
character,
|
||||
ResolutionKey(screen),
|
||||
handle.Name,
|
||||
Capture(handle));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"settings: window layout save failed [{handle.Name}]: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static UiWindowLayout Capture(RetailWindowHandle handle)
|
||||
{
|
||||
RetainedWindowState state = handle.StateController?.CaptureWindowState() ?? default;
|
||||
return new UiWindowLayout(
|
||||
handle.Left,
|
||||
state.PersistedTop ?? handle.Top,
|
||||
handle.Width,
|
||||
state.PersistedHeight ?? handle.Height,
|
||||
handle.IsVisible,
|
||||
state.Collapsed,
|
||||
state.Maximized);
|
||||
}
|
||||
|
||||
private static void Apply(
|
||||
RetailWindowHandle handle,
|
||||
UiWindowLayout layout,
|
||||
(int Width, int Height) screen)
|
||||
{
|
||||
UiElement frame = handle.OuterFrame;
|
||||
float width = ClampDimension(layout.Width, frame.Width, frame.MinWidth, frame.MaxWidth, screen.Width);
|
||||
float height = ClampDimension(layout.Height, frame.Height, frame.MinHeight, frame.MaxHeight, screen.Height);
|
||||
handle.ResizeTo(width, height);
|
||||
|
||||
float maxX = MathF.Max(0f, screen.Width - handle.Width);
|
||||
float maxY = MathF.Max(0f, screen.Height - handle.Height);
|
||||
float x = Math.Clamp(FiniteOr(layout.X, handle.Left), 0f, maxX);
|
||||
float y = Math.Clamp(FiniteOr(layout.Y, handle.Top), 0f, maxY);
|
||||
handle.MoveTo(x, y);
|
||||
|
||||
handle.StateController?.RestoreWindowState(new RetainedWindowState(
|
||||
Collapsed: layout.Collapsed,
|
||||
Maximized: layout.Maximized));
|
||||
|
||||
if (layout.Visible) handle.Show();
|
||||
else handle.Hide();
|
||||
}
|
||||
|
||||
private (int Width, int Height) ValidScreenSize()
|
||||
{
|
||||
var screen = _screenSize();
|
||||
return (Math.Max(1, screen.Width), Math.Max(1, screen.Height));
|
||||
}
|
||||
|
||||
private static float ClampDimension(
|
||||
float saved,
|
||||
float current,
|
||||
float minimum,
|
||||
float maximum,
|
||||
int screenExtent)
|
||||
{
|
||||
float value = FiniteOr(saved, current);
|
||||
float upper = MathF.Max(minimum, MathF.Min(maximum, screenExtent));
|
||||
return Math.Clamp(value, minimum, upper);
|
||||
}
|
||||
|
||||
private static float FiniteOr(float value, float fallback)
|
||||
=> float.IsFinite(value) ? value : fallback;
|
||||
|
||||
private static string ResolutionKey((int Width, int Height) screen)
|
||||
=> $"{screen.Width}x{screen.Height}";
|
||||
|
||||
private static bool CanPersist(string key)
|
||||
=> !string.IsNullOrWhiteSpace(key)
|
||||
&& !string.Equals(key, "default", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
foreach (RetailWindowHandle handle in _attached)
|
||||
{
|
||||
handle.Moved -= OnChanged;
|
||||
handle.Resized -= OnChanged;
|
||||
handle.Shown -= OnChanged;
|
||||
handle.Hidden -= OnChanged;
|
||||
}
|
||||
_attached.Clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,8 @@ public sealed class RetailWindowManager : IDisposable
|
|||
string name,
|
||||
UiElement outerFrame,
|
||||
UiElement? contentRoot = null,
|
||||
IRetainedPanelController? controller = null)
|
||||
IRetainedPanelController? controller = null,
|
||||
IRetainedWindowStateController? stateController = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
|
|
@ -46,11 +47,15 @@ public sealed class RetailWindowManager : IDisposable
|
|||
throw new InvalidOperationException(
|
||||
$"Retained window '{name}' must be mounted as a direct UiRoot child before registration.");
|
||||
|
||||
IRetainedWindowStateController? resolvedStateController =
|
||||
stateController ?? outerFrame as IRetainedWindowStateController;
|
||||
|
||||
if (_byName.TryGetValue(name, out var sameName))
|
||||
{
|
||||
if (ReferenceEquals(sameName.OuterFrame, outerFrame)
|
||||
&& ReferenceEquals(sameName.ContentRoot, contentRoot ?? outerFrame)
|
||||
&& ReferenceEquals(sameName.Controller, controller))
|
||||
&& ReferenceEquals(sameName.Controller, controller)
|
||||
&& ReferenceEquals(sameName.StateController, resolvedStateController))
|
||||
return sameName;
|
||||
|
||||
Unregister(name);
|
||||
|
|
@ -64,7 +69,8 @@ public sealed class RetailWindowManager : IDisposable
|
|||
name,
|
||||
outerFrame,
|
||||
contentRoot ?? outerFrame,
|
||||
controller);
|
||||
controller,
|
||||
resolvedStateController);
|
||||
_byName.Add(name, handle);
|
||||
_byFrame.Add(outerFrame, handle);
|
||||
handle.NotifyInitialState();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.UI;
|
|||
/// the dragged height to the nearer stop so the frame always rests collapsed or expanded — never a
|
||||
/// half-row. Toolkit UX (keystone.dll has no decomp; the dat stacks both rows always) — see IA-17.
|
||||
/// </summary>
|
||||
public sealed class UiCollapsibleFrame : UiNineSlicePanel
|
||||
public sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStateController
|
||||
{
|
||||
public UiCollapsibleFrame(Func<uint, (uint, int, int)> resolve) : base(resolve) { }
|
||||
|
||||
|
|
@ -33,4 +33,15 @@ public sealed class UiCollapsibleFrame : UiNineSlicePanel
|
|||
|
||||
/// <summary>Test hook — OnTick is protected. Drives one snap+visibility reconcile.</summary>
|
||||
internal void TickForTest(double dt) => OnTick(dt);
|
||||
|
||||
public RetainedWindowState CaptureWindowState()
|
||||
=> new(Collapsed: !IsExpanded);
|
||||
|
||||
public void RestoreWindowState(RetainedWindowState state)
|
||||
{
|
||||
if (ExpandedHeight <= CollapsedHeight) return;
|
||||
Height = state.Collapsed ? CollapsedHeight : ExpandedHeight;
|
||||
for (int i = 0; i < SecondRow.Count; i++)
|
||||
SecondRow[i].Visible = !state.Collapsed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -526,6 +526,18 @@ public abstract class UiElement
|
|||
(int)Parent.Height));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture the current mounted rect as the baseline for a compatibility
|
||||
/// anchor policy immediately. Hidden trees do not receive a layout traversal,
|
||||
/// so lazy capture after a persistence resize would use the wrong parent size.
|
||||
/// </summary>
|
||||
internal void CaptureCurrentAnchorBaseline()
|
||||
{
|
||||
if (Parent is null || Anchors == AnchorEdges.None) return;
|
||||
_anchorCaptured = false;
|
||||
ApplyAnchor(Parent.Width, Parent.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebase each immediate child after a host deliberately changes this element's
|
||||
/// design extent. This is used when a production DAT root contains auxiliary
|
||||
|
|
|
|||
|
|
@ -111,8 +111,9 @@ public sealed class UiHost : System.IDisposable
|
|||
string name,
|
||||
UiElement window,
|
||||
UiElement? contentRoot = null,
|
||||
IRetainedPanelController? controller = null)
|
||||
=> Root.RegisterWindow(name, window, contentRoot, controller);
|
||||
IRetainedPanelController? controller = null,
|
||||
IRetainedWindowStateController? stateController = null)
|
||||
=> Root.RegisterWindow(name, window, contentRoot, controller, stateController);
|
||||
|
||||
public bool UnregisterWindow(string name) => Root.UnregisterWindow(name);
|
||||
|
||||
|
|
|
|||
|
|
@ -704,8 +704,9 @@ public sealed class UiRoot : UiElement
|
|||
string name,
|
||||
UiElement window,
|
||||
UiElement? contentRoot = null,
|
||||
IRetainedPanelController? controller = null)
|
||||
=> WindowManager.Register(name, window, contentRoot, controller);
|
||||
IRetainedPanelController? controller = null,
|
||||
IRetainedWindowStateController? stateController = null)
|
||||
=> WindowManager.Register(name, window, contentRoot, controller, stateController);
|
||||
|
||||
public bool UnregisterWindow(string name) => WindowManager.Unregister(name);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue