using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.UI; using AcDream.Plugin.Abstractions; using AcDream.UI.Abstractions.Panels.Settings; namespace AcDream.App.Tests.UI; public sealed class PluginSidePanelTests { [Fact] public void ManyPluginsWrapIntoReachableColumnsWithinTheLiveScreenHeight() { var root = new UiRoot { Width = 800f, Height = 260f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); for (int i = 0; i < 12; i++) { var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle handle = root.WindowManager.Register( $"plugin:test:{i}", frame); shelf.Add( new PluginUiOwner($"test.{i}", $"Plugin {i}"), new PluginPanelDescriptor("main", $"Plugin {i}"), handle); } root.Tick(0.016d, 16L); Assert.Equal(12, shelf.EntryCount); Assert.True(shelf.Width > 36f); Assert.True(shelf.Top + shelf.Height <= root.Height); Assert.All( shelf.Children, child => Assert.True(child.Top + child.Height <= shelf.Height)); Assert.Equal(root.Width - shelf.Width - 4f, shelf.Left); } [Fact] public void ShelfAndMinimizeButtonsHideAndRestoreWithoutUnregisteringWindow() { var root = new UiRoot { Width = 1280f, Height = 720f }; var frame = new UiPanel { Width = 320f, Height = 180f, Visible = true, }; root.AddChild(frame); RetailWindowHandle handle = root.WindowManager.Register( "plugin:acdream.test:main", frame); using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin") { IconText = "TP", }, handle); Assert.True(shelf.Visible); Assert.Equal(1, shelf.EntryCount); // Review fix round finding 1: the grip and collapse toggle are now // real sibling children too, so shelf.Children is no longer just the // one entry button — filter by the entry-button TYPE instead of // asserting a raw child count. PluginSidePanel.PluginShelfButton shelfButton = Assert.Single( shelf.Children.OfType()); shelfButton.OnEvent(new UiEvent { Type = UiEventType.Click }); Assert.False(handle.IsVisible); Assert.True(handle.IsRegistered); shelfButton.OnEvent(new UiEvent { Type = UiEventType.Click }); Assert.True(handle.IsVisible); UiSimpleButton minimize = Assert.IsAssignableFrom( Assert.Single(frame.Children)); minimize.OnEvent(new UiEvent { Type = UiEventType.Click }); Assert.False(handle.IsVisible); Assert.True(handle.IsRegistered); root.WindowManager.Unregister(handle.Name); Assert.Equal(0, shelf.EntryCount); Assert.False(shelf.Visible); } [Fact] public void FullWidthPluginWindowStartsAndStaysReachableAtMinimumCanvas() { var root = new UiRoot { Width = 800f, Height = 600f }; var frame = new UiPanel { Left = 28f, Top = 42f, Width = 800f, Height = 244f, Visible = true, }; root.AddChild(frame); RetailWindowHandle handle = root.WindowManager.Register( "plugin:acdream.mosstank:main", frame); using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); shelf.Add( new PluginUiOwner("acdream.mosstank", "MossTank"), new PluginPanelDescriptor("main", "MossTank"), handle); Assert.Equal(0f, handle.Left); Assert.Equal(42f, handle.Top); Assert.True(frame.ConstrainDragToParent); Assert.True(frame.ConstrainResizeToParent); frame.Left = 700f; frame.Top = 590f; root.Tick(0.016d, 16L); Assert.Equal(0f, handle.Left); Assert.Equal(356f, handle.Top); } // ── Slice A: movable, hideable plugin shelf ───────────────────────────── // docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md [Fact] public void DefaultDock_NoSavedLayout_SitsAtRightEdgeTopBar() { var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); Assert.Equal(800f - shelf.Width - 4f, shelf.Left); Assert.Equal(116f, shelf.Top); } [Fact] public void TopRightCorner_StaysFixed_WhenReflowChangesWidthWhileStillDocked() { // Forces a genuine multi-column re-wrap (not just an entry-count // change, which Reflow's own infinite-height default keeps single- // column — see the PluginSidePanel class doc) by shrinking the // available height between two ticks, then confirms the shelf's // RIGHT edge (not its Left) stayed put while docked. var root = new UiRoot { Width = 800f, Height = 260f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); for (int i = 0; i < 6; i++) { var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle handle = root.WindowManager.Register( $"plugin:test:{i}", frame); shelf.Add( new PluginUiOwner($"test.{i}", $"Plugin {i}"), new PluginPanelDescriptor("main", $"Plugin {i}"), handle); } root.Tick(0.016d, 16L); float rightEdge = shelf.Left + shelf.Width; float widthBefore = shelf.Width; root.Height = 150f; root.Tick(0.016d, 16L); Assert.NotEqual(widthBefore, shelf.Width); Assert.Equal(rightEdge, shelf.Left + shelf.Width, precision: 3); } [Fact] public void Drag_ViaGripBand_MovesShelf_AndTopLeftSurvivesTheNextReflow() { var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); // establishes the initial right-edge dock // Press inside the grip band (y < 12) but left of the collapse // toggle's rect (which starts at Width-16) so this is a plain // drag, not a toggle click. int pressX = (int)shelf.Left + 10; int pressY = (int)shelf.Top + 5; root.OnMouseDown(UiMouseButton.Left, pressX, pressY); root.OnMouseMove(pressX + 100, pressY + 100); root.OnMouseUp(UiMouseButton.Left, pressX + 100, pressY + 100); Assert.Equal(764f, shelf.Left); // clamped to parent (800 - 36) Assert.Equal(216f, shelf.Top); float leftAfterDrag = shelf.Left; float widthBeforeCollapse = shelf.Width; // Force a width change the same way the collapse toggle does, and // confirm the LEFT edge (not the right edge) is what survives now // that the shelf has been moved once. shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true)); Assert.NotEqual(widthBeforeCollapse, shelf.Width); Assert.Equal(leftAfterDrag, shelf.Left); } [Fact] public void Drag_StartingOnShelfPadding_DoesNotMoveTheShelf() { // NEW-1 (residual round, docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md // Slice A): pins the actual Draggable:true -> false change 4fada238e made. // A press on the shelf's own PADDING — left of the first entry button's // Left=4 (OuterPadding), below the grip band — must never promote // to a whole-window drag now that Draggable is false; only the grip's // WindowMoveHandle subtree can start a move (see the class doc). Proven // to fail against the PRE-4fada238e shelf: temporarily flipping the // ctor's `Draggable = false;` to `Draggable = true;` and re-running this // test moved the shelf by the full drag delta (100,100) instead of // leaving it in place, confirming this test actually exercises the // fixed behavior — see the commit message for the exact numbers observed. var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); // establishes the initial right-edge dock int pressX = (int)shelf.Left + 2; // left of button.Left (4) // Below the grip band: derived from the shelf's own EXPANDED band // height (owner-reported-defect fix, docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md) // rather than a re-hard-coded literal — with font: null this is // still the 12px bitmap-fallback band, so the numeric press point is // unchanged, but the intent now survives a font change. int pressY = (int)shelf.Top + (int)shelf.ExpandedGripBandHeight + 8; float leftBefore = shelf.Left; float topBefore = shelf.Top; root.OnMouseDown(UiMouseButton.Left, pressX, pressY); root.OnMouseMove(pressX + 100, pressY + 100); root.OnMouseUp(UiMouseButton.Left, pressX + 100, pressY + 100); Assert.Equal(leftBefore, shelf.Left); Assert.Equal(topBefore, shelf.Top); } [Fact] public void Drag_Refused_WhileUiLocked() { var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); float leftBefore = shelf.Left; float topBefore = shelf.Top; root.UiLocked = true; int pressX = (int)shelf.Left + 10; int pressY = (int)shelf.Top + 5; root.OnMouseDown(UiMouseButton.Left, pressX, pressY); root.OnMouseMove(pressX + 100, pressY + 100); root.OnMouseUp(UiMouseButton.Left, pressX + 100, pressY + 100); Assert.Equal(leftBefore, shelf.Left); Assert.Equal(topBefore, shelf.Top); } [Fact] public void Collapse_HidesButtonsAndShrinksWidth_RoundTripsThroughWindowState() { var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); float expandedWidth = shelf.Width; float leftBeforeCollapse = shelf.Left; PluginSidePanel.PluginShelfButton button = Assert.Single( shelf.Children.OfType()); Assert.True(button.Visible); // Click the collapse toggle at the grip's right end through the real // mouse pipeline (not the state controller directly), proving the // drawn toggle rect is genuinely clickable and does not promote to a // window drag. int toggleX = (int)shelf.Left + (int)shelf.Width - 8; int toggleY = (int)shelf.Top + 4; root.OnMouseDown(UiMouseButton.Left, toggleX, toggleY); root.OnMouseUp(UiMouseButton.Left, toggleX, toggleY); Assert.False(button.Visible); Assert.True(shelf.Width < expandedWidth); RetainedWindowState captured = shelf.CaptureWindowState(); Assert.True(captured.Collapsed); // Right edge preserved (still docked — this test never dragged it). Assert.Equal( leftBeforeCollapse + expandedWidth, shelf.Left + shelf.Width, precision: 3); shelf.RestoreWindowState(new RetainedWindowState(Collapsed: false)); Assert.True(button.Visible); Assert.Equal(expandedWidth, shelf.Width); Assert.False(shelf.CaptureWindowState().Collapsed); } [Fact] public void CollapseThenExpand_WhileStillDocked_ReturnsToTheIdenticalLeftAndTop() { // Owner-reported defect fix (docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md): // the collapsed tab's HEIGHT changed (button-sized, not the thin grip // band) — confirms that change alone does not disturb the docked // anchor math: collapsing shifts Left to preserve the right edge (a // width change), and expanding back must return to the EXACT same // Left/Top, not merely an equal Width. var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); // establishes the initial right-edge dock float leftBefore = shelf.Left; float topBefore = shelf.Top; shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true)); // Width shrank while still docked, so Left shifted right to keep the // right edge fixed; Top never moves on a collapse/expand. Assert.NotEqual(leftBefore, shelf.Left); Assert.Equal(topBefore, shelf.Top); shelf.RestoreWindowState(new RetainedWindowState(Collapsed: false)); Assert.Equal(leftBefore, shelf.Left, precision: 3); Assert.Equal(topBefore, shelf.Top, precision: 3); } [Fact] public void ToggleVisibility_ShowHideShow_AndStaysHiddenWhenANewWindowRegisters() { var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame1 = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame1); RetailWindowHandle handle1 = root.WindowManager.Register( "plugin:test:1", frame1); shelf.Add( new PluginUiOwner("test.1", "Plugin 1"), new PluginPanelDescriptor("main", "Plugin 1"), handle1); Assert.True(shelf.Visible); shelf.Hide(); Assert.False(shelf.Visible); shelf.Show(); Assert.True(shelf.Visible); shelf.Hide(); Assert.False(shelf.Visible); var frame2 = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame2); RetailWindowHandle handle2 = root.WindowManager.Register( "plugin:test:2", frame2); shelf.Add( new PluginUiOwner("test.2", "Plugin 2"), new PluginPanelDescriptor("main", "Plugin 2"), handle2); Assert.Equal(2, shelf.EntryCount); Assert.False(shelf.Visible); // a new window does not un-hide it } [Fact] public void Persistence_RoundTripsPositionVisibilityAndCollapsed() { string directory = Path.Combine( Path.GetTempPath(), "acdream-plugin-shelf-tests-" + Guid.NewGuid().ToString("N")); string path = Path.Combine(directory, "settings.json"); try { var store = new SettingsStore(path); var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); using var persistence = new RetailWindowLayoutPersistence( root.WindowManager, store, () => "Alice", () => (800, 600)); root.WindowManager.MoveTo(WindowNames.PluginShelf, 120f, 88f); shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true)); shelf.Hide(); UiWindowLayout saved = Assert.IsType( store.LoadWindowLayout("Alice", "800x600", WindowNames.PluginShelf, default)); Assert.Equal((120f, 88f), (saved.X, saved.Y)); Assert.False(saved.Visible); Assert.True(saved.Collapsed); // Restore onto a fresh shelf instance, mirroring a relog. var root2 = new UiRoot { Width = 800f, Height = 600f }; using var shelf2 = new PluginSidePanel( root2.WindowManager, _ => (0u, 0, 0), font: null); root2.AddChild(shelf2); root2.WindowManager.Register(WindowNames.PluginShelf, shelf2, shelf2, controller: shelf2); var frame2 = new UiPanel { Width = 200f, Height = 100f }; root2.AddChild(frame2); RetailWindowHandle pluginHandle2 = root2.WindowManager.Register( "plugin:acdream.test:main", frame2); shelf2.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle2); using var persistence2 = new RetailWindowLayoutPersistence( root2.WindowManager, store, () => "Alice", () => (800, 600)); persistence2.RestoreAll(); Assert.Equal((120f, 88f), (shelf2.Left, shelf2.Top)); Assert.False(shelf2.Visible); Assert.True(shelf2.CaptureWindowState().Collapsed); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true); } } // ── Review fix round (2026-09-06): grip/toggle real children, dialog // z-order, persisted collapse/hide intent ────────────────────────────── [Fact] public void Drag_StartingOnAnEntryButton_DoesNotMoveTheShelf() { // Finding 1 / finding 7a: a press that lands on a real entry button // (UiSimpleButton.HandlesClick => true) must be handled by the button, // never promoted to a whole-shelf drag. var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); PluginSidePanel.PluginShelfButton button = Assert.Single( shelf.Children.OfType()); Vector2 buttonScreen = button.ScreenPosition; int pressX = (int)buttonScreen.X + 5; int pressY = (int)buttonScreen.Y + 5; float leftBefore = shelf.Left; float topBefore = shelf.Top; root.OnMouseDown(UiMouseButton.Left, pressX, pressY); root.OnMouseMove(pressX + 100, pressY + 100); root.OnMouseUp(UiMouseButton.Left, pressX + 100, pressY + 100); Assert.Equal(leftBefore, shelf.Left); Assert.Equal(topBefore, shelf.Top); } [Fact] public void PerTickZOrderRaise_Removed_ADialogAddedAfterKeepsItsHigherZOrder() { // Finding 2: the OnTick block that used to bump the shelf's ZOrder // above every sibling, every frame, is gone. Registration's own // press-to-raise remains (a grip drag calls UiRoot.BringToFront before // the drag starts) but ticking alone must never re-assert dominance — // RetailDialogFactory.Tick re-raises open dialogs every frame the same // way, and the two must not fight. var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); int shelfZBefore = shelf.ZOrder; var dialog = new UiPanel { Width = 100f, Height = 60f, ZOrder = shelfZBefore + 50 }; root.AddChild(dialog); root.Tick(0.016d, 16L); root.Tick(0.016d, 16L); Assert.Equal(shelfZBefore, shelf.ZOrder); Assert.True(dialog.ZOrder > shelf.ZOrder); } [Fact] public void ToggleClick_SavesCollapsedOnly_WithNoSubsequentHideOrMove() { // Finding 3a. string directory = Path.Combine( Path.GetTempPath(), "acdream-plugin-shelf-tests-" + Guid.NewGuid().ToString("N")); string path = Path.Combine(directory, "settings.json"); try { var store = new SettingsStore(path); var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); using var persistence = new RetailWindowLayoutPersistence( root.WindowManager, store, () => "Alice", () => (800, 600), stateManagedVisibilityWindows: [WindowNames.PluginShelf]); float topBefore = shelf.Top; float rightEdgeBefore = shelf.Left + shelf.Width; int toggleX = (int)shelf.Left + (int)shelf.Width - 8; int toggleY = (int)shelf.Top + 4; root.OnMouseDown(UiMouseButton.Left, toggleX, toggleY); root.OnMouseUp(UiMouseButton.Left, toggleX, toggleY); UiWindowLayout saved = Assert.IsType( store.LoadWindowLayout("Alice", "800x600", WindowNames.PluginShelf, default)); Assert.True(saved.Collapsed); Assert.True(saved.Visible); // no Hide happened Assert.Equal(topBefore, saved.Y); // no vertical Move happened // Collapsing shrinks Width; the shelf is still DOCKED, so Reflow's // own anchor math shifts Left to keep the right edge fixed — that // is not a "Move" (it never goes through handle.MoveTo/NotifyMoved), // just the collapse's own geometry, captured in the same save. Assert.Equal(shelf.Left, saved.X); Assert.Equal(rightEdgeBefore, shelf.Left + shelf.Width, precision: 3); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true); } } [Fact] public void UnregisteringTheLastPlugin_DoesNotPersistTheAvailabilityHideAsAUserHide() { // Finding 3b. string directory = Path.Combine( Path.GetTempPath(), "acdream-plugin-shelf-tests-" + Guid.NewGuid().ToString("N")); string path = Path.Combine(directory, "settings.json"); try { var store = new SettingsStore(path); var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); using var persistence = new RetailWindowLayoutPersistence( root.WindowManager, store, () => "Alice", () => (800, 600), stateManagedVisibilityWindows: [WindowNames.PluginShelf]); // Establish a genuine "the user wants this visible" save. shelf.Show(); Assert.True(shelf.Visible); root.WindowManager.Unregister(pluginHandle.Name); Assert.Equal(0, shelf.EntryCount); Assert.False(shelf.Visible); // derived Visible DOES flip (no entries)... // ...but the persisted INTENT must still read visible=true: the // availability-hide is not subscribed for a state-managed window. UiWindowLayout saved = Assert.IsType( store.LoadWindowLayout("Alice", "800x600", WindowNames.PluginShelf, default)); Assert.True(saved.Visible); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true); } } [Fact] public void RestoreWindowState_HiddenAndCollapsedIntent_AppliesDirectly_ThenShowExpandsAndReveals() { // Finding 3c: restoring Collapsed=true/RequestedVisible=false onto a // fresh shelf that already has entries must hide AND collapse it — // without going through Show/Hide (RestoreWindowState sets the intent // and re-derives Visible through the same synchronous ApplyVisibility // path every other state change uses). Show() must then reveal AND // expand it. var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); Assert.True(shelf.Visible); shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true, RequestedVisible: false)); Assert.False(shelf.Visible); Assert.True(shelf.CaptureWindowState().Collapsed); shelf.Show(); Assert.True(shelf.Visible); Assert.False(shelf.CaptureWindowState().Collapsed); } [Fact] public void CollapseExpandViaTheToggle_DoesNotCollapseTheColumnWrapToOneColumn() { // Finding 5: every Reflow() call site OTHER than OnTick's row-wrap // used to default to an unbounded height, forcing a single-column // layout for one frame after Add/remove/collapse/expand/restore — // reusing _lastLayoutHeight keeps the column count OnTick already // established. 12 entries at 800x260 forces a real multi-column wrap. var root = new UiRoot { Width = 800f, Height = 260f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); for (int i = 0; i < 12; i++) { var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle handle = root.WindowManager.Register($"plugin:test:{i}", frame); shelf.Add( new PluginUiOwner($"test.{i}", $"Plugin {i}"), new PluginPanelDescriptor("main", $"Plugin {i}"), handle); } root.Tick(0.016d, 16L); // Collapse via the real toggle click. int toggleX = (int)shelf.Left + (int)shelf.Width - 8; int toggleY = (int)shelf.Top + 4; root.OnMouseDown(UiMouseButton.Left, toggleX, toggleY); root.OnMouseUp(UiMouseButton.Left, toggleX, toggleY); // Expand via the real toggle click (coordinates recomputed: the // shelf's collapsed Width moved the toggle). toggleX = (int)shelf.Left + (int)shelf.Width - 8; toggleY = (int)shelf.Top + 4; root.OnMouseDown(UiMouseButton.Left, toggleX, toggleY); root.OnMouseUp(UiMouseButton.Left, toggleX, toggleY); root.Tick(0.016d, 16L); Assert.True(shelf.Top + shelf.Height <= root.Height); Assert.All( shelf.Children.OfType(), button => Assert.True(button.Top + button.Height <= shelf.Height)); } [Fact] public void FreshShelf_OneTick_SavesTheDockedPositionImmediately() { // NEW-7 (residual round): the one-time dock used to write Left // directly, so the FIRST-run docked position went unsaved until some // later, unrelated event happened to fire a save. Routing the dock // through the retained-window handle's MoveTo (when registered) // means RetailWindowLayoutPersistence — subscribed to Moved — picks // it up on the very first tick. string directory = Path.Combine( Path.GetTempPath(), "acdream-plugin-shelf-tests-" + Guid.NewGuid().ToString("N")); string path = Path.Combine(directory, "settings.json"); try { var store = new SettingsStore(path); var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); using var persistence = new RetailWindowLayoutPersistence( root.WindowManager, store, () => "Alice", () => (800, 600)); root.Tick(0.016d, 16L); // the one-time dock happens inside this tick float expectedLeft = 800f - shelf.Width - 4f; Assert.Equal(expectedLeft, shelf.Left); Assert.Equal(116f, shelf.Top); UiWindowLayout saved = Assert.IsType( store.LoadWindowLayout("Alice", "800x600", WindowNames.PluginShelf, default)); Assert.Equal((expectedLeft, 116f), (saved.X, saved.Y)); // NEW-7 also asks: does routing the dock through MoveTo (which // synchronously re-enters OnHandleMoved, NEW-2) itself flip // _userPositioned? If it had, a later width change (collapse) // would preserve the LEFT edge instead of the RIGHT edge. float rightEdgeBefore = shelf.Left + shelf.Width; shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true)); Assert.Equal(rightEdgeBefore, shelf.Left + shelf.Width, precision: 3); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true); } } [Fact] public void ClampAllToScreen_AfterShrinkingTheRoot_DoesNotFlipAnchoring_ButARealDragStillDoes() { // NEW-2 (residual round): a screen shrink that forces // RetailWindowLayoutPersistence.ClampAllToScreen to re-clamp the // still-docked shelf must not be mistaken for a user drag, even // though ClampAllToScreen's generic clamp formula // (screen.Width - handle.Width) lands 4px (OuterPadding) off the // dock formula's own result (parent.Width - Width - OuterPadding) // for the SAME new size. string directory = Path.Combine( Path.GetTempPath(), "acdream-plugin-shelf-tests-" + Guid.NewGuid().ToString("N")); string path = Path.Combine(directory, "settings.json"); try { var store = new SettingsStore(path); var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); // dock at Left=760 (800-36-4) under an 800-wide parent using var persistence = new RetailWindowLayoutPersistence( root.WindowManager, store, () => "Alice", () => (760, 600)); // Shrink the parent so the shelf's docked position no longer // fits, then run the SAME reachability sweep // RetailUiRuntime.Draw's #390 edge detector runs on a live resize. root.Width = 760f; persistence.ClampAllToScreen(); // Clamped hard to the new edge (760-36=724) — 4px right of what // the dock formula itself would produce (760-36-4=720). Assert.Equal(724f, shelf.Left); float rightEdgeBefore = shelf.Left + shelf.Width; root.Tick(0.016d, 16L); // a later reflow — still docked, right edge holds Assert.Equal(rightEdgeBefore, shelf.Left + shelf.Width, precision: 3); shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true)); Assert.Equal(rightEdgeBefore, shelf.Left + shelf.Width, precision: 3); shelf.RestoreWindowState(new RetainedWindowState(Collapsed: false)); // A REAL drag away from the edge afterward must still flip anchoring. int pressX = (int)shelf.Left + 10; int pressY = (int)shelf.Top + 5; root.OnMouseDown(UiMouseButton.Left, pressX, pressY); root.OnMouseMove(pressX - 100, pressY); root.OnMouseUp(UiMouseButton.Left, pressX - 100, pressY); float leftAfterDrag = shelf.Left; Assert.NotEqual(724f, leftAfterDrag); shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true)); Assert.Equal(leftAfterDrag, shelf.Left); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true); } } [Fact] public void ZeroMovementGripReleaseDoesNotFlipAnchoring_ButARealDragDoes() { // Finding 6: RetailWindowHandle.Moved fires unconditionally on every // completed window-drag gesture — including a press+release on the // grip with NO intervening mouse move. (RetailWindowManager.MoveTo // itself short-circuits an already-equal position before ever firing // Moved, so the meaningful equivalent of "a MoveTo to the identical // dock position" is this zero-pixel drag-release, not a same-value // MoveTo call.) That must not be mistaken for a user drag; a REAL // drag still must. var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, _ => (0u, 0, 0), font: null); root.AddChild(shelf); root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle pluginHandle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin"), pluginHandle); root.Tick(0.016d, 16L); // establishes the initial right-edge dock int pressX = (int)shelf.Left + 10; int pressY = (int)shelf.Top + 5; root.OnMouseDown(UiMouseButton.Left, pressX, pressY); root.OnMouseUp(UiMouseButton.Left, pressX, pressY); // zero movement float rightEdgeBeforeCollapse = shelf.Left + shelf.Width; shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true)); // Still docked: the RIGHT edge survived the width change. Assert.Equal(rightEdgeBeforeCollapse, shelf.Left + shelf.Width, precision: 3); shelf.RestoreWindowState(new RetainedWindowState(Collapsed: false)); // Now a REAL drag via the grip. root.OnMouseDown(UiMouseButton.Left, pressX, pressY); root.OnMouseMove(pressX + 40, pressY + 40); root.OnMouseUp(UiMouseButton.Left, pressX + 40, pressY + 40); float leftAfterDrag = shelf.Left; shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true)); // User-positioned now: the LEFT edge survives instead. Assert.Equal(leftAfterDrag, shelf.Left); } [Fact] public void ShelfButton_NormalizesADecalBareIndexDescriptorId_BeforeResolving() { // Review fix round finding 14 (docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md // Slice B): the descriptor's bare Decal-style index must resolve // through PluginIcons.Normalize at the shelf button, same as every // markup did sink — 0x165 (Melee Defense's real installed-DAT index, // SampleData.cs:69) becomes 0x06000165. var resolvedIds = new List(); var root = new UiRoot { Width = 800f, Height = 600f }; using var shelf = new PluginSidePanel( root.WindowManager, id => { resolvedIds.Add(id); return (9u, 32, 32); }, font: null); root.AddChild(shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle handle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin") { IconSurfaceId = 0x165u, }, handle); PluginSidePanel.PluginShelfButton button = Assert.Single( shelf.Children.OfType()); button.DrawSelfAndChildren(TestUiRenderContext()); Assert.Contains(0x06000165u, resolvedIds); } /// /// Residual round finding N1: this fake's shape matters, not just its /// return value. Production wires to /// IMarkupIconResolver.ResolveDid (the same seam every other /// markup icon sink shares) — NEVER to /// RetailUiAssets.ResolveSprite (= ResolveChrome = /// TextureCache.GetOrUploadRenderSurface), which returns a /// non-zero 1x1 magenta placeholder handle for a missing id rather than /// (0, 0, 0). A bare _ => (0u, 0, 0) lambda proves the /// button's OWN fallback logic works, but says nothing about whether the /// resolver actually WIRED IN would ever produce that input — a /// resolver that instead mirrored ResolveChrome's magenta-on-miss /// contract would have passed a test built that way while still /// leaving the fallback dead in production. This type exists so the /// test reads as "given a resolver that behaves like /// RetailMarkupIconResolver.ResolveDid", matching /// RetailMarkupIconResolverInstalledDatTests.ShelfButton_BogusDescriptorId_OnTheRealResolver_FallsBackToInitials, /// which runs the same shape against the REAL resolver and a real DAT. /// private sealed class FakeResolveDidResolver { public (uint tex, int w, int h) ResolveDid(uint did) => (0u, 0, 0); } [Fact] public void ShelfButton_FallsBackToInitials_WhenTheResolveNeverYieldsATexture() { // Review fix round finding 10: a non-zero descriptor icon id whose // resolve never succeeds (a bad Decal index, a DAT id from a // different installation) must fall back to the initials text // rather than rendering a blank button forever. var root = new UiRoot { Width = 800f, Height = 600f }; var resolver = new FakeResolveDidResolver(); using var shelf = new PluginSidePanel( root.WindowManager, resolver.ResolveDid, font: null); root.AddChild(shelf); var frame = new UiPanel { Width = 200f, Height = 100f }; root.AddChild(frame); RetailWindowHandle handle = root.WindowManager.Register( "plugin:acdream.test:main", frame); shelf.Add( new PluginUiOwner("acdream.test", "Test Plugin"), new PluginPanelDescriptor("main", "Test Plugin") { IconText = "TP", IconSurfaceId = 0x165u, }, handle); PluginSidePanel.PluginShelfButton button = Assert.Single( shelf.Children.OfType()); // Before any draw, the button still shows empty text (a non-zero // icon id was supplied and has not been attempted yet). Assert.Equal(string.Empty, button.Text); button.DrawSelfAndChildren(TestUiRenderContext()); Assert.Equal("TP", button.Text); } private static UiRenderContext TestUiRenderContext() { var device = new RecordingGpuDevice(); var renderer = new TextRenderer( device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(200f, 200f)); return new UiRenderContext(renderer, new Vector2(200f, 200f)); } private sealed class NullGpuFrameSource : ICurrentGpuFrameSource { public IGpuFrame? CurrentFrame => null; } }