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

@ -180,8 +180,9 @@ public class ChatLayoutConformanceTests
maxMin.OnClick!();
Assert.True(controller.IsMaximized);
Assert.Equal(90f, handle.Top);
Assert.Equal(130f, handle.Top);
Assert.Equal(370f, handle.Height); // DAT max 360 + 2*5px shared chrome
Assert.Equal(500f, handle.Top + handle.Height); // lower edge stays fixed while growing upward
Assert.Equal(RetailUiStateIds.Maximized, maxMin.ActiveRetailStateId);
controller.Root.ApplyAnchor(handle.Width, handle.Height);
Assert.Equal(360f, controller.Root.Height);

View file

@ -106,6 +106,59 @@ public sealed class RetailWindowFrameTests
Assert.False(content.Resizable);
}
[Fact]
public void HiddenInventory_RestoredExpandedBeforeFirstDraw_GrowsContentsRows()
{
ImportedLayout layout = FixtureLoader.LoadInventory();
UiItemList grid = Assert.IsType<UiItemList>(
layout.FindElement(InventoryController.ContentsGridId));
grid.Columns = 6;
grid.CellWidth = 32f;
grid.CellHeight = 32f;
grid.Flush();
for (int i = 0; i < 102; i++)
grid.AddItem(new UiItemSlot());
var root = new UiRoot { Width = 1280, Height = 720 };
RetailWindowHandle handle = RetailWindowFrame.Mount(
root,
layout.Root,
NoTex,
new RetailWindowFrame.Options
{
WindowName = WindowNames.Inventory,
Chrome = RetailWindowChrome.NineSlice,
ContentWidth = layout.Root.Width,
ContentHeight = layout.Root.Height,
Visible = false,
ResizeX = false,
ResizeY = true,
ResizableEdges = ResizeEdges.Bottom,
MaxHeight = 560f,
});
// Production installs the controller-owned policies after mounting, while
// inventory is still hidden, and captures their design baseline immediately.
InventoryController.ConfigureResizeLayout(layout);
// Persistence restores the hidden window before its first visible draw.
handle.ResizeTo(handle.Width, 527f);
ApplyAnchors(handle.OuterFrame);
grid.LayoutCells();
Assert.Equal(251f, grid.Height);
Assert.Equal(42, grid.Children.Count(child => child.Visible));
}
private static void ApplyAnchors(UiElement parent)
{
foreach (UiElement child in parent.Children)
{
child.ApplyAnchor(parent.Width, parent.Height);
ApplyAnchors(child);
}
}
private static ElementInfo Constraints(int minHeight, int maxHeight)
{
var info = new ElementInfo();

View file

@ -0,0 +1,102 @@
using AcDream.App.UI;
using AcDream.UI.Abstractions.Panels.Settings;
namespace AcDream.App.Tests.UI;
public sealed class RetailWindowLayoutPersistenceTests : IDisposable
{
private readonly string _directory = Path.Combine(
Path.GetTempPath(), "acdream-window-layout-tests-" + Guid.NewGuid().ToString("N"));
private string PathName => Path.Combine(_directory, "settings.json");
[Fact]
public void CompletedInteractions_SaveCompleteCharacterResolutionLayout()
{
var store = new SettingsStore(PathName);
var root = new UiRoot { Width = 800, Height = 600 };
RetailWindowHandle handle = Mount(root, "chat");
using var persistence = new RetailWindowLayoutPersistence(
root.WindowManager, store, () => "Alice", () => (800, 600));
handle.MoveTo(44f, 55f);
handle.ResizeTo(320f, 180f);
handle.Hide();
UiWindowLayout saved = Assert.IsType<UiWindowLayout>(
store.LoadWindowLayout("Alice", "800x600", "chat", default));
Assert.Equal((44f, 55f, 320f, 180f), (saved.X, saved.Y, saved.Width, saved.Height));
Assert.False(saved.Visible);
}
[Fact]
public void Restore_ClampsBoundsAndReappliesPanelStateAndVisibility()
{
var store = new SettingsStore(PathName);
store.SaveWindowLayout(
"Alice",
"640x480",
"chat",
new UiWindowLayout(900f, 700f, 500f, 300f, false, false, true));
var root = new UiRoot { Width = 640, Height = 480 };
var state = new FakeWindowState();
RetailWindowHandle handle = Mount(root, "chat", state);
using var persistence = new RetailWindowLayoutPersistence(
root.WindowManager, store, () => "Alice", () => (640, 480));
persistence.RestoreAll();
Assert.Equal((500f, 300f), (handle.Width, handle.Height));
Assert.Equal((140f, 180f), (handle.Left, handle.Top));
Assert.False(handle.IsVisible);
Assert.True(state.Restored.Maximized);
}
[Fact]
public void DefaultPreLoginKey_NeverOverwritesCharacterLayouts()
{
var store = new SettingsStore(PathName);
var root = new UiRoot { Width = 800, Height = 600 };
RetailWindowHandle handle = Mount(root, "radar");
using var persistence = new RetailWindowLayoutPersistence(
root.WindowManager, store, () => "default", () => (800, 600));
handle.MoveTo(123f, 234f);
Assert.Null(store.LoadWindowLayout("default", "800x600", "radar", default));
}
private static RetailWindowHandle Mount(
UiRoot root,
string name,
IRetainedWindowStateController? state = null)
{
var frame = new UiPanel
{
Left = 10f,
Top = 20f,
Width = 200f,
Height = 100f,
MinWidth = 100f,
MinHeight = 80f,
MaxWidth = 600f,
MaxHeight = 400f,
Draggable = true,
Resizable = true,
};
root.AddChild(frame);
return root.RegisterWindow(name, frame, stateController: state);
}
public void Dispose()
{
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
}
private sealed class FakeWindowState : IRetainedWindowStateController
{
public RetainedWindowState Restored { get; private set; }
public RetainedWindowState CaptureWindowState() => Restored;
public void RestoreWindowState(RetainedWindowState state) => Restored = state;
}
}

View file

@ -384,4 +384,54 @@ public sealed class SettingsStoreTests : System.IDisposable
Assert.Null(store.LoadWindowPosition("Alice", "inventory"));
Assert.True(store.LoadGameplay().LockUI);
}
[Fact]
public void WindowLayouts_RoundTripPerCharacterResolutionAndPreserveSettings()
{
var store = new SettingsStore(_tempPath);
store.SaveGameplay(GameplaySettings.Default with { LockUI = true });
var alice1080 = new UiWindowLayout(10f, 20f, 500f, 300f, true, false, true);
var alice1440 = new UiWindowLayout(30f, 40f, 650f, 420f, false, true, false);
var bob1080 = new UiWindowLayout(50f, 60f, 310f, 132f, true, false, false);
store.SaveWindowLayout("Alice", "1920x1080", "chat", alice1080);
store.SaveWindowLayout("Alice", "2560x1440", "chat", alice1440);
store.SaveWindowLayout("Bob", "1920x1080", "toolbar", bob1080);
Assert.Equal(alice1080, store.LoadWindowLayout("Alice", "1920x1080", "chat", default));
Assert.Equal(alice1440, store.LoadWindowLayout("Alice", "2560x1440", "chat", default));
Assert.Equal(bob1080, store.LoadWindowLayout("Bob", "1920x1080", "toolbar", default));
Assert.Null(store.LoadWindowLayout("Alice", "1920x1080", "inventory", default));
Assert.True(store.LoadGameplay().LockUI);
}
[Fact]
public void WindowLayouts_MigrateLegacyRadarPositionOverCurrentDefaults()
{
var store = new SettingsStore(_tempPath);
store.SaveWindowPosition("Alice", "radar", new UiWindowPosition(321.5f, 18f));
var current = new UiWindowLayout(10f, 10f, 160f, 180f, true, false, false);
UiWindowLayout migrated = Assert.IsType<UiWindowLayout>(
store.LoadWindowLayout("Alice", "1920x1080", "radar", current));
Assert.Equal(321.5f, migrated.X);
Assert.Equal(18f, migrated.Y);
Assert.Equal(current.Width, migrated.Width);
Assert.Equal(current.Height, migrated.Height);
Assert.Equal(current.Visible, migrated.Visible);
}
[Fact]
public void WindowLayouts_FallBackToNearestSavedResolution()
{
var store = new SettingsStore(_tempPath);
var hd = new UiWindowLayout(100f, 110f, 400f, 250f, true, false, false);
var uhd = new UiWindowLayout(300f, 310f, 600f, 450f, true, false, false);
store.SaveWindowLayout("Alice", "1280x720", "chat", hd);
store.SaveWindowLayout("Alice", "3840x2160", "chat", uhd);
Assert.Equal(hd, store.LoadWindowLayout("Alice", "1600x900", "chat", default));
Assert.Equal(uhd, store.LoadWindowLayout("Alice", "3200x1800", "chat", default));
}
}