feat(ui): persist retained window layouts

This commit is contained in:
Erik 2026-07-10 23:17:29 +02:00
parent a8e9503d2e
commit 921c388e2c
22 changed files with 705 additions and 141 deletions

View file

@ -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();