Two independent Opus reviews of01b98ca30(Slice A - movable, collapsible plugin shelf) found blocking design and behavior issues plus several should-fix gaps. All addressed in this commit: 1. Grip/toggle are now REAL children instead of a drawn band + a cursor-position-dependent HandlesClick override + an OnEvent toggle hack: ShelfGripPanel (WindowMoveHandle=true) spans the top band minus the toggle width; a UiSimpleButton toggle sits beside it (HandlesClick already wins at UiRoot.OnMouseDown before the Draggable-window fallback). The shelf's own Draggable is now FALSE - verified against UiRoot.FindDragHandleWindow, which never reads a window's own Draggable flag at all (it walks for a WindowMoveHandle ancestor-or-self then climbs to the nearest child of UiRoot), so Draggable=true was never required for the grip to work and only armed the whole-window- drag fallback for clicks on the shelf's own padding - exactly the behavior the review asked NOT to have. The two Assert.Single(shelf. Children) test sites now filter by the (now internal) PluginShelfButton type instead of asserting child count. 2. Deleted the per-tick "always highest ZOrder" raise in OnTick. It fought RetailDialogFactory.Tick's own per-frame dialog re-raise, so a dialog opened while the shelf was visible could never end up on top of it. Registration's ordinary press-to-raise (a grip press calls BringToFront before the drag starts) remains. 3. Collapse and hide/show intent are now persisted through their own channel. RetainedWindowState gained a nullable RequestedVisible; RetailWindowHandle gained an internal StateChanged event that RetailWindowLayoutPersistence subscribes to (alongside Moved/Resized/ Shown/Hidden) and that the shelf raises after a collapse toggle or Show/Hide. Capture() now persists state.RequestedVisible (the panel's own intent) instead of the derived IsVisible, so an availability hide (last plugin window unregistered) is never mistaken for a user hide. WindowNames.PluginShelf is now one of RetailUiRuntime's state-managed visibility windows, so Apply() restores the intent through RestoreWindowState directly rather than via Show()/Hide(). 4. RetailWindowLayoutPersistence now subscribes to RetailWindowManager.WindowRegistered/WindowUnregistered so a window (a plugin window loaded after startup, or the shelf on any path that constructs persistence first) attaches even when it registers after persistence already exists. 5. Reflow()'s default height argument is now nullable and falls back to the last height OnTick actually measured (or unbounded if none yet), instead of always forcing a single-column layout - every call site OTHER than OnTick's own row-wrap (Add, unregister, Show/Hide, the collapse toggle, RestoreWindowState) used to collapse a wrapped multi-column layout to one column for a frame. 6. _userPositioned is now flipped only when the handle's position differs from the recorded dock placement, not on every RetailWindowHandle. Moved (which fires unconditionally on any completed window-drag release, including a zero-movement grip click, and on any ClampAllToScreen reachable-clamp). 7. New tests cover: a press+drag starting on an entry button does not move the shelf; the removed per-tick raise (a sibling with higher ZOrder keeps it after a tick); TogglePluginManager's hidden-and- collapsed -> shown-and-expanded / visible -> hidden transitions at the shelf API (no RetailUiRuntime construction harness exists in this test suite to exercise the action-routing switch itself - the "no plugin windows registered" message branch is therefore not covered here). 8. The hide branch of TogglePluginManager now displays "Plugin shelf hidden. Press Shift+Ctrl+F1 to show it again."; the show branch stays silent. 9. The collapse toggle now draws ASCII '<'/'>' instead of the DAT-font- dependent '«'/'»' glyphs (the only use of those code points in the App UI, silently dropped by UiDatFont when absent), and gets the same DatFont + bitmap fallback the shelf's entry buttons already have through UiSimpleButton. A new installed-DAT test pins that the default font actually carries both ASCII glyphs. 10. ResizeX/ResizeY are false on the shelf so a restored layout's saved dimensions can never stomp the derived Width/Height via ResizeTo. 11. WindowNames.PluginShelf replaces the "plugin-shelf" literal at every site (RetailUiRuntime, docs comments, tests). 12. The grip dims to half opacity while RetailWindowManager.IsLocked, the same visual cue every other retail window gets (the shelf's grip has no DatElementId, so RetailWindowLockPresentationController's authored- chrome dimming does not reach it on its own). Every new test was verified to fail against the pre-fix source: reverting src/AcDream.App/UI/PluginSidePanel.cs, IRetainedWindowStateController.cs, RetailWindowHandle.cs, WindowNames.cs, and RetailUiRuntime.cs to their01b98ca30state makes the whole PluginSidePanelTests.cs file fail to even compile (missing WindowNames.PluginShelf, the now-internal PluginShelfButton type, and RetainedWindowState.RequestedVisible); reverting RetailWindowLayoutPersistence.cs alone (fixed source elsewhere) makes WindowRegisteredAfterConstruction_StillRoundTrips fail at runtime with a null saved layout, confirming finding 4 in isolation. Verified: dotnet build src/AcDream.App (Release) green; dotnet test tests/AcDream.App.Tests (Release) targeted filter (PluginSidePanel|RetailWindowLayout|Markup|UiRootInput) 105/105 green; full suite 7303 passed / 97 skipped / 36 failed (identical failure set to the pre-fix-round baseline - installed-DAT live-mount probes, Linux-only pacing/credential tests, and alpha-flush COUNT-only conformance divergences, none touching plugin UI; +9 tests, all passing, over the prior 7294/97/36 baseline). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
485 lines
18 KiB
C#
485 lines
18 KiB
C#
using AcDream.App.UI;
|
||
using AcDream.App.UI.Layout;
|
||
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 MainPanelMove_PersistsCanonicalPositionForEveryPrimaryChild()
|
||
{
|
||
var store = new SettingsStore(PathName);
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
RetailWindowHandle inventory = Mount(root, WindowNames.Inventory);
|
||
RetailWindowHandle character = Mount(root, WindowNames.Character);
|
||
inventory.Hide();
|
||
character.Hide();
|
||
|
||
using var panels = new RetailPanelUiController(
|
||
root.IsWindowVisible,
|
||
root.ShowWindow,
|
||
root.HideWindow);
|
||
panels.RegisterMainPanel(
|
||
RetailPanelCatalog.Inventory,
|
||
WindowNames.Inventory,
|
||
inventory);
|
||
panels.RegisterMainPanel(
|
||
RetailPanelCatalog.Character,
|
||
WindowNames.Character,
|
||
character);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager,
|
||
store,
|
||
() => "Alice",
|
||
() => (800, 600));
|
||
|
||
panels.SetPanelVisibility(RetailPanelCatalog.Inventory, visible: true);
|
||
inventory.MoveTo(244f, 133f);
|
||
|
||
UiWindowLayout savedInventory = Assert.IsType<UiWindowLayout>(
|
||
store.LoadWindowLayout(
|
||
"Alice", "800x600", WindowNames.Inventory, default));
|
||
UiWindowLayout savedCharacter = Assert.IsType<UiWindowLayout>(
|
||
store.LoadWindowLayout(
|
||
"Alice", "800x600", WindowNames.Character, default));
|
||
Assert.Equal((244f, 133f), (savedInventory.X, savedInventory.Y));
|
||
Assert.Equal((244f, 133f), (savedCharacter.X, savedCharacter.Y));
|
||
}
|
||
|
||
[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 Restore_OlderAuthoredGeometryRevision_ResetsOnlySavedExtent()
|
||
{
|
||
var store = new SettingsStore(PathName);
|
||
store.SaveWindowLayout(
|
||
"Alice",
|
||
"800x600",
|
||
WindowNames.Examination,
|
||
new UiWindowLayout(
|
||
44f,
|
||
55f,
|
||
310f,
|
||
545f,
|
||
true,
|
||
false,
|
||
false,
|
||
AuthoredGeometryRevision: 0));
|
||
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
RetailWindowHandle handle = Mount(
|
||
root,
|
||
WindowNames.Examination,
|
||
authoredGeometryRevision: 1,
|
||
width: 310f,
|
||
height: 400f);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager,
|
||
store,
|
||
() => "Alice",
|
||
() => (800, 600));
|
||
|
||
persistence.RestoreAll();
|
||
|
||
Assert.Equal((44f, 55f), (handle.Left, handle.Top));
|
||
Assert.Equal((310f, 400f), (handle.Width, handle.Height));
|
||
UiWindowLayout migrated = Assert.IsType<UiWindowLayout>(
|
||
store.LoadWindowLayout(
|
||
"Alice",
|
||
"800x600",
|
||
WindowNames.Examination,
|
||
default));
|
||
Assert.Equal(1, migrated.AuthoredGeometryRevision);
|
||
}
|
||
|
||
[Fact]
|
||
public void RestoreNamed_OlderRevisionUsesMountedAuthoredExtent_NotCurrentSize()
|
||
{
|
||
var store = new SettingsStore(PathName);
|
||
store.SaveNamedWindowLayout(
|
||
"legacy",
|
||
WindowNames.Examination,
|
||
new UiWindowLayout(
|
||
44f,
|
||
55f,
|
||
310f,
|
||
545f,
|
||
true,
|
||
false,
|
||
false,
|
||
AuthoredGeometryRevision: 0));
|
||
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
RetailWindowHandle handle = Mount(
|
||
root,
|
||
WindowNames.Examination,
|
||
authoredGeometryRevision: 1,
|
||
width: 310f,
|
||
height: 400f);
|
||
handle.ResizeTo(500f, 300f);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager,
|
||
store,
|
||
() => "Alice",
|
||
() => (800, 600));
|
||
|
||
persistence.RestoreNamed("legacy");
|
||
|
||
Assert.Equal((44f, 55f), (handle.Left, handle.Top));
|
||
Assert.Equal((310f, 400f), (handle.Width, handle.Height));
|
||
}
|
||
|
||
[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));
|
||
}
|
||
|
||
[Fact]
|
||
public void Restore_StateManagedWindow_PreservesAuthoritativeVisibility()
|
||
{
|
||
var store = new SettingsStore(PathName);
|
||
store.SaveWindowLayout(
|
||
"Alice",
|
||
"800x600",
|
||
"combat",
|
||
new UiWindowLayout(44f, 55f, 200f, 100f, true, false, false));
|
||
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
RetailWindowHandle handle = Mount(root, "combat");
|
||
handle.Hide();
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager,
|
||
store,
|
||
() => "Alice",
|
||
() => (800, 600),
|
||
stateManagedVisibilityWindows: ["combat"]);
|
||
|
||
persistence.RestoreAll();
|
||
|
||
Assert.Equal((44f, 55f), (handle.Left, handle.Top));
|
||
Assert.False(handle.IsVisible);
|
||
|
||
// Combat mode owns subsequent show/hide transitions as well; they must
|
||
// not turn into saved user visibility preferences.
|
||
handle.Show();
|
||
UiWindowLayout saved = Assert.IsType<UiWindowLayout>(
|
||
store.LoadWindowLayout("Alice", "800x600", "combat", default));
|
||
Assert.False(saved.Visible);
|
||
}
|
||
|
||
[Fact]
|
||
public void NamedProfile_SaveAndRestore_AppliesEveryMountedWindow()
|
||
{
|
||
var store = new SettingsStore(PathName);
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
RetailWindowHandle chat = Mount(root, "chat");
|
||
RetailWindowHandle radar = Mount(root, "radar");
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager, store, () => "Alice", () => (800, 600));
|
||
|
||
chat.MoveTo(44f, 55f);
|
||
radar.MoveTo(300f, 120f);
|
||
radar.Hide();
|
||
persistence.SaveNamed("hunting");
|
||
|
||
chat.MoveTo(1f, 2f);
|
||
radar.MoveTo(3f, 4f);
|
||
radar.Show();
|
||
persistence.RestoreNamed("hunting");
|
||
|
||
Assert.Equal((44f, 55f), (chat.Left, chat.Top));
|
||
Assert.Equal((300f, 120f), (radar.Left, radar.Top));
|
||
Assert.False(radar.IsVisible);
|
||
}
|
||
|
||
// ── #390: retail's display-change anti-stranding cascade ────────────
|
||
// The clamp rule is each floating window's own MoveTo override,
|
||
// x = max(0, min(x, parentW − selfW)) (top-left priority), re-applied
|
||
// unconditionally on every display change — see
|
||
// docs/research/2026-08-13-retail-ui-display-change.md.
|
||
|
||
[Fact]
|
||
public void ClampAllToScreen_ReclampsStrandedWindows_TopLeftPriority()
|
||
{
|
||
var store = new SettingsStore(PathName);
|
||
var root = new UiRoot { Width = 2560, Height = 1440 };
|
||
RetailWindowHandle handle = Mount(
|
||
root, WindowNames.Examination, width: 310f, height: 400f);
|
||
handle.MoveTo(2200f, 1000f); // parked near the bottom-right corner
|
||
(int Width, int Height) screen = (2560, 1440);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager, store, () => "Alice", () => screen);
|
||
|
||
screen = (1280, 720); // the display shrank
|
||
persistence.ClampAllToScreen();
|
||
|
||
Assert.Equal(1280f - 310f, handle.Left);
|
||
Assert.Equal(720f - 400f, handle.Top);
|
||
}
|
||
|
||
[Fact]
|
||
public void ClampAllToScreen_OversizedWindow_PinsTopLeftToZero()
|
||
{
|
||
// max(0, min(...)) — the max applies LAST, so a window larger than
|
||
// the screen pins to 0 and its top-left chrome stays reachable.
|
||
var store = new SettingsStore(PathName);
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
RetailWindowHandle handle = Mount(
|
||
root, WindowNames.Examination, width: 310f, height: 400f);
|
||
handle.MoveTo(400f, 300f);
|
||
(int Width, int Height) screen = (800, 600);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager, store, () => "Alice", () => screen);
|
||
|
||
screen = (200, 200); // smaller than the window itself
|
||
persistence.ClampAllToScreen();
|
||
|
||
Assert.Equal(0f, handle.Left);
|
||
Assert.Equal(0f, handle.Top);
|
||
}
|
||
|
||
[Fact]
|
||
public void ClampAllToScreen_InBoundsWindow_DoesNotMove()
|
||
{
|
||
var store = new SettingsStore(PathName);
|
||
var root = new UiRoot { Width = 2560, Height = 1440 };
|
||
RetailWindowHandle handle = Mount(
|
||
root, WindowNames.Examination, width: 310f, height: 400f);
|
||
handle.MoveTo(100f, 120f);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager, store, () => "Alice", () => (1280, 720));
|
||
|
||
persistence.ClampAllToScreen();
|
||
|
||
Assert.Equal((100f, 120f), (handle.Left, handle.Top));
|
||
}
|
||
|
||
[Fact]
|
||
public void ClampAllToScreen_DoesNotSaveTheClampedPositions()
|
||
{
|
||
// The clamp runs per resize FRAME during a live window drag; its
|
||
// MoveTo must not trip the per-move save (settings.json writes per
|
||
// frame, keyed by whatever intermediate resolution the drag passes
|
||
// through). Only USER moves save.
|
||
var store = new SettingsStore(PathName);
|
||
var root = new UiRoot { Width = 2560, Height = 1440 };
|
||
RetailWindowHandle handle = Mount(
|
||
root, WindowNames.Examination, width: 310f, height: 400f);
|
||
handle.MoveTo(2200f, 1000f); // pre-attach: no save subscription yet
|
||
(int Width, int Height) screen = (2560, 1440);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager, store, () => "Alice", () => screen);
|
||
|
||
screen = (1280, 720);
|
||
persistence.ClampAllToScreen();
|
||
|
||
Assert.Equal(1280f - 310f, handle.Left); // it DID clamp…
|
||
Assert.Null(store.LoadWindowLayout( // …and did NOT write.
|
||
"Alice", "1280x720", WindowNames.Examination, default));
|
||
}
|
||
|
||
[Fact]
|
||
public void RestoreAfterDisplayChange_WritesNothingToTheStore()
|
||
{
|
||
// The live display-change reload (#390) must not persist — retail
|
||
// saves layouts only via @saveui/@saveautoui, and a mid-drag reload
|
||
// writing would litter settings.json with intermediate-resolution
|
||
// keys.
|
||
var store = new SettingsStore(PathName);
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
_ = Mount(root, WindowNames.Examination, width: 310f, height: 400f);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager, store, () => "Alice", () => (800, 600));
|
||
|
||
persistence.RestoreAfterDisplayChange();
|
||
|
||
Assert.Null(store.LoadWindowLayout(
|
||
"Alice", "800x600", WindowNames.Examination, default));
|
||
}
|
||
|
||
[Fact]
|
||
public void RestoreAfterDisplayChange_RestoresGeometryButPreservesLiveVisibility()
|
||
{
|
||
// Retail's global-message-0xE auto-layout file contains only X/Y/W/H.
|
||
// Our richer ambient settings schema also remembers visibility and
|
||
// collapsed/maximized state, but replaying visibility during a
|
||
// resolution change used to hide an open Options panel. Config's OnHidden reset
|
||
// then restored the previous resolution, creating the user-observed
|
||
// resize-out/resize-back blip.
|
||
var store = new SettingsStore(PathName);
|
||
store.SaveWindowLayout(
|
||
"Alice",
|
||
"1280x720",
|
||
WindowNames.Options,
|
||
new UiWindowLayout(
|
||
X: 700f,
|
||
Y: 200f,
|
||
Width: 310f,
|
||
Height: 400f,
|
||
Visible: false,
|
||
Collapsed: true,
|
||
Maximized: true));
|
||
|
||
var root = new UiRoot { Width = 1280, Height = 720 };
|
||
var state = new FakeWindowState();
|
||
var lifecycle = new FakePanelController();
|
||
RetailWindowHandle handle = Mount(
|
||
root,
|
||
WindowNames.Options,
|
||
state,
|
||
controller: lifecycle,
|
||
width: 300f,
|
||
height: 300f);
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager,
|
||
store,
|
||
() => "Alice",
|
||
() => (1280, 720));
|
||
|
||
persistence.RestoreAfterDisplayChange();
|
||
|
||
Assert.Equal((700f, 200f, 310f, 400f),
|
||
(handle.Left, handle.Top, handle.Width, handle.Height));
|
||
Assert.True(handle.IsVisible);
|
||
Assert.Equal(0, lifecycle.HiddenCount);
|
||
Assert.Equal(1, state.RestoreCount);
|
||
Assert.True(state.Restored.Collapsed);
|
||
Assert.True(state.Restored.Maximized);
|
||
}
|
||
|
||
// ── Review fix round finding 4 (plugin-shelf campaign, 2026-09-06):
|
||
// attach late registrations, not only windows present at construction ──
|
||
|
||
[Fact]
|
||
public void WindowRegisteredAfterConstruction_StillRoundTrips()
|
||
{
|
||
var store = new SettingsStore(PathName);
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
using var persistence = new RetailWindowLayoutPersistence(
|
||
root.WindowManager, store, () => "Alice", () => (800, 600));
|
||
|
||
// The window is registered AFTER persistence already exists — before
|
||
// this fix only windows present in RetailWindowManager.Windows at
|
||
// construction time ever got a save subscription.
|
||
RetailWindowHandle handle = Mount(root, "late-window");
|
||
handle.MoveTo(77f, 88f);
|
||
|
||
UiWindowLayout saved = Assert.IsType<UiWindowLayout>(
|
||
store.LoadWindowLayout("Alice", "800x600", "late-window", default));
|
||
Assert.Equal((77f, 88f), (saved.X, saved.Y));
|
||
}
|
||
|
||
private static RetailWindowHandle Mount(
|
||
UiRoot root,
|
||
string name,
|
||
IRetainedWindowStateController? state = null,
|
||
int authoredGeometryRevision = 0,
|
||
float width = 200f,
|
||
float height = 100f,
|
||
IRetainedPanelController? controller = null)
|
||
{
|
||
var frame = new UiPanel
|
||
{
|
||
Left = 10f,
|
||
Top = 20f,
|
||
Width = width,
|
||
Height = height,
|
||
MinWidth = 100f,
|
||
MinHeight = 80f,
|
||
MaxWidth = 600f,
|
||
MaxHeight = 400f,
|
||
Draggable = true,
|
||
Resizable = true,
|
||
};
|
||
root.AddChild(frame);
|
||
return root.RegisterWindow(
|
||
name,
|
||
frame,
|
||
controller: controller,
|
||
stateController: state,
|
||
authoredGeometryRevision: authoredGeometryRevision);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
||
}
|
||
|
||
private sealed class FakeWindowState : IRetainedWindowStateController
|
||
{
|
||
public RetainedWindowState Restored { get; private set; }
|
||
public int RestoreCount { get; private set; }
|
||
public RetainedWindowState CaptureWindowState() => Restored;
|
||
public void RestoreWindowState(RetainedWindowState state)
|
||
{
|
||
Restored = state;
|
||
RestoreCount++;
|
||
}
|
||
}
|
||
|
||
private sealed class FakePanelController : IRetainedPanelController
|
||
{
|
||
public int HiddenCount { get; private set; }
|
||
|
||
public void OnShown() { }
|
||
|
||
public void OnHidden() => HiddenCount++;
|
||
|
||
public void Dispose() { }
|
||
}
|
||
}
|