Owner report: "I dont see the < after I minimize the window" — after
collapsing the plugin shelf with the > toggle, the < (expand) glyph was not
visible.
Root cause, established with a real-DAT draw probe (font 0x40000000) before
changing anything: '<' and '>' share IDENTICAL glyph metrics (OffsetY=4,
Width=5, Height=7, VerticalOffsetBefore=5), so there is no per-glyph
asymmetry to explain "I see one but not the other." Against the pre-fix 12px
grip band, the toggle's FILL glyph plane measured fully INSIDE the band in
both states (local y=[3,10] of [0,12]) — the "16px line box overhangs a 12px
band" theory alone does not erase the glyph, so a bare clip fix would not
have addressed the report. What IS true: the border-inflated OUTLINE
(background/shadow) plane, drawn first per retail's UIElement_Text::DrawSelf,
spans y=[-1,14] before clipping and was cropped by the band's self-clip
(UiElement.ClipsChildren) to exactly [0,12] — a real but minor defect. The
actual explanation for the report is discoverability: the collapsed shelf
shrank to a bare 24x12 near-black sliver at the screen edge, several times
smaller than any other clickable affordance in the UI — easy to overlook even
though its pixels were, in fact, being drawn.
Fix (src/AcDream.App/UI/PluginSidePanel.cs):
- ExpandedGripBandHeight (new internal property) derives the EXPANDED
grip/toggle band from the real font metrics — max(12, font.LineHeight + 2)
— so neither the fill nor the border-inflated outline plane can clip for
any font; the 12px constant remains only as the bitmap-font fallback.
Threaded through the ctor's initial Height, OnTick's row-wrap height calc,
LayoutChrome, and Reflow's entry-Top/expanded-Height math.
- The COLLAPSED tab is now ButtonExtent (28px) tall instead of the 12px grip
band — the same size as an ordinary entry button — with the toggle glyph
filling and centering in the taller band. This is the actual fix for the
report: the collapsed affordance is now button-sized and findable, not a
bug-for-bug-identical-but-larger clip fix.
Tests:
- tests/AcDream.App.Tests/UI/PluginSidePanelToggleGlyphClipTests.cs (new,
Lane=InstalledDat): loads the real DAT font, builds a live shelf, and
proves via TextRenderer.DebugSpriteSegmentVerts that the toggle's ink (fill
+ outline) is fully contained in its own clip band in BOTH the expanded
('>') and real-click-collapsed ('<') states, plus that the collapsed tab is
button-sized. Verified failing against the pre-fix code (git stash of just
this file) with concrete numbers: computed unclipped span [-1,14] does not
fit inside the 12px band; collapsed height measured 12 (not button-sized).
Passes after the fix.
- tests/AcDream.App.Tests/UI/PluginSidePanelTests.cs: added
CollapseThenExpand_WhileStillDocked_ReturnsToTheIdenticalLeftAndTop (the
dock/anchor invariant survives the collapsed-height change), and derived
Drag_StartingOnShelfPadding_DoesNotMoveTheShelf's press-below-the-grip-band
Y coordinate from the new ExpandedGripBandHeight accessor instead of a
re-hard-coded literal.
Verification: dotnet build (App + tests) green. Filtered run
(PluginSidePanel|UiDatFont|Markup|UiRootInput) 131/131 passed, 0 skipped —
the InstalledDat lane tests actually ran (DAT dir resolved). Full
AcDream.App.Tests suite: 7334 passed / 97 skipped / 36 failed (baseline was
7331/97/36) — the 3 new tests are the only delta; the 36 failed test names
are byte-identical to the pre-existing set (cathedral collision installed-dat
gates, alpha-flush conformance, layout live-mount probes, Linux frame-pacing,
credential resolver — all unrelated to this change).
No retail-divergence register change needed: IA-27 already covers the
plugin shelf's non-retail collapse-toggle glyphs/behavior in general; this
is a bug fix within that already-declared deviation, not a new one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
974 lines
42 KiB
C#
974 lines
42 KiB
C#
using System.Numerics;
|
|
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<PluginSidePanel.PluginShelfButton>());
|
|
|
|
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<UiSimpleButton>(
|
|
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<PluginSidePanel.PluginShelfButton>());
|
|
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<UiWindowLayout>(
|
|
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<PluginSidePanel.PluginShelfButton>());
|
|
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<UiWindowLayout>(
|
|
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<UiWindowLayout>(
|
|
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<PluginSidePanel.PluginShelfButton>(),
|
|
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<UiWindowLayout>(
|
|
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);
|
|
}
|
|
}
|