Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions FancyWM.DllImports/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
SHGetFileInfo
PeekMessage
AllowSetForegroundWindow
ZeroMemory

Check warning on line 30 in FancyWM.DllImports/NativeMethods.txt

View workflow job for this annotation

GitHub Actions / build (Release)

Method or type "ZeroMemory" not found.
SendInput
SendMessage
SendMessageTimeout
CreateWindowEx
GetMessage
Expand All @@ -45,7 +46,7 @@
GetConsoleWindow
FreeConsole
SystemParametersInfo
GetWINDOWS_EX_STYLE

Check warning on line 49 in FancyWM.DllImports/NativeMethods.txt

View workflow job for this annotation

GitHub Actions / build (Release)

Method or type "GetWINDOWS_EX_STYLE" not found.

WINDOWS_EX_STYLE
WM_KEYDOWN
Expand All @@ -55,6 +56,7 @@
WM_QUIT
WM_TIMER
WM_COPYDATA
WM_NCHITTEST
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_RBUTTONDOWN
Expand All @@ -65,8 +67,8 @@
VK_MENU
INFINITE
CREATE_WAITABLE_TIMER_MANUAL_RESET
GWL_EXSTYLE

Check warning on line 70 in FancyWM.DllImports/NativeMethods.txt

View workflow job for this annotation

GitHub Actions / build (Release)

Use the name of the enum that declares this constant: GetWindowLongPtr_nIndex
WS_EX_TOPMOST

Check warning on line 71 in FancyWM.DllImports/NativeMethods.txt

View workflow job for this annotation

GitHub Actions / build (Release)

Use the name of the enum that declares this constant: WINDOWS_EX_STYLE

MSLLHOOKSTRUCT
KBDLLHOOKSTRUCT
Expand Down
293 changes: 287 additions & 6 deletions FancyWM.Tests/TilingWorkspaceTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;

using FancyWM;
using FancyWM.Layouts.Tiling;
using FancyWM.Models;
using FancyWM.Tests.TestUtilities;

using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand All @@ -17,6 +20,17 @@ public class TilingWorkspaceTest
private readonly WindowMockFactory m_windowFactory = new();
private readonly Rectangle m_workarea = new(0, 0, 1920, 1080);

/// <summary>
/// Point over <paramref name="target"/> in the left edge band (outside the center stack zone), so
/// <see cref="TilingWorkspace.ClassifyDropZone"/> yields <see cref="TilingWorkspace.DropZone.Left"/> and
/// insertion index matches the legacy flat <c>Move()</c> reorder tests.
/// </summary>
private static Point SiblingReorderPointOver(WindowNode target)
{
var r = target.ComputedRectangle;
return new Point(r.Left + (int)(r.Width * 0.28), r.Top + r.Height / 2);
}

[TestMethod]
public void TestAddRemoveDesktop()
{
Expand Down Expand Up @@ -318,7 +332,7 @@ public void TestMoveNodeFlat()
tree.Measure();
tree.Arrange();

workspace.MoveNode(node1, node2.ComputedRectangle.Center);
workspace.MoveNode(node1, SiblingReorderPointOver(node2));

Assert.AreEqual(node1.Parent.IndexOf(node1), 1);
Assert.AreEqual(node2.Parent.IndexOf(node2), 0);
Expand Down Expand Up @@ -362,25 +376,26 @@ void AssertPositions(params TilingNode[] nodes)

AssertPositions(node1, node2, node3, node4, node5);

workspace.MoveNode(node1, node2.ComputedRectangle.Center);
workspace.MoveNode(node1, SiblingReorderPointOver(node2));
tree.Measure();
tree.Arrange();

AssertPositions(node2, node1, node3, node4, node5);

workspace.MoveNode(node3, node2.ComputedRectangle.Center);
workspace.MoveNode(node3, SiblingReorderPointOver(node2));
tree.Measure();
tree.Arrange();

AssertPositions(node3, node2, node1, node4, node5);

workspace.MoveNode(node5, node3.ComputedRectangle.Center);
workspace.MoveNode(node5, SiblingReorderPointOver(node3));
tree.Measure();
tree.Arrange();

AssertPositions(node5, node3, node2, node1, node4);

workspace.MoveNode(node2, node1.ComputedRectangle.Center);
// Swap adjacent siblings (center hit would stack if allowNesting were true)
workspace.MoveNode(node2, node1.ComputedRectangle.Center, allowNesting: false);
tree.Measure();
tree.Arrange();

Expand Down Expand Up @@ -411,6 +426,272 @@ public void TestMoveWindowFlat()
Assert.AreEqual(node2.Parent.IndexOf(node2), 0);
}

[TestMethod]
public void TestRegisterWindowOverflowStackCreatesStackPanel()
{
var workspace = new TilingWorkspace();
var desktop = m_desktopFactory.CreateVirtualDesktop();
workspace.RegisterDesktop(desktop, m_workarea, PanelOrientation.Horizontal);

workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), 2, OverflowPlacementStrategy.Stack);
workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), 2, OverflowPlacementStrategy.Stack);
var overflowNode = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), 2, OverflowPlacementStrategy.Stack);

Assert.IsInstanceOfType(overflowNode.Parent, typeof(StackPanelNode));
}

[TestMethod]
public void TestRegisterWindowOverflowStackReusesExistingStack()
{
var workspace = new TilingWorkspace();
var desktop = m_desktopFactory.CreateVirtualDesktop();
workspace.RegisterDesktop(desktop, m_workarea, PanelOrientation.Horizontal);

workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), 2, OverflowPlacementStrategy.Stack);
workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), 2, OverflowPlacementStrategy.Stack);
workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), 2, OverflowPlacementStrategy.Stack);
workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), 2, OverflowPlacementStrategy.Stack);

var tree = workspace.GetTree(desktop)!;
var stacks = CollectStackPanelsUnderRoot(tree.Root!).ToList();
Assert.AreEqual(2, stacks.Count, "with max width 2, overflow stacking should use two stack columns when possible");
var counts = stacks.Select(s => s.Children.Count(c => c is WindowNode)).OrderBy(x => x).ToArray();
CollectionAssert.AreEqual(new[] { 2, 2 }, counts, "new windows should spread across stacks (least-loaded first)");
}

private static IEnumerable<StackPanelNode> CollectStackPanelsUnderRoot(PanelNode root)
{
foreach (var ch in root.Children)
{
foreach (var sp in EnumerateStackPanelsInSubtree(ch))
yield return sp;
}
}

private static IEnumerable<StackPanelNode> EnumerateStackPanelsInSubtree(TilingNode node)
{
if (node is StackPanelNode sp)
{
yield return sp;
yield break;
}

if (node is PanelNode p)
{
foreach (var c in p.Children)
{
foreach (var nested in EnumerateStackPanelsInSubtree(c))
yield return nested;
}
}
}

private static int CountSplitPanels(TilingNode node)
{
var count = node is SplitPanelNode ? 1 : 0;
if (node is PanelNode panel)
{
foreach (var child in panel.Children)
{
count += CountSplitPanels(child);
}
}

return count;
}

[TestMethod]
public void TestMoveNodeCenterCreatesStackDrop()
{
var workspace = new TilingWorkspace();
var desktop = m_desktopFactory.CreateVirtualDesktop();
workspace.RegisterDesktop(desktop, m_workarea, PanelOrientation.Horizontal);

var source = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var target = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var tree = workspace.GetTree(desktop)!;
tree.WorkArea = new WinMan.Rectangle(0, 0, 2000, 2000);
tree.Measure();
tree.Arrange();

workspace.MoveNode(source, target.ComputedRectangle.Center);

Assert.IsInstanceOfType(source.Parent, typeof(StackPanelNode));
Assert.AreSame(source.Parent, target.Parent);
}

[TestMethod]
public void TestMoveNodeCenterReordersWithinSameStack()
{
var workspace = new TilingWorkspace();
var desktop = m_desktopFactory.CreateVirtualDesktop();
workspace.RegisterDesktop(desktop, m_workarea, PanelOrientation.Horizontal);

var a = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var b = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var tree = workspace.GetTree(desktop)!;
tree.WorkArea = new WinMan.Rectangle(0, 0, 2000, 2000);
tree.Measure();
tree.Arrange();

workspace.MoveNode(a, b.ComputedRectangle.Center);

var c = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
tree.Measure();
tree.Arrange();

workspace.MoveNode(c, a.ComputedRectangle.Center);

Assert.IsInstanceOfType(a.Parent, typeof(StackPanelNode));
var stack = (StackPanelNode)a.Parent!;
Assert.AreEqual(3, stack.Children.Count(x => x is WindowNode));
Assert.IsInstanceOfType(b.Parent, typeof(StackPanelNode));
Assert.AreSame(stack, b.Parent);

workspace.MoveNode(b, a.ComputedRectangle.Center);

Assert.AreSame(stack, b.Parent);
Assert.AreEqual(3, stack.Children.Count(x => x is WindowNode));
}

[TestMethod]
public void TestClassifyDropZoneCornerIsNeutral()
{
var r = new Rectangle(0, 0, 1000, 800);
var corner = (int)(Math.Min(r.Width, r.Height) * TilingWorkspace.DropZoneCornerFraction);
var pt = new Point(r.Left + corner / 2, r.Top + corner / 2);
Assert.AreEqual(TilingWorkspace.DropZone.Neutral, TilingWorkspace.ClassifyDropZone(r, pt));
}

[TestMethod]
public void TestMoveNodeCrossParentNeutralCornerKeepsSameSplitParent()
{
var workspace = new TilingWorkspace();
var desktop = m_desktopFactory.CreateVirtualDesktop();
workspace.RegisterDesktop(desktop, m_workarea, PanelOrientation.Horizontal);
var w1 = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var w2 = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var tree = workspace.GetTree(desktop)!;
tree.WorkArea = new Rectangle(0, 0, 2000, 2000);
tree.Measure();
tree.Arrange();
var rootSplit = (SplitPanelNode)w1.Parent!;
Assert.AreSame(rootSplit, w2.Parent);
var r2 = w2.ComputedRectangle;
var corner = (int)(Math.Min(r2.Width, r2.Height) * TilingWorkspace.DropZoneCornerFraction);
var ptNeutral = new Point(r2.Left + corner / 2, r2.Top + corner / 2);
Assert.AreEqual(TilingWorkspace.DropZone.Neutral, TilingWorkspace.ClassifyDropZone(r2, ptNeutral));
workspace.MoveNode(w1, ptNeutral);
tree.Measure();
tree.Arrange();
Assert.AreSame(rootSplit, w1.Parent);
Assert.AreSame(rootSplit, w2.Parent);
}

[TestMethod]
public void TestMoveNodeFromOneStackToAnotherEdgeDropDoesNotThrow()
{
var workspace = new TilingWorkspace();
var desktop = m_desktopFactory.CreateVirtualDesktop();
workspace.RegisterDesktop(desktop, m_workarea, PanelOrientation.Horizontal);

var w1 = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var w2 = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var tree = workspace.GetTree(desktop)!;
tree.WorkArea = new WinMan.Rectangle(0, 0, 2000, 2000);
tree.Measure();
tree.Arrange();

workspace.MoveNode(w1, w2.ComputedRectangle.Center);
tree.Measure();
tree.Arrange();

Assert.IsInstanceOfType(w1.Parent, typeof(StackPanelNode));
var rootSplit = (SplitPanelNode)w1.Parent!.Parent!;
var w3 = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), rootSplit);
var w4 = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow(), rootSplit);
tree.Measure();
tree.Arrange();

workspace.MoveNode(w3, w4.ComputedRectangle.Center);
tree.Measure();
tree.Arrange();

var stack2 = (StackPanelNode)w3.Parent!;
Assert.IsInstanceOfType(w1.Parent, typeof(StackPanelNode));
Assert.AreSame(stack2, w4.Parent);
Assert.AreNotSame(w1.Parent, stack2);

// Edge of target (not center stack band): cross-parent path must not WrapInSplitPanel the stacked target.
workspace.MoveNode(w1, SiblingReorderPointOver(w4));
tree.Measure();
tree.Arrange();

Assert.AreSame(stack2, w1.Parent);
CollectionAssert.Contains(stack2.Children.ToList(), w1);
}

[TestMethod]
public void TestMoveNodeDropOnStackPrefersFocusedTabNotAdjacentSoloTile()
{
var workspace = new TilingWorkspace();
var desktop = m_desktopFactory.CreateVirtualDesktop();
workspace.RegisterDesktop(desktop, m_workarea, PanelOrientation.Horizontal);

var a = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var b = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var tree = workspace.GetTree(desktop)!;
tree.WorkArea = new WinMan.Rectangle(0, 0, 2000, 2000);
tree.Measure();
tree.Arrange();

workspace.MoveNode(a, b.ComputedRectangle.Center);

var solo = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var incoming = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
tree.Measure();
tree.Arrange();

workspace.SetFocus(b);
var pt = b.ComputedRectangle.Center;
workspace.MoveNode(incoming, pt);

Assert.IsInstanceOfType(incoming.Parent, typeof(StackPanelNode));
var stack = (StackPanelNode)incoming.Parent!;
Assert.AreSame(a.Parent, incoming.Parent);
Assert.AreEqual(3, stack.Children.Count(static c => c is WindowNode));
Assert.IsInstanceOfType(solo.Parent, typeof(SplitPanelNode));
}

[TestMethod]
public void TestMoveNodeSimpleTwoWindowSplitChainReusesAncestorInsteadOfNesting()
{
var workspace = new TilingWorkspace();
var desktop = m_desktopFactory.CreateVirtualDesktop();
workspace.RegisterDesktop(desktop, m_workarea, PanelOrientation.Horizontal);

var a = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var b = workspace.RegisterWindow(m_windowFactory.CreateExplorerWindow());
var tree = workspace.GetTree(desktop)!;
tree.WorkArea = new WinMan.Rectangle(0, 0, 2000, 2000);
tree.Measure();
tree.Arrange();

workspace.WrapInSplitPanel(a, vertical: true);
tree.Measure();
tree.Arrange();

var rootSplit = (SplitPanelNode)tree.Root!;
var splitCountBefore = CountSplitPanels(rootSplit);

workspace.MoveNode(b, SiblingReorderPointOver(a));
tree.Measure();
tree.Arrange();

var splitCountAfter = CountSplitPanels(rootSplit);
Assert.AreEqual(splitCountBefore, splitCountAfter, "simple two-window split chains should not gain extra nested splits");
}

[TestMethod]
public void TestMoveAfterSameParent()
{
Expand Down
Loading
Loading