From 6ccaa472fbd1adaf2ea2ab4a6d029a91a0e3c6b9 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 27 Oct 2024 14:26:55 +1100 Subject: [PATCH] Restore ProxyFloatingLayoutEngine functionality (#1058) The following `FloatingLayoutPlugin` functionality has been restored: - Mark a window as floating - Mark a window as docked - Move non-floating window across monitors while the `FloatingLayoutPlugin` is enabled - Move floating window across monitors while the `FloatingLayoutPlugin` is enabled - Move formerly floating windows across monitors while the `FloatingLayoutPlugin` is enabled During the course of the rewrite to facilitate the above features, windows now float across workspaces. This actually significantly simplified the implementation as it meant that side-effects within the `ProxyFloatingLayoutEngine` could be removed. Additionally, the `GapsLayoutEngine` lost the ability to prevent shifting for floating windows. This has been fixed, and new edge cases have been handled by using multiple iterators. --- .../BaseProxyFloatingLayoutEngineTests.cs | 2 +- .../FloatingLayoutEngineTests.cs | 2 +- .../FloatingUtilsTests.cs | 122 ++ .../FloatingWindowPluginTests.cs | 443 ++--- .../GlobalSuppressions.cs | 1 + .../ProxyFloatingLayoutEngineTests.cs | 1660 +++++++---------- src/Whim.FloatingWindow/FloatingUtils.cs | 10 +- .../FloatingWindowPlugin.cs | 106 +- .../IFloatingWindowPlugin.cs | 14 +- .../IInternalFloatingWindowPlugin.cs | 20 - .../ProxyFloatingLayoutEngine.cs | 202 +- src/Whim.Gaps.Tests/GapsLayoutEngineTests.cs | 116 +- src/Whim.Gaps/GapsLayoutEngine.cs | 30 +- src/Whim.TestUtils/Whim.TestUtils.csproj | 3 +- .../WorkspaceSector/WorkspacePickersTests.cs | 32 + ...amlLoader_LoadFloatingWindowPluginTests.cs | 60 - src/Whim.Yaml/YamlPluginLoader.cs | 12 +- src/Whim/Commands/CoreCommands.cs | 2 +- .../Transforms/MoveWindowToPointTransform.cs | 6 +- .../FocusWindowInDirectionTransform.cs | 2 +- .../Transforms/MinimizeWindowEndTransform.cs | 6 +- .../MinimizeWindowStartTransform.cs | 2 +- ...indowEdgesInDirectionWorkspaceTransform.cs | 2 +- .../MoveWindowToPointInWorkspaceTransform.cs | 2 +- .../SwapWindowInDirectionTransform.cs | 2 +- .../Store/WorkspaceSector/WorkspacePickers.cs | 28 +- .../Store/WorkspaceSector/WorkspaceSector.cs | 2 +- .../Store/WorkspaceSector/WorkspaceUtils.cs | 19 +- 28 files changed, 1374 insertions(+), 1534 deletions(-) create mode 100644 src/Whim.FloatingWindow.Tests/FloatingUtilsTests.cs delete mode 100644 src/Whim.FloatingWindow/IInternalFloatingWindowPlugin.cs diff --git a/src/Whim.FloatingWindow.Tests/BaseProxyFloatingLayoutEngineTests.cs b/src/Whim.FloatingWindow.Tests/BaseProxyFloatingLayoutEngineTests.cs index 4f104489c..721d29c7b 100644 --- a/src/Whim.FloatingWindow.Tests/BaseProxyFloatingLayoutEngineTests.cs +++ b/src/Whim.FloatingWindow.Tests/BaseProxyFloatingLayoutEngineTests.cs @@ -11,7 +11,7 @@ public class BaseProxyFloatingLayoutEngineTests : ProxyLayoutEngineBaseTests { IContext context = Substitute.For(); IMonitor monitor = Substitute.For(); - IInternalFloatingWindowPlugin plugin = Substitute.For(); + IFloatingWindowPlugin plugin = Substitute.For(); ILayoutEngine innerLayoutEngine = Substitute.For(); context diff --git a/src/Whim.FloatingWindow.Tests/FloatingLayoutEngineTests.cs b/src/Whim.FloatingWindow.Tests/FloatingLayoutEngineTests.cs index 615893eac..fed03b1d4 100644 --- a/src/Whim.FloatingWindow.Tests/FloatingLayoutEngineTests.cs +++ b/src/Whim.FloatingWindow.Tests/FloatingLayoutEngineTests.cs @@ -221,7 +221,7 @@ public void MoveWindowEdgesInDirection(IContext context, IWindow window) IRectangle rect = new Rectangle(); // When - ILayoutEngine newEngine = engine.MoveWindowToPoint(window, rect); + ILayoutEngine newEngine = engine.MoveWindowEdgesInDirection(Direction.Up, rect, window); // Then Assert.Equal(engine, newEngine); diff --git a/src/Whim.FloatingWindow.Tests/FloatingUtilsTests.cs b/src/Whim.FloatingWindow.Tests/FloatingUtilsTests.cs new file mode 100644 index 000000000..7928aa737 --- /dev/null +++ b/src/Whim.FloatingWindow.Tests/FloatingUtilsTests.cs @@ -0,0 +1,122 @@ +using System.Collections.Immutable; +using NSubstitute; +using Whim.TestUtils; +using Windows.Win32.Foundation; +using Xunit; + +namespace Whim.FloatingWindow.Tests; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] +public class FloatingUtilsTests +{ + [Theory, AutoSubstituteData] + public void UpdateWindowRectangle_NoRectangle(IContext ctx) + { + // Given a window with no rectangle + IWindow window = StoreTestUtils.CreateWindow((HWND)1); + ctx.NativeManager.DwmGetWindowRectangle(window.Handle).Returns((IRectangle?)null); + + // When we update the rectangle + var result = FloatingUtils.UpdateWindowRectangle( + ctx, + ImmutableDictionary>.Empty, + window + ); + + // Then the result should be null + Assert.Null(result); + } + + [Theory, AutoSubstituteData] + public void UpdateWindowRectangle_NoMonitorForWindow(IContext ctx) + { + // Given a window with a rectangle, but no monitor + IWindow window = StoreTestUtils.CreateWindow((HWND)1); + ctx.NativeManager.DwmGetWindowRectangle(window.Handle).Returns(new Rectangle()); + + // When we update the rectangle + var result = FloatingUtils.UpdateWindowRectangle( + ctx, + ImmutableDictionary>.Empty, + window + ); + + // Then the result should be null + Assert.Null(result); + } + + [Theory, AutoSubstituteData] + internal void UpdateWindowRectangle_NoOldRectangle(IContext ctx, MutableRootSector root) + { + // Given a window with a rectangle and a monitor, but no old rectangle + IWindow window = StoreTestUtils.CreateWindow((HWND)1); + IMonitor monitor = StoreTestUtils.CreateMonitor(); + Workspace workspace = StoreTestUtils.CreateWorkspace(ctx); + + StoreTestUtils.PopulateThreeWayMap(ctx, root, monitor, workspace, window); + + ImmutableDictionary> dict = new Dictionary>() + { + { StoreTestUtils.CreateWindow((HWND)123), new Rectangle() }, + }.ToImmutableDictionary(); + + // When we update the rectangle + var result = FloatingUtils.UpdateWindowRectangle(ctx, dict, window); + + // Then the result should be the new dictionary + Assert.NotEqual(dict, result); + Assert.Equal(2, result!.Count); + Assert.Contains(window, result.Keys); + } + + [Theory, AutoSubstituteData] + internal void UpdateWindowRectangle_NoChange(IContext ctx, MutableRootSector root) + { + // Given a window with a rectangle and a monitor, but no change + IWindow window = StoreTestUtils.CreateWindow((HWND)1); + IMonitor monitor = StoreTestUtils.CreateMonitor(); + Workspace workspace = StoreTestUtils.CreateWorkspace(ctx); + + StoreTestUtils.PopulateThreeWayMap(ctx, root, monitor, workspace, window); + + ImmutableDictionary> dict = new Dictionary>() + { + { window, monitor.WorkingArea.NormalizeRectangle(new Rectangle()) }, + }.ToImmutableDictionary(); + + // When we update the rectangle + var result = FloatingUtils.UpdateWindowRectangle(ctx, dict, window); + + // Then the result should be the same dictionary + Assert.Same(dict, result); + } + + [Theory, AutoSubstituteData] + internal void UpdateWindowRectangle_Change(IContext ctx, MutableRootSector root) + { + // Given a window with a rectangle and a monitor, and a change + IWindow window = StoreTestUtils.CreateWindow((HWND)1); + IMonitor monitor = StoreTestUtils.CreateMonitor(); + monitor.WorkingArea.Returns(new Rectangle(0, 0, 1920, 1080)); + + Workspace workspace = StoreTestUtils.CreateWorkspace(ctx); + + StoreTestUtils.PopulateThreeWayMap(ctx, root, monitor, workspace, window); + + ImmutableDictionary> dict = new Dictionary>() + { + { window, monitor.WorkingArea.NormalizeRectangle(new Rectangle(10, 10, 1920, 1080)) }, + }.ToImmutableDictionary(); + + ctx.NativeManager.DwmGetWindowRectangle(window.Handle).Returns(new Rectangle(0, 0, 192, 108)); + + // When we update the rectangle + var result = FloatingUtils.UpdateWindowRectangle(ctx, dict, window); + + // Then the result should be the new dictionary + Assert.NotEqual(dict, result); + Assert.Single(result!); + Assert.Contains(window, result!.Keys); + Assert.Equal(new Rectangle(0, 0, 0.1, 0.1), result[window]); + } +} diff --git a/src/Whim.FloatingWindow.Tests/FloatingWindowPluginTests.cs b/src/Whim.FloatingWindow.Tests/FloatingWindowPluginTests.cs index d88404fe2..addf79ece 100644 --- a/src/Whim.FloatingWindow.Tests/FloatingWindowPluginTests.cs +++ b/src/Whim.FloatingWindow.Tests/FloatingWindowPluginTests.cs @@ -1,64 +1,17 @@ +using System.Collections.Immutable; using System.Text.Json; -using AutoFixture; -using NSubstitute; using Whim.TestUtils; +using Windows.Win32.Foundation; using Xunit; namespace Whim.FloatingWindow.Tests; -public class FloatingWindowPluginCustomization : ICustomization -{ - public void Customize(IFixture fixture) - { - IContext context = fixture.Freeze(); - IWorkspace workspace = fixture.Freeze(); - - IMonitor monitor = fixture.Freeze(); - monitor.WorkingArea.Returns( - new Rectangle() - { - X = 0, - Y = 0, - Width = 3, - Height = 3, - } - ); - - // The workspace will have a null last focused window - workspace.LastFocusedWindow.Returns((IWindow?)null); - - // The workspace manager should have a single workspace - context.WorkspaceManager.GetEnumerator().Returns((_) => new List() { workspace }.GetEnumerator()); - context.WorkspaceManager.ActiveWorkspace.Returns(workspace); - - // Monitor manager will return the monitor at the point (1, 2). - context.MonitorManager.GetMonitorAtPoint(Arg.Is>(p => p.X == 1 && p.Y == 2)).Returns(monitor); - - fixture.Inject(context); - fixture.Inject(workspace); - fixture.Inject(monitor); - } -} - +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] public class FloatingWindowPluginTests { private static FloatingWindowPlugin CreateSut(IContext context) => new(context); - private static void AssertFloatingWindowsEqual( - IReadOnlyDictionary> expected, - IReadOnlyDictionary> actual - ) - { - Assert.Equal(expected.Count, actual.Count); - - foreach (KeyValuePair> expectedWindow in expected) - { - Assert.Contains(expectedWindow.Key, actual.Keys); - Assert.Equal(expectedWindow.Value, actual[expectedWindow.Key]); - } - } - - [Theory, AutoSubstituteData] + [Theory, AutoSubstituteData] public void Name(IContext context) { // Given @@ -71,7 +24,7 @@ public void Name(IContext context) Assert.Equal("whim.floating_window", name); } - [Theory, AutoSubstituteData] + [Theory, AutoSubstituteData] public void PluginCommands(IContext context) { // Given @@ -84,8 +37,8 @@ public void PluginCommands(IContext context) Assert.NotEmpty(commands.Commands); } - [Theory, AutoSubstituteData] - public void PreInitialize(IContext context) + [Theory, AutoSubstituteData] + public void PreInitialize(IContext context, List transforms, ILayoutEngine layoutEngine) { // Given FloatingWindowPlugin plugin = CreateSut(context); @@ -94,11 +47,14 @@ public void PreInitialize(IContext context) plugin.PreInitialize(); // Then - context.WorkspaceManager.Received(1).AddProxyLayoutEngine(Arg.Any()); + Assert.IsType(transforms[0]); + ProxyLayoutEngineCreator creator = ((AddProxyLayoutEngineTransform)transforms[0]).ProxyLayoutEngineCreator; + ILayoutEngine result = creator(layoutEngine); + Assert.IsType(result); } - [Theory, AutoSubstituteData] - public void PostInitialize(IContext context) + [Theory, AutoSubstituteData] + public void PostInitialize(IContext context, List transforms) { // Given FloatingWindowPlugin plugin = CreateSut(context); @@ -107,82 +63,66 @@ public void PostInitialize(IContext context) plugin.PostInitialize(); // Then - context.WorkspaceManager.DidNotReceive().AddProxyLayoutEngine(Arg.Any()); + Assert.Empty(transforms); } - [Theory, AutoSubstituteData] - public void WindowManager_WindowRemoved( - IContext context, - IWindow window, - IMonitor monitor, - IWorkspace activeWorkspace - ) + [Theory, AutoSubstituteData] + internal void WindowManager_WindowRemoved(IContext ctx, MutableRootSector root, IWindow window) { // Given - Rectangle rect = new(); - WindowState windowState = - new() - { - Window = window, - Rectangle = rect, - WindowSize = WindowSize.Normal, - }; + Workspace activeWorkspace = StoreTestUtils.CreateWorkspace(ctx); + StoreTestUtils.PopulateWindowWorkspaceMap(ctx, root, window, activeWorkspace); - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns(activeWorkspace); - activeWorkspace.TryGetWindowState(window).Returns(windowState); - context.MonitorManager.GetMonitorAtPoint(rect).Returns(monitor); - - FloatingWindowPlugin plugin = CreateSut(context); + FloatingWindowPlugin plugin = CreateSut(ctx); // When plugin.PreInitialize(); plugin.MarkWindowAsFloating(window); - context.WindowManager.WindowRemoved += Raise.EventWith(new WindowRemovedEventArgs() { Window = window }); + Assert.Single(plugin.FloatingWindows); + + root.WindowSector.QueueEvent(new WindowRemovedEventArgs() { Window = window }); + root.WindowSector.DispatchEvents(); // Then nothing Assert.Empty(plugin.FloatingWindows); } - #region MarkWindowAsFloating - [Theory, AutoSubstituteData] - public void MarkWindowAsFloating_NoWindow(IContext context) + [Theory, AutoSubstituteData] + public void SaveState(IContext context) { // Given FloatingWindowPlugin plugin = CreateSut(context); // When - plugin.MarkWindowAsFloating(null); + JsonElement? json = plugin.SaveState(); // Then - Assert.Empty(plugin.FloatingWindows); + Assert.Null(json); } - [Theory, AutoSubstituteData] - public void MarkWindowAsFloating_NoWorkspaceForWindow(IContext context, IWindow window) + [Theory, AutoSubstituteData] + public void LoadState(IContext context) { // Given FloatingWindowPlugin plugin = CreateSut(context); - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns((IWorkspace?)null); // When - plugin.MarkWindowAsFloating(window); + plugin.LoadState(JsonDocument.Parse("{}").RootElement); - // Then + // Then nothing Assert.Empty(plugin.FloatingWindows); } +} - [Theory, AutoSubstituteData] - public void MarkWindowAsFloating_NoWorkspaceForWindow_LastFocusedWindow( - IContext context, - IWindow window, - IWorkspace activeWorkspace - ) +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] +public class FloatingWindowPlugin_MarkWindowAsFloatingTests +{ + [Theory, AutoSubstituteData] + internal void MarkWindowAsFloating_NoWindow(IContext context, MutableRootSector root) { // Given - FloatingWindowPlugin plugin = CreateSut(context); - - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns((IWorkspace?)null); - activeWorkspace.LastFocusedWindow.Returns(window); + FloatingWindowPlugin plugin = new(context); + StoreTestUtils.AddActiveWorkspace(context, root, StoreTestUtils.CreateWorkspace(context)); // When plugin.MarkWindowAsFloating(); @@ -191,14 +131,11 @@ IWorkspace activeWorkspace Assert.Empty(plugin.FloatingWindows); } - [Theory, AutoSubstituteData] - public void MarkWindowAsFloating_NoWindowState(IContext context, IWindow window, IWorkspace activeWorkspace) + [Theory, AutoSubstituteData] + public void MarkWindowAsFloating_NoWindowPosition(IContext context, IWindow window) { // Given - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns(activeWorkspace); - activeWorkspace.TryGetWindowState(window).Returns((IWindowState?)null); - - FloatingWindowPlugin plugin = CreateSut(context); + FloatingWindowPlugin plugin = new(context); // When plugin.MarkWindowAsFloating(window); @@ -207,279 +144,169 @@ public void MarkWindowAsFloating_NoWindowState(IContext context, IWindow window, Assert.Empty(plugin.FloatingWindows); } - [Theory, AutoSubstituteData] - public void MarkWindowAsFloating(IContext context, IWindow window, IWorkspace activeWorkspace) + [Theory, AutoSubstituteData] + internal void MarkWindowAsFloating( + IContext context, + MutableRootSector root, + IWindow window, + List transforms + ) { // Given - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns(activeWorkspace); - activeWorkspace - .TryGetWindowState(window) - .Returns( - new WindowState() - { - Rectangle = new Rectangle() { X = 1, Y = 2 }, - Window = window, - WindowSize = WindowSize.Normal, - } - ); - - FloatingWindowPlugin plugin = CreateSut(context); + FloatingWindowPlugin plugin = new(context); + Workspace activeWorkspace = StoreTestUtils.CreateWorkspace(context) with + { + WindowPositions = new Dictionary() + { + { window.Handle, new WindowPosition(WindowSize.Normal, new Rectangle(0, 0, 100, 100)) }, + }.ToImmutableDictionary(), + }; + StoreTestUtils.PopulateWindowWorkspaceMap(context, root, window, activeWorkspace); // When plugin.MarkWindowAsFloating(window); // Then Assert.Single(plugin.FloatingWindows); - Assert.Equal(window, plugin.FloatingWindows.Keys.First()); - activeWorkspace.Received(1).MoveWindowToPoint(window, Arg.Any>()); + Assert.Contains( + transforms, + t => t.Equals(new MoveWindowToPointTransform(window.Handle, new Rectangle(0, 0, 100, 100))) + ); } - #endregion +} - #region MarkWindowAsDocked - [Theory, AutoSubstituteData] - public void MarkWindowAsDocked_WindowIsNotFloating(IContext context, IWindow window, IWorkspace activeWorkspace) +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] +public class FloatingWindowPlugin_MarkWindowAsDockedTests +{ + [Theory, AutoSubstituteData] + internal void MarkWindowAsDocked_NoWindow(IContext context, MutableRootSector root) { // Given - FloatingWindowPlugin plugin = CreateSut(context); + FloatingWindowPlugin plugin = new(context); + StoreTestUtils.AddActiveWorkspace(context, root, StoreTestUtils.CreateWorkspace(context)); // When - plugin.MarkWindowAsDocked(window); + plugin.MarkWindowAsDocked(); // Then Assert.Empty(plugin.FloatingWindows); - activeWorkspace.DidNotReceive().MoveWindowToPoint(window, Arg.Any>()); - activeWorkspace.DidNotReceive().DoLayout(); } - [Theory, AutoSubstituteData] - public void MarkWindowAsDocked_WindowIsFloating(IContext context, IWindow window, IWorkspace activeWorkspace) + [Theory, AutoSubstituteData] + internal void MarkWindowAsDocked_NoWindowPosition(IContext context, IWindow window) { // Given - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns(activeWorkspace); - activeWorkspace - .TryGetWindowState(window) - .Returns( - new WindowState() - { - Rectangle = new Rectangle() { X = 1, Y = 2 }, - Window = window, - WindowSize = WindowSize.Normal, - } - ); - - FloatingWindowPlugin plugin = CreateSut(context); - plugin.MarkWindowAsFloating(window); + FloatingWindowPlugin plugin = new(context); // When plugin.MarkWindowAsDocked(window); // Then Assert.Empty(plugin.FloatingWindows); - activeWorkspace.Received(2).MoveWindowToPoint(window, Arg.Any>()); - } - #endregion - - #region ToggleWindowFloating - [Theory, AutoSubstituteData] - public void ToggleWindowFloating_NoWindow(IContext context) - { - // Given - FloatingWindowPlugin plugin = CreateSut(context); - - // When - plugin.ToggleWindowFloating(null); - - // Then - Assert.Empty(plugin.FloatingWindows); - } - - [Theory, AutoSubstituteData] - public void ToggleWindowFloating_ToFloating(IContext context, IWindow window, IWorkspace activeWorkspace) - { - // Given - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns(activeWorkspace); - activeWorkspace - .TryGetWindowState(window) - .Returns( - new WindowState() - { - Rectangle = new Rectangle() { X = 1, Y = 2 }, - Window = window, - WindowSize = WindowSize.Normal, - } - ); - - FloatingWindowPlugin plugin = CreateSut(context); - - // When - plugin.ToggleWindowFloating(window); - - // Then - Assert.Single(plugin.FloatingWindows); - Assert.Equal(window, plugin.FloatingWindows.Keys.First()); } - [Theory, AutoSubstituteData] - public void ToggleWindowFloating_ToDocked(IContext context, IWindow window, IWorkspace activeWorkspace) + [Theory, AutoSubstituteData] + internal void MarkWindowAsDocked(IContext context, MutableRootSector root, IWindow window, List transforms) { // Given - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns(activeWorkspace); - activeWorkspace - .TryGetWindowState(window) - .Returns( - new WindowState() - { - Rectangle = new Rectangle() { X = 1, Y = 2 }, - Window = window, - WindowSize = WindowSize.Normal, - } - ); - - FloatingWindowPlugin plugin = CreateSut(context); - + FloatingWindowPlugin plugin = new(context); + Workspace activeWorkspace = StoreTestUtils.CreateWorkspace(context) with + { + WindowPositions = new Dictionary() + { + { window.Handle, new WindowPosition(WindowSize.Normal, new Rectangle(0, 0, 100, 100)) }, + }.ToImmutableDictionary(), + }; + StoreTestUtils.PopulateWindowWorkspaceMap(context, root, window, activeWorkspace); plugin.MarkWindowAsFloating(window); // When - plugin.ToggleWindowFloating(window); + plugin.MarkWindowAsDocked(window); // Then Assert.Empty(plugin.FloatingWindows); + Assert.Contains( + transforms, + t => t.Equals(new MoveWindowToPointTransform(window.Handle, new Rectangle(0, 0, 100, 100))) + ); } - #endregion +} - [Theory, AutoSubstituteData] - public void SaveState(IContext context) +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] +public class FloatingWindowPlugin_ToggleWindowFloatingTests +{ + [Theory, AutoSubstituteData] + internal void ToggleWindowFloating_NoWindow(IContext context, MutableRootSector root) { // Given - FloatingWindowPlugin plugin = CreateSut(context); + FloatingWindowPlugin plugin = new(context); + StoreTestUtils.AddActiveWorkspace(context, root, StoreTestUtils.CreateWorkspace(context)); // When - JsonElement? json = plugin.SaveState(); + plugin.ToggleWindowFloating(); // Then - Assert.Null(json); - } - - [Theory, AutoSubstituteData] - public void LoadState(IContext context) - { - // Given - FloatingWindowPlugin plugin = CreateSut(context); - - // When - plugin.LoadState(JsonDocument.Parse("{}").RootElement); - - // Then nothing Assert.Empty(plugin.FloatingWindows); } - #region MarkWindowAsDockedInLayoutEngine - [Theory, AutoSubstituteData] - public void MarkWindowAsDockedInLayoutEngine_WindowIsNotFloating( + [Theory, AutoSubstituteData] + internal void ToggleWindowFloating_FloatWindow( IContext context, + MutableRootSector root, IWindow window, - IWorkspace activeWorkspace + List transforms ) { // Given - ILayoutEngine layoutEngine = activeWorkspace.ActiveLayoutEngine; - FloatingWindowPlugin plugin = CreateSut(context); - - Assert.Empty(plugin.FloatingWindows); + FloatingWindowPlugin plugin = new(context); + Workspace activeWorkspace = StoreTestUtils.CreateWorkspace(context) with + { + WindowPositions = new Dictionary() + { + { window.Handle, new WindowPosition(WindowSize.Normal, new Rectangle(0, 0, 100, 100)) }, + }.ToImmutableDictionary(), + }; + StoreTestUtils.PopulateWindowWorkspaceMap(context, root, window, activeWorkspace); // When - plugin.MarkWindowAsDockedInLayoutEngine(window, layoutEngine.Identity); + plugin.ToggleWindowFloating(window); // Then - Assert.Empty(plugin.FloatingWindows); + Assert.Single(plugin.FloatingWindows); + Assert.Contains( + transforms, + t => t.Equals(new MoveWindowToPointTransform(window.Handle, new Rectangle(0, 0, 100, 100))) + ); } - [Theory, AutoSubstituteData] - public void MarkWindowAsDockedInLayoutEngine_WindowIsFloating( + [Theory, AutoSubstituteData] + internal void ToggleWindowFloating_DockWindow( IContext context, + MutableRootSector root, IWindow window, - IWorkspace activeWorkspace + List transforms ) { // Given - ILayoutEngine layoutEngine = activeWorkspace.ActiveLayoutEngine; - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns(activeWorkspace); - activeWorkspace - .TryGetWindowState(window) - .Returns( - new WindowState() - { - Rectangle = new Rectangle() { X = 1, Y = 2 }, - Window = window, - WindowSize = WindowSize.Normal, - } - ); - - FloatingWindowPlugin plugin = CreateSut(context); - plugin.MarkWindowAsFloating(window); - - AssertFloatingWindowsEqual( - new Dictionary>() + FloatingWindowPlugin plugin = new(context); + Workspace activeWorkspace = StoreTestUtils.CreateWorkspace(context) with + { + WindowPositions = new Dictionary() { - { - window, - new HashSet() { layoutEngine.Identity } - }, - }, - plugin.FloatingWindows - ); + { window.Handle, new WindowPosition(WindowSize.Normal, new Rectangle(0, 0, 100, 100)) }, + }.ToImmutableDictionary(), + }; + StoreTestUtils.PopulateWindowWorkspaceMap(context, root, window, activeWorkspace); + plugin.MarkWindowAsFloating(window); // When - plugin.MarkWindowAsDockedInLayoutEngine(window, layoutEngine.Identity); + plugin.ToggleWindowFloating(window); // Then Assert.Empty(plugin.FloatingWindows); - } - - [Theory, AutoSubstituteData] - public void MarkWindowAsDocked_WindowIsFloatingInMultipleLayoutEngines( - IContext context, - IWindow window, - ILayoutEngine layoutEngine2, - IWorkspace activeWorkspace - ) - { - // Given - ILayoutEngine layoutEngine = activeWorkspace.ActiveLayoutEngine; - layoutEngine2.Identity.Returns(new LayoutEngineIdentity()); - - context.Butler.Pantry.GetWorkspaceForWindow(window).Returns(activeWorkspace); - activeWorkspace - .TryGetWindowState(window) - .Returns( - new WindowState() - { - Rectangle = new Rectangle() { X = 1, Y = 2 }, - Window = window, - WindowSize = WindowSize.Normal, - } - ); - - // When - FloatingWindowPlugin plugin = CreateSut(context); - plugin.MarkWindowAsFloating(window); - - activeWorkspace.ActiveLayoutEngine.Returns(layoutEngine2); - plugin.MarkWindowAsFloating(window); - - plugin.MarkWindowAsDockedInLayoutEngine(window, layoutEngine.Identity); - - // Then - AssertFloatingWindowsEqual( - new Dictionary>() - { - { - window, - new HashSet() { layoutEngine2.Identity } - }, - }, - plugin.FloatingWindows + Assert.Contains( + transforms, + t => t.Equals(new MoveWindowToPointTransform(window.Handle, new Rectangle(0, 0, 100, 100))) ); } - #endregion } diff --git a/src/Whim.FloatingWindow.Tests/GlobalSuppressions.cs b/src/Whim.FloatingWindow.Tests/GlobalSuppressions.cs index 08038463e..724ac4ee7 100644 --- a/src/Whim.FloatingWindow.Tests/GlobalSuppressions.cs +++ b/src/Whim.FloatingWindow.Tests/GlobalSuppressions.cs @@ -7,6 +7,7 @@ // The general justification for these suppression messages is that this project contains tests, and it's // a bit excessive to have these particular rules for tests. +[assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists")] [assembly: SuppressMessage("Design", "CA1014:Mark assemblies with CLSCompliantAttribute")] [assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods")] [assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")] diff --git a/src/Whim.FloatingWindow.Tests/ProxyFloatingLayoutEngineTests.cs b/src/Whim.FloatingWindow.Tests/ProxyFloatingLayoutEngineTests.cs index eee14291c..2b7fb561d 100644 --- a/src/Whim.FloatingWindow.Tests/ProxyFloatingLayoutEngineTests.cs +++ b/src/Whim.FloatingWindow.Tests/ProxyFloatingLayoutEngineTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using NSubstitute; using Whim.TestUtils; using Windows.Win32.Foundation; @@ -6,1242 +5,1005 @@ namespace Whim.FloatingWindow.Tests; -public class ProxyFloatingLayoutEngineTests +internal static class ProxyFloatingLayoutEngineUtils { - private ProxyFloatingLayoutEngineTests MarkWindowAsFloating( - IInternalFloatingWindowPlugin plugin, + /// + /// Sets up UpdateInner. + /// + /// + /// + /// + /// + /// + /// + public static void SetupUpdate( + IContext ctx, + MutableRootSector root, + IMonitor monitor, + Workspace workspace, IWindow window, - ILayoutEngine innerLayoutEngine + Rectangle rect ) { - IReadOnlyDictionary> floatingWindows = new Dictionary< - IWindow, - ISet - > - { - { - window, - new HashSet { innerLayoutEngine.Identity } - }, - }; - plugin.FloatingWindows.Returns(floatingWindows); - return this; + monitor.WorkingArea.Returns(new Rectangle(0, 0, 100, 100)); + ctx.NativeManager.DwmGetWindowRectangle(window.Handle).Returns(rect); + StoreTestUtils.PopulateThreeWayMap(ctx, root, monitor, workspace, window); } - private ProxyFloatingLayoutEngineTests Setup_RemoveWindow( - ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine - ) + /// + /// Sets up UpdateInner, if you're only setting it up once. + /// + /// + /// + /// + public static IWindow SetupUpdateInner(IContext ctx, MutableRootSector root) { - innerLayoutEngine.RemoveWindow(window).Returns(newInnerLayoutEngine); - newInnerLayoutEngine.Identity.Returns(innerLayoutEngine.Identity); - return this; + (IMonitor monitor, Workspace workspace, IWindow window) = Create(ctx); + SetupUpdate(ctx, root, monitor, workspace, window, new Rectangle(0, 0, 10, 10)); + return window; } - private ProxyFloatingLayoutEngineTests Setup_AddWindow( - ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine - ) + public static (IMonitor, Workspace, IWindow) Create(IContext ctx) { - innerLayoutEngine.AddWindow(window).Returns(newInnerLayoutEngine); - newInnerLayoutEngine.Identity.Returns(innerLayoutEngine.Identity); - return this; + IMonitor monitor = StoreTestUtils.CreateMonitor(); + Workspace workspace = StoreTestUtils.CreateWorkspace(ctx); + IWindow window = StoreTestUtils.CreateWindow((HWND)1); + return (monitor, workspace, window); } +} - #region AddWindow - [Theory, AutoSubstituteData] - internal void AddWindow_UseInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) +public class ProxyFloatingLayoutEngine_AddWindowTests +{ + [Theory, AutoSubstituteData] + internal void AddNewWindow_PassToInner(IContext ctx, IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a new window + IWindow window = StoreTestUtils.CreateWindow((HWND)1); + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); - // When - ILayoutEngine newEngine = engine.AddWindow(window); + // WHEN adding the window + ILayoutEngine result = sut.AddWindow(window); - // Then - Assert.NotSame(engine, newEngine); + // THEN the inner layout engine should have the window. + ProxyFloatingLayoutEngine proxy = Assert.IsType(result); + Assert.NotSame(proxy, sut); + Assert.Empty(proxy.FloatingWindowRects); innerLayoutEngine.Received(1).AddWindow(window); } - [Theory, AutoSubstituteData] - internal void AddWindow_UseInner_SameInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) + [Theory, AutoSubstituteData] + internal void AddNewWindow_FailUpdate(IContext ctx, IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN an existing window which is marked as floating in the plugin + IWindow window = StoreTestUtils.CreateWindow((HWND)1); - innerLayoutEngine.AddWindow(window).Returns(innerLayoutEngine); + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - // When - ILayoutEngine newEngine = engine.AddWindow(window); + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // Then - Assert.Same(engine, newEngine); - innerLayoutEngine.Received(1).AddWindow(window); - } + // WHEN adding the window again + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.AddWindow(window); - [Theory, AutoSubstituteData] - internal void AddWindow_FloatingInPlugin_Succeed( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) - { - // Given - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - - // When - ILayoutEngine newEngine = engine.AddWindow(window); + // THEN the window should remain floating. + Assert.NotSame(proxy, result); + Assert.NotSame(sut, result); - // Then - Assert.NotSame(engine, newEngine); - innerLayoutEngine.DidNotReceive().AddWindow(window); + Assert.Empty(proxy.FloatingWindowRects); + Assert.Empty(result.FloatingWindowRects); } - [Theory, AutoSubstituteData] - internal void AddWindow_FloatingInPlugin_FailOnNoRectangle( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window + [Theory, AutoSubstituteData] + internal void AddExistingWindow_Dock( + IContext ctx, + IFloatingWindowPlugin plugin, + MutableRootSector root, + ILayoutEngine innerLayoutEngine ) { - // Given - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a floating window which we add to the engine + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - context.NativeManager.DwmGetWindowRectangle(Arg.Any()).Returns((Rectangle?)null); + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); - // When - ILayoutEngine newEngine = engine.AddWindow(window); + // (mark the window as floating) + HashSet floatingWindows = [window.Handle]; + plugin.FloatingWindows.Returns(_ => floatingWindows); - // Then - Assert.NotSame(engine, newEngine); - innerLayoutEngine.Received(1).AddWindow(window); - } + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - [Theory, AutoSubstituteData] - internal void AddWindow_FloatingInPlugin_FailOnSameRectangle( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine - ) - { - // Given - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - - // When - ILayoutEngine newEngine1 = engine.AddWindow(window); - ILayoutEngine newEngine2 = newEngine1.AddWindow(window); - - // Then - Assert.NotSame(engine, newEngine1); - Assert.Same(newEngine1, newEngine2); - innerLayoutEngine.Received(1).RemoveWindow(window); - newInnerLayoutEngine.DidNotReceive().AddWindow(window); - } + // WHEN adding the window again, but the window is no longer marked as floating + floatingWindows.Clear(); + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.AddWindow(window); - [Theory, AutoSubstituteData] - internal void AddWindow_FloatingInPlugin_RemoveFromInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine - ) - { - // Given - Setup_AddWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - - // When - ILayoutEngine newEngine = engine.AddWindow(window); - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ILayoutEngine newEngine2 = newEngine.AddWindow(window); - - // Then - Assert.NotSame(engine, newEngine); - Assert.NotSame(newEngine, newEngine2); + // THEN the window should be docked. + Assert.NotSame(proxy, result); + Assert.NotSame(sut, result); + + Assert.Empty(result.FloatingWindowRects); innerLayoutEngine.Received(1).AddWindow(window); - newInnerLayoutEngine.Received(1).RemoveWindow(window); } - #endregion - #region RemoveWindow - [Theory, AutoSubstituteData] - internal void RemoveWindow_UseInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window + [Theory, AutoSubstituteData] + internal void AddExistingWindow_UpdatePosition( + IContext ctx, + MutableRootSector root, + IFloatingWindowPlugin plugin, + ILayoutEngine innerLayoutEngine ) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - - // When - ILayoutEngine newEngine = engine.RemoveWindow(window); + // GIVEN an existing window which is marked as floating in the plugin + // (mark as the window as floating) + (IMonitor monitor, Workspace workspace, IWindow window) = ProxyFloatingLayoutEngineUtils.Create(ctx); + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); + ProxyFloatingLayoutEngineUtils.SetupUpdate(ctx, root, monitor, workspace, window, new()); - // Then - Assert.NotSame(engine, newEngine); - innerLayoutEngine.Received(1).RemoveWindow(window); - } + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - [Theory, AutoSubstituteData] - internal void RemoveWindow_UseInner_SameInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) - { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - innerLayoutEngine.RemoveWindow(window).Returns(innerLayoutEngine); + // (update the window's position) + Rectangle rect = new(0, 0, 10, 10); + ProxyFloatingLayoutEngineUtils.SetupUpdate(ctx, root, monitor, workspace, window, rect); - // When - ILayoutEngine newEngine = engine.RemoveWindow(window); - - // Then - Assert.Same(engine, newEngine); - innerLayoutEngine.Received(1).RemoveWindow(window); - } + // WHEN adding the window again + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.AddWindow(window); - [Theory, AutoSubstituteData] - internal void RemoveWindow_FloatingInPlugin( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine - ) - { - // Given - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - - // When - ILayoutEngine newEngine1 = engine.AddWindow(window); - innerLayoutEngine.ClearReceivedCalls(); - plugin.ClearReceivedCalls(); - - ILayoutEngine newEngine2 = newEngine1.RemoveWindow(window); - - // Then - Assert.NotSame(engine, newEngine1); - Assert.NotSame(newEngine1, newEngine2); - innerLayoutEngine.DidNotReceive().RemoveWindow(window); - _ = plugin.Received(1).FloatingWindows; - } - #endregion + // THEN the window should remain floating. + Assert.NotSame(proxy, result); + Assert.NotSame(sut, result); - #region MoveWindowToPoint - [Theory, AutoSubstituteData] - internal void MoveWindowToPoint_UseInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) - { - // Given - IRectangle rect = new Rectangle(); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + Assert.Single(proxy.FloatingWindowRects); + Assert.Single(result.FloatingWindowRects); - // When - ILayoutEngine newEngine = engine.MoveWindowToPoint(window, rect); + Assert.Equal(new Rectangle(0, 0, 0.1, 0.1), result.FloatingWindowRects[window]); - // Then - Assert.NotSame(engine, newEngine); - innerLayoutEngine.Received(1).MoveWindowToPoint(window, rect); + innerLayoutEngine.DidNotReceive().AddWindow(window); } +} - [Theory, AutoSubstituteData] - internal void MoveWindowToPoint_UseInner_SameInner( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_RemoveWindowTests +{ + [Theory, AutoSubstituteData] + internal void RemoveWindow_NotFloating( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + ILayoutEngine resultLayoutEngine, + MutableRootSector root ) { - // Given - IRectangle rect = new Rectangle(); + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + + innerLayoutEngine.AddWindow(window).Returns(resultLayoutEngine); + resultLayoutEngine.RemoveWindow(window).Returns(innerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - innerLayoutEngine.MoveWindowToPoint(window, rect).Returns(innerLayoutEngine); + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // When - ILayoutEngine newEngine = engine.MoveWindowToPoint(window, rect); + // WHEN removing the window + ILayoutEngine result = proxy.RemoveWindow(window); - // Then - Assert.Same(engine, newEngine); - innerLayoutEngine.Received(1).MoveWindowToPoint(window, rect); + // THEN the window should be removed from the inner layout engine. + Assert.NotSame(proxy, result); + innerLayoutEngine.Received(1).AddWindow(window); + resultLayoutEngine.Received(1).RemoveWindow(window); } - [Theory, AutoSubstituteData] - internal void MoveWindowToPoint_FloatingInPlugin_WindowIsNew( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void RemoveWindow_Floating( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + MutableRootSector root ) { - // Given - IRectangle rect = new Rectangle(); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - // When - ILayoutEngine newEngine = engine.MoveWindowToPoint(window, rect); + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // Then - Assert.NotSame(engine, newEngine); - innerLayoutEngine.DidNotReceive().MoveWindowToPoint(window, rect); - } + // WHEN removing the window + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.RemoveWindow(window); - [Theory, AutoSubstituteData] - internal void MoveWindowToPoint_FloatingInPlugin_WindowIsNotNew_SameRectangle( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine - ) - { - // Given - IRectangle rect = new Rectangle(); - - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // THEN the window should be removed from the proxy and the inner layout engine. + Assert.NotSame(proxy, result); - // When - ILayoutEngine newEngine1 = engine.AddWindow(window); - ILayoutEngine newEngine2 = newEngine1.MoveWindowToPoint(window, rect); + Assert.NotEmpty(proxy.FloatingWindowRects); + Assert.Empty(result.FloatingWindowRects); - // Then - Assert.NotSame(engine, newEngine1); - Assert.Same(newEngine1, newEngine2); - innerLayoutEngine.DidNotReceive().MoveWindowToPoint(window, rect); + innerLayoutEngine.DidNotReceive().AddWindow(window); + innerLayoutEngine.Received(1).RemoveWindow(window); } +} - [Theory, AutoSubstituteData] - internal void MoveWindowToPoint_FloatingInPlugin_WindowIsNotNew_DifferentRectangle( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_MoveWindowToPointTests +{ + [Theory, AutoSubstituteData] + internal void MoveWindowToPoint_AddToInner( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - IRectangle rect = new Rectangle(); + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); - // When - ILayoutEngine newEngine1 = engine.AddWindow(window); - ILayoutEngine newEngine2 = newEngine1.MoveWindowToPoint(window, rect); + // WHEN moving the window + sut.MoveWindowToPoint(window, new Rectangle(0, 0, 0.1, 0.1)); - // Then - Assert.NotSame(engine, newEngine1); - Assert.Same(newEngine1, newEngine2); - innerLayoutEngine.DidNotReceive().MoveWindowToPoint(window, rect); + // THEN the window should be moved in the inner layout engine. + innerLayoutEngine.Received(1).MoveWindowToPoint(window, new Rectangle(0, 0, 0.1, 0.1)); } - [Theory, AutoSubstituteData] - internal void MoveWindowToPoint_FloatingInPlugin_CannotGetDwmRectangle( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void MoveWindowToPoint_AddFloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - IRectangle rect = new Rectangle(); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_AddWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - context.NativeManager.DwmGetWindowRectangle(Arg.Any()).Returns((Rectangle?)null); + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); - // When - ILayoutEngine newEngine1 = engine.AddWindow(window); - ILayoutEngine newEngine2 = newEngine1.MoveWindowToPoint(window, rect); + // WHEN moving the window + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine) + sut.MoveWindowToPoint(window, new Rectangle(0, 0, 0.1, 0.1)); - // Then - Assert.NotSame(engine, newEngine1); - Assert.NotSame(newEngine1, newEngine2); - innerLayoutEngine.Received(1).AddWindow(window); - newInnerLayoutEngine.Received(1).MoveWindowToPoint(window, rect); + // THEN the window should be moved in the proxy. + Assert.NotSame(sut, result); + Assert.Single(result.FloatingWindowRects); + Assert.Equal(new Rectangle(0, 0, 0.1, 0.1), result.FloatingWindowRects[window]); + + innerLayoutEngine.DidNotReceive().MoveWindowToPoint(window, Arg.Any>()); } - #endregion - #region MoveWindowEdgesInDirection - [Theory, AutoSubstituteData] - internal void MoveWindowEdgesInDirection_UseInner( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void MoveWindowToPoint_DockFloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + MutableRootSector root ) { - // Given - Direction direction = Direction.Left; - IPoint deltas = new Point(); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (mark the window as floating) + HashSet floatingWindows = [window.Handle]; + plugin.FloatingWindows.Returns(_ => floatingWindows); - // When - ILayoutEngine newEngine = engine.MoveWindowEdgesInDirection(direction, deltas, window); + // (set up the inner layout engine) + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); - // Then - Assert.NotSame(engine, newEngine); - innerLayoutEngine.Received(1).MoveWindowEdgesInDirection(direction, deltas, window); - } + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine) + sut.MoveWindowToPoint(window, new Point(0, 0)); - [Theory, AutoSubstituteData] - internal void MoveWindowEdgesInDirection_UseInner_SameInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) - { - // Given - Direction direction = Direction.Left; - IPoint deltas = new Point(); - - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // WHEN moving the window + // (mark the window as docked) + floatingWindows.Clear(); + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine) + proxy.MoveWindowToPoint(window, new Rectangle(0, 0, 0.1, 0.1)); - innerLayoutEngine.MoveWindowEdgesInDirection(direction, deltas, window).Returns(innerLayoutEngine); + // THEN the window should be docked. + Assert.NotSame(proxy, result); + Assert.NotSame(sut, result); - // When - ILayoutEngine newEngine = engine.MoveWindowEdgesInDirection(direction, deltas, window); - - // Then - Assert.Same(engine, newEngine); - innerLayoutEngine.Received(1).MoveWindowEdgesInDirection(direction, deltas, window); + Assert.Empty(result.FloatingWindowRects); + innerLayoutEngine.Received(1).MoveWindowToPoint(window, new Rectangle(0, 0, 0.1, 0.1)); } +} - [Theory, AutoSubstituteData] - internal void MoveWindowEdgesInDirection_FloatingInPlugin_WindowIsNew( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_MoveWindowEdgesInDirectionTests +{ + [Theory, AutoSubstituteData] + internal void MoveWindowEdgesInDirection_MoveInnerLayoutEngine( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + MutableRootSector root ) { - // Given - Direction direction = Direction.Left; - IPoint deltas = new Point(); + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (set up the inner layout engine) + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); - // When - ILayoutEngine newEngine = engine.MoveWindowEdgesInDirection(direction, deltas, window); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // Then - Assert.NotSame(engine, newEngine); - innerLayoutEngine.DidNotReceive().MoveWindowEdgesInDirection(direction, deltas, window); - } + // WHEN moving the window + ILayoutEngine result = proxy.MoveWindowEdgesInDirection(Direction.Up, new Point(0, 0), window); - [Theory, AutoSubstituteData] - internal void MoveWindowEdgesInDirection_FloatingInPlugin_WindowIsNotNew_SameRectangle( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine - ) - { - // Given - Direction direction = Direction.Left; - IPoint deltas = new Point(); - - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - - // When - ILayoutEngine newEngine1 = engine.AddWindow(window); - ILayoutEngine newEngine2 = newEngine1.MoveWindowEdgesInDirection(direction, deltas, window); - - // Then - Assert.NotSame(engine, newEngine1); - Assert.Same(newEngine1, newEngine2); - innerLayoutEngine.DidNotReceive().MoveWindowEdgesInDirection(direction, deltas, window); + // THEN the window should be moved in the inner layout engine. + Assert.NotSame(proxy, result); + innerLayoutEngine.Received(1).MoveWindowEdgesInDirection(Direction.Up, new Point(0, 0), window); } - [Theory, AutoSubstituteData] - internal void MoveWindowEdgesInDirection_FloatingInPlugin_WindowIsNotNew_DifferentRectangle( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void MoveWindowEdgesInDirection_MoveFloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Direction direction = Direction.Left; - IPoint deltas = new Point(); - - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - - // When - ILayoutEngine newEngine1 = engine.AddWindow(window); - ILayoutEngine newEngine2 = newEngine1.MoveWindowEdgesInDirection(direction, deltas, window); - - // Then - Assert.NotSame(engine, newEngine1); - Assert.Same(newEngine1, newEngine2); - innerLayoutEngine.DidNotReceive().MoveWindowEdgesInDirection(direction, deltas, window); - } + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - [Theory, AutoSubstituteData] - internal void MoveWindowEdgesInDirection_FloatingInPlugin_CannotGetDwmRectangle( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine - ) - { - // Given - Direction direction = Direction.Left; - IPoint deltas = new Point(); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_AddWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - context.NativeManager.DwmGetWindowRectangle(Arg.Any()).Returns((Rectangle?)null); + ctx.NativeManager.DwmGetWindowRectangle(window.Handle).Returns(new Rectangle(0, 1, 2, 3)); - // When - ILayoutEngine newEngine1 = engine.AddWindow(window); - ILayoutEngine newEngine2 = newEngine1.MoveWindowEdgesInDirection(direction, deltas, window); + // WHEN moving the window + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine) + proxy.MoveWindowEdgesInDirection(Direction.Up, new Point(1, 1), window); - // Then - Assert.NotSame(engine, newEngine1); - Assert.NotSame(newEngine1, newEngine2); - innerLayoutEngine.Received(1).AddWindow(window); - newInnerLayoutEngine.Received(1).MoveWindowEdgesInDirection(direction, deltas, window); + // THEN the window should be moved in the proxy (we don't yet support window edges being moved) + Assert.NotSame(proxy, result); + Assert.Single(result.FloatingWindowRects); + + innerLayoutEngine + .DidNotReceive() + .MoveWindowEdgesInDirection(Arg.Any(), Arg.Any>(), Arg.Any()); } - #endregion +} - #region DoLayout +public class ProxyFloatingLayoutEngine_DoLayoutTests +{ [Theory, AutoSubstituteData] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] internal void DoLayout( IContext context, - MutableRootSector root, - IInternalFloatingWindowPlugin plugin, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - ILayoutEngine newInnerLayoutEngine, - IMonitor monitor + MutableRootSector root ) { - // Given the window has been added and the inner layout engine has a layout - IWindow[] allWindows = root.WindowSector.Windows.Values.ToArray(); + // GIVEN windows have been added to the layout engine + IWindow window1 = StoreTestUtils.CreateWindow((HWND)1); + IWindow window2 = StoreTestUtils.CreateWindow((HWND)2); + IWindow window3 = StoreTestUtils.CreateWindow((HWND)3); - IWindow window1 = allWindows[0]; - IWindow window2 = allWindows[1]; - IWindow floatingWindow = allWindows[2]; - MarkWindowAsFloating(plugin, floatingWindow, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, floatingWindow, newInnerLayoutEngine); + // (set up the inner layout engine) + IMonitor monitor = StoreTestUtils.CreateMonitor(); + Workspace workspace = StoreTestUtils.CreateWorkspace(context); + ProxyFloatingLayoutEngineUtils.SetupUpdate( + context, + root, + monitor, + workspace, + window1, + new Rectangle(0, 0, 10, 10) + ); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - - newInnerLayoutEngine + // (set up the layout for the third window in the inner layout engine) + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine .DoLayout(Arg.Any>(), Arg.Any()) - .Returns( + .Returns(_ => [ - new WindowState() - { - Window = window1, - Rectangle = new Rectangle(), - WindowSize = WindowSize.Normal, - }, - new WindowState() + new WindowState { - Window = window2, - Rectangle = new Rectangle(), + Window = window3, + Rectangle = new Rectangle(0, 0, 10, 10), WindowSize = WindowSize.Normal, }, ] ); - newInnerLayoutEngine.Count.Returns(2); - - // When - ILayoutEngine newEngine = engine.AddWindow(floatingWindow); - IWindowState[] windowStates = newEngine - .DoLayout( - new Rectangle() - { - X = 0, - Y = 0, - Width = 1000, - Height = 1000, - }, - monitor - ) - .ToArray(); - int count = newEngine.Count; - - // Then - Assert.Equal(3, windowStates.Length); - - IWindowState[] expected = - [ - new WindowState() - { - Window = floatingWindow, - Rectangle = new Rectangle() - { - X = 0, - Y = 0, - Width = 100, - Height = 100, - }, - WindowSize = WindowSize.Normal, - }, - new WindowState() - { - Window = window1, - Rectangle = new Rectangle(), - WindowSize = WindowSize.Normal, - }, - new WindowState() - { - Window = window2, - Rectangle = new Rectangle(), - WindowSize = WindowSize.Normal, - }, - ]; - - windowStates.Should().Equal(expected); - - Assert.Equal(3, count); - } - #endregion - #region GetFirstWindow - [Theory, AutoSubstituteData] - internal void GetFirstWindow_NoInnerFirstWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine - ) - { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - innerLayoutEngine.GetFirstWindow().Returns((IWindow?)null); + ProxyFloatingLayoutEngine sut = new(context, plugin, innerLayoutEngine); - // When - IWindow? firstWindow = engine.GetFirstWindow(); + // (mark the first and second windows as floating) + plugin.FloatingWindows.Returns(_ => (HashSet)([window1.Handle, window2.Handle])); - // Then - Assert.Null(firstWindow); - } + // (add the windows) + sut = (ProxyFloatingLayoutEngine)sut.AddWindow(window1); + sut = (ProxyFloatingLayoutEngine)sut.AddWindow(window2); + sut = (ProxyFloatingLayoutEngine)sut.AddWindow(window3); - [Theory, AutoSubstituteData] - internal void GetFirstWindow_InnerFirstWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) - { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - innerLayoutEngine.GetFirstWindow().Returns(window); + // (minimize the second window) + sut = (ProxyFloatingLayoutEngine)sut.MinimizeWindowStart(window2); + + // WHEN laying out the windows + IWindowState[] result = sut.DoLayout(new Rectangle(0, 0, 100, 100), monitor).ToArray(); - // When - IWindow? firstWindow = engine.GetFirstWindow(); + // THEN the windows should be laid out + Assert.Equal(3, result.Length); - // Then - Assert.Same(window, firstWindow); + // (window 1 should be laid out by the inner layout engine) + Assert.Equal(window1.Handle, result[0].Window.Handle); + Assert.Equal(new Rectangle(0, 0, 10, 10), result[0].Rectangle); + + // (window 2 should be laid out by the proxy, and minimized) + Assert.Equal(window2.Handle, result[1].Window.Handle); + Assert.Equal(new Rectangle(0, 0, 10, 10), result[1].Rectangle); + Assert.Equal(WindowSize.Minimized, result[1].WindowSize); + + // (window 3 should be laid out by the proxy) + Assert.Equal(window3.Handle, result[2].Window.Handle); + Assert.Equal(new Rectangle(0, 0, 10, 10), result[2].Rectangle); } +} - [Theory, AutoSubstituteData] - internal void GetFirstWindow_FloatingFirstWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_MinimizeWindowStartTests +{ + [Theory, AutoSubstituteData] + internal void MinimizeWindowStart_MinimizeFloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); + // (set up the inner layout engine) - newInnerLayoutEngine.GetFirstWindow().Returns((IWindow?)null); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - // When - IWindow? firstWindow = engine.AddWindow(window).GetFirstWindow(); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // Then - Assert.Same(window, firstWindow); - } - #endregion + // WHEN minimizing the window + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowStart(window); - #region FocusWindowInDirection - [Theory, AutoSubstituteData] - internal void FocusWindowInDirection_UseInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) - { - // Given - Direction direction = Direction.Left; - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - innerLayoutEngine.GetFirstWindow().Returns(window); + // THEN the window should be minimized in the proxy. + Assert.NotSame(sut, proxy); + Assert.NotSame(proxy, result); + + Assert.Single(proxy.FloatingWindowRects); + Assert.Empty(proxy.MinimizedWindowRects); - // When - ILayoutEngine newEngine = engine.FocusWindowInDirection(direction, window); + Assert.Empty(result.FloatingWindowRects); + Assert.Single(result.MinimizedWindowRects); - // Then - innerLayoutEngine.Received(1).FocusWindowInDirection(direction, window); - innerLayoutEngine.DidNotReceive().GetFirstWindow(); - window.DidNotReceive().Focus(); - Assert.NotSame(engine, newEngine); - Assert.IsType(newEngine); + innerLayoutEngine.DidNotReceive().MinimizeWindowStart(window); } - [Theory, AutoSubstituteData] - internal void FocusWindowInDirection_FloatingWindow_NullFirstWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void MinimizeWindowStart_WindowAlreadyMinimized( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Direction direction = Direction.Left; + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (set up the inner layout engine) - // When - ILayoutEngine newEngine = engine.AddWindow(window).FocusWindowInDirection(direction, window); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - // Then - innerLayoutEngine.DidNotReceive().FocusWindowInDirection(direction, window); - innerLayoutEngine.DidNotReceive().GetFirstWindow(); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - newInnerLayoutEngine.DidNotReceive().FocusWindowInDirection(direction, window); - newInnerLayoutEngine.Received(1).GetFirstWindow(); + // (minimize the window) + proxy = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowStart(window); - window.DidNotReceive().Focus(); - Assert.NotSame(engine, newEngine); - Assert.IsType(newEngine); + // WHEN minimizing the window again + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowStart(window); + + // THEN the window should remain minimized. + Assert.NotSame(sut, proxy); + Assert.Same(proxy, result); + + Assert.Empty(result.FloatingWindowRects); + Assert.Single(result.MinimizedWindowRects); + + innerLayoutEngine.DidNotReceive().MinimizeWindowStart(window); } - [Theory, AutoSubstituteData] - internal void FocusWindowInDirection_FloatingWindow_DefinedFirstWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void MinimizeWindowStart_MinimizeInInnerLayoutEngine( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Direction direction = Direction.Left; + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (set up the inner layout engine) + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); - newInnerLayoutEngine.GetFirstWindow().Returns(window); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // When - ILayoutEngine newEngine = engine.AddWindow(window).FocusWindowInDirection(direction, window); + // WHEN minimizing the window + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowStart(window); - // Then - innerLayoutEngine.DidNotReceive().FocusWindowInDirection(direction, window); - innerLayoutEngine.DidNotReceive().GetFirstWindow(); + // THEN the window should be minimized in the inner layout engine. + Assert.NotSame(proxy, result); - newInnerLayoutEngine.DidNotReceive().FocusWindowInDirection(direction, window); - newInnerLayoutEngine.Received(1).GetFirstWindow(); + Assert.Empty(result.FloatingWindowRects); + Assert.Empty(result.MinimizedWindowRects); - window.Received(1).Focus(); - Assert.NotSame(engine, newEngine); - Assert.IsType(newEngine); + innerLayoutEngine.Received(1).MinimizeWindowStart(window); } - #endregion +} - #region SwapWindowInDirection - [Theory, AutoSubstituteData] - internal void SwapWindowInDirection_UseInner( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_MinimizeWindowEndTests +{ + [Theory, AutoSubstituteData] + internal void MinimizeWindowEnd_WindowIsNotMinimized( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + MutableRootSector root ) { - // Given - Direction direction = Direction.Left; - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - // When - ILayoutEngine newEngine = engine.SwapWindowInDirection(direction, window); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - // Then - innerLayoutEngine.Received(1).SwapWindowInDirection(direction, window); - Assert.NotSame(engine, newEngine); - Assert.IsType(newEngine); - } + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - [Theory, AutoSubstituteData] - internal void SwapWindowInDirection_UseInner_SameInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) - { - // Given - Direction direction = Direction.Left; - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // WHEN ending the minimization + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowEnd(window); - innerLayoutEngine.SwapWindowInDirection(direction, window).Returns(innerLayoutEngine); + // THEN the window should not be minimized. + Assert.Same(proxy, result); - // When - ILayoutEngine newEngine = engine.SwapWindowInDirection(direction, window); + Assert.Single(result.FloatingWindowRects); + Assert.Empty(result.MinimizedWindowRects); - // Then - innerLayoutEngine.Received(1).SwapWindowInDirection(direction, window); - Assert.Same(engine, newEngine); - Assert.IsType(newEngine); + innerLayoutEngine.DidNotReceive().MinimizeWindowEnd(window); } - [Theory, AutoSubstituteData] - internal void SwapWindowInDirection_FloatingWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void MinimizeWindowEnd_FloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Direction direction = Direction.Left; + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); + + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (minimize the window) + proxy = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowStart(window); - // When - ILayoutEngine newEngine = engine.AddWindow(window).SwapWindowInDirection(direction, window); + // WHEN ending the minimization + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowEnd(window); - // Then - innerLayoutEngine.DidNotReceive().SwapWindowInDirection(direction, window); - Assert.NotSame(engine, newEngine); - Assert.IsType(newEngine); + // THEN the window should no longer be minimized. + Assert.NotSame(sut, proxy); + Assert.NotSame(proxy, result); + + Assert.Single(result.FloatingWindowRects); + Assert.Empty(result.MinimizedWindowRects); + + innerLayoutEngine.DidNotReceive().MinimizeWindowEnd(window); } - #endregion - #region ContainsWindow - [Theory, AutoSubstituteData] - internal void ContainsWindow_UseInner( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void MinimizeWindowEnd_InnerLayoutEngine( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + ILayoutEngine resultLayoutEngine, + MutableRootSector root ) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - // When - bool containsWindow = engine.ContainsWindow(window); + // (set up the inner layout engine) + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.MinimizeWindowStart(Arg.Any()).Returns(resultLayoutEngine); - // Then - Assert.False(containsWindow); - innerLayoutEngine.Received(1).ContainsWindow(window); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); + + // (minimize the window) + proxy = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowStart(window); + + // WHEN ending the minimization + ProxyFloatingLayoutEngine result = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowEnd(window); + + // THEN the window should no longer be minimized. + Assert.NotSame(proxy, result); + + Assert.Empty(result.FloatingWindowRects); + Assert.Empty(result.MinimizedWindowRects); + + resultLayoutEngine.Received(1).MinimizeWindowEnd(window); } +} - [Theory, AutoSubstituteData] - internal void ContainsWindow_UseInner_SameInner( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_ContainsWindowTests +{ + [Theory, AutoSubstituteData] + internal void ContainsWindow_InnerLayoutEngine( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + MutableRootSector root ) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + + // (set up the inner layout engine) + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); innerLayoutEngine.ContainsWindow(window).Returns(true); - // When - bool containsWindow = engine.ContainsWindow(window); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); + + // WHEN checking if the window is contained + bool result = proxy.ContainsWindow(window); - // Then - Assert.True(containsWindow); + // THEN the window should be contained in the inner layout engine. + Assert.True(result); innerLayoutEngine.Received(1).ContainsWindow(window); } - [Theory, AutoSubstituteData] - internal void ContainsWindow_FloatingWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void ContainsWindow_MinimizedFloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - MarkWindowAsFloating(plugin, window, innerLayoutEngine) - .Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); + + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); + + // (minimize the window) + proxy = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowStart(window); - // When - bool containsWindow = engine.AddWindow(window).ContainsWindow(window); + // WHEN checking if the window is contained + bool result = proxy.ContainsWindow(window); + + // THEN the window should be contained in the proxy. + Assert.True(result); - // Then - Assert.True(containsWindow); innerLayoutEngine.DidNotReceive().ContainsWindow(window); } - #endregion - #region WindowWasFloating_ShouldBeGarbageCollectedByUpdateInner - [Theory, AutoSubstituteData] - internal void WindowWasFloating_AddWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void ContainsWindow_FloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - // When the window is floating... - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ILayoutEngine newEngine = engine.AddWindow(window); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // ...marked as docked... - plugin.FloatingWindows.Returns(new Dictionary>()); + // WHEN checking if the window is contained + bool result = proxy.ContainsWindow(window); - // ... and then added - ILayoutEngine newEngine2 = newEngine.AddWindow(window); + // THEN the window should be contained in the proxy. + Assert.True(result); - // Then AddWindow should be called on the inner layout engine - Assert.NotSame(engine, newEngine); - Assert.NotSame(newEngine, newEngine2); - newInnerLayoutEngine.Received(1).AddWindow(window); + innerLayoutEngine.DidNotReceive().ContainsWindow(window); } - [Theory, AutoSubstituteData] - internal void WindowWasFloating_MoveWindowToPoint( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void ContainsWindow_NotContained( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - // When the window is floating... - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ILayoutEngine newEngine = engine.AddWindow(window); + // (set up the inner layout engine) + innerLayoutEngine.ContainsWindow(window).Returns(false); - // ...marked as docked... - plugin.FloatingWindows.Returns(new Dictionary>()); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); - // ... and then moved - ILayoutEngine newEngine2 = newEngine.MoveWindowToPoint(window, new Rectangle()); + // WHEN checking if the window is contained + bool result = sut.ContainsWindow(window); - // Then MoveWindowToPoint should be called on the inner layout engine - Assert.NotSame(engine, newEngine); - Assert.NotSame(newEngine, newEngine2); - newInnerLayoutEngine.Received(1).MoveWindowToPoint(window, new Rectangle()); + // THEN the window should not be contained in the inner layout engine. + Assert.False(result); + innerLayoutEngine.Received(1).ContainsWindow(window); } +} - [Theory, AutoSubstituteData] - internal void WindowWasFloating_RemoveWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_FocusWindowInDirectionTests +{ + [Theory, AutoSubstituteData] + internal void FocusWindowInDirection_InnerLayoutEngine( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - // When the window is floating... - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ILayoutEngine newEngine = engine.AddWindow(window); + // (set up the inner layout engine) + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); - // ...marked as docked... - plugin.FloatingWindows.Returns(new Dictionary>()); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // ... and then removed - ILayoutEngine newEngine2 = newEngine.RemoveWindow(window); + // WHEN focusing the window + ILayoutEngine result = proxy.FocusWindowInDirection(Direction.Up, window); - // Then RemoveWindow should be called on the inner layout engine - Assert.NotSame(engine, newEngine); - Assert.NotSame(newEngine, newEngine2); - newInnerLayoutEngine.Received(1).RemoveWindow(window); + // THEN the window should be focused in the inner layout engine. + Assert.NotSame(proxy, result); + innerLayoutEngine.Received(1).FocusWindowInDirection(Direction.Up, window); } - [Theory, AutoSubstituteData] - internal void WindowWasFloating_MoveWindowEdgesInDirection( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void FocusWindowInDirection_FloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Point deltas = new(); + // GIVEN a window which is floating + IWindow window1 = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + IWindow window2 = StoreTestUtils.CreateWindow((HWND)2); + + // (set up the inner layout engine) + innerLayoutEngine.GetFirstWindow().Returns((IWindow?)null); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); - Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window1.Handle, window2.Handle }); - // When the window is floating... - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ILayoutEngine newEngine = engine.AddWindow(window); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window1); + ProxyFloatingLayoutEngine proxy2 = (ProxyFloatingLayoutEngine)proxy.AddWindow(window2); - // ...marked as docked... - plugin.FloatingWindows.Returns(new Dictionary>()); + // WHEN focusing the window + ILayoutEngine result = proxy.FocusWindowInDirection(Direction.Up, window2); - // ... and then the edges are moved - ILayoutEngine newEngine2 = newEngine.MoveWindowEdgesInDirection(Direction.Left, deltas, window); + // THEN the window should be focused in the proxy. + Assert.NotSame(sut, proxy); + Assert.NotSame(proxy, proxy2); + Assert.NotSame(proxy2, result); - // Then MoveWindowEdgesInDirection should be called on the inner layout engine - Assert.NotSame(engine, newEngine); - Assert.NotSame(newEngine, newEngine2); - newInnerLayoutEngine.Received(1).MoveWindowEdgesInDirection(Direction.Left, deltas, window); + innerLayoutEngine.DidNotReceive().FocusWindowInDirection(Direction.Up, window1); + window1.Received(1).Focus(); } +} - [Theory, AutoSubstituteData] - internal void WindowWasFloating_SwapWindowInDirection( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_SwapWindowInDirectionTests +{ + [Theory, AutoSubstituteData] + internal void SwapWindowInDirection_InnerLayoutEngine( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window, - ILayoutEngine newInnerLayoutEngine + MutableRootSector root ) { - // Given - Setup_RemoveWindow(innerLayoutEngine, window, newInnerLayoutEngine); - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); - // When the window is floating... - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ILayoutEngine newEngine = engine.AddWindow(window); + // (set up the inner layout engine) + innerLayoutEngine.AddWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); - // ...marked as docked... - plugin.FloatingWindows.Returns(new Dictionary>()); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // ... and then window is swapped in a direction - ILayoutEngine newEngine2 = newEngine.SwapWindowInDirection(Direction.Left, window); + // WHEN swapping the window + ILayoutEngine result = proxy.SwapWindowInDirection(Direction.Up, window); - // Then SwapWindowInDirection should be called on the inner layout engine - Assert.NotSame(engine, newEngine); - Assert.NotSame(newEngine, newEngine2); - newInnerLayoutEngine.Received(1).SwapWindowInDirection(Direction.Left, window); + // THEN the window should be swapped in the inner layout engine. + Assert.NotSame(proxy, result); + innerLayoutEngine.Received(1).SwapWindowInDirection(Direction.Up, window); } - #endregion - #region PerformCustomAction - [Theory, AutoSubstituteData] - internal void PerformCustomAction_UseInner( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine + [Theory, AutoSubstituteData] + internal void SwapWindowInDirection_FloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, + ILayoutEngine innerLayoutEngine, + MutableRootSector root ) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - LayoutEngineCustomAction action = - new() - { - Name = "Action", - Payload = "payload", - Window = null, - }; - - // When - ILayoutEngine newEngine = engine.PerformCustomAction(action); - - // Then - Assert.NotSame(engine, newEngine); - Assert.IsType(newEngine); - } + // GIVEN a window which is floating + IWindow window1 = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + IWindow window2 = StoreTestUtils.CreateWindow((HWND)2); - [Theory, AutoSubstituteData] - internal void PerformCustomAction_UseInner_WindowIsDefined( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine - ) - { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - LayoutEngineCustomAction action = - new() - { - Name = "Action", - Payload = "payload", - Window = Substitute.For(), - }; - innerLayoutEngine.PerformCustomAction(action).Returns(innerLayoutEngine); - - // When - ILayoutEngine newEngine = engine.PerformCustomAction(action); - - // Then - Assert.Same(engine, newEngine); - innerLayoutEngine.Received(1).PerformCustomAction(action); - Assert.IsType(newEngine); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window1.Handle, window2.Handle }); + + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window1); + ProxyFloatingLayoutEngine proxy2 = (ProxyFloatingLayoutEngine)proxy.AddWindow(window2); + + // WHEN swapping the window + ILayoutEngine result = proxy.SwapWindowInDirection(Direction.Up, window2); + + // THEN the window should be swapped in the proxy. + Assert.NotSame(sut, proxy); + Assert.NotSame(proxy, proxy2); + Assert.Same(proxy, result); + + innerLayoutEngine.DidNotReceive().SwapWindowInDirection(Direction.Up, window1); } +} - [Theory, AutoSubstituteData] - internal void PerformCustomAction_FloatingWindow( - IContext context, - IInternalFloatingWindowPlugin plugin, +public class ProxyFloatingLayoutEngine_GetFirstWindowTests +{ + [Theory, AutoSubstituteData] + internal void GetFirstWindow_InnerLayoutEngine( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + MutableRootSector root ) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - LayoutEngineCustomAction action = - new() - { - Name = "Action", - Payload = "payload", - Window = window, - }; - MarkWindowAsFloating(plugin, window, innerLayoutEngine); - ILayoutEngine newEngine = engine.AddWindow(window); - - // When - ILayoutEngine newEngine2 = newEngine.PerformCustomAction(action); - - // Then - Assert.NotSame(engine, newEngine); - Assert.Same(newEngine, newEngine2); - innerLayoutEngine.DidNotReceive().PerformCustomAction(action); - Assert.IsType(newEngine); - } - #endregion + // GIVEN a window which is not floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + // (set up the inner layout engine) + innerLayoutEngine.GetFirstWindow().Returns(window); - [Theory, AutoSubstituteData] - internal void MinimizeWindowStart_NotSame( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) - { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); - // When - ILayoutEngine newEngine = engine.MinimizeWindowStart(window); + // WHEN getting the first window + IWindow? result = sut.GetFirstWindow(); - // Then - Assert.NotSame(engine, newEngine); + // THEN the first window should be returned from the inner layout engine. + Assert.Same(window, result); + innerLayoutEngine.Received(1).GetFirstWindow(); } - [Theory, AutoSubstituteData] - internal void MinimizeWindowStart_Same( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void GetFirstWindow_FloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + MutableRootSector root ) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - innerLayoutEngine.MinimizeWindowStart(window).Returns(innerLayoutEngine); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + + // (set up the inner layout engine) + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.GetFirstWindow().Returns((IWindow?)null); - // When - ILayoutEngine newEngine = engine.MinimizeWindowStart(window); + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); - // Then - Assert.Same(engine, newEngine); + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); + + // WHEN getting the first window + IWindow? result = proxy.GetFirstWindow(); + + // THEN the first window should be returned from the proxy. + Assert.Same(window, result); } - [Theory, AutoSubstituteData] - internal void MinimizeWindowEnd_NotSame( - IContext context, - IInternalFloatingWindowPlugin plugin, + [Theory, AutoSubstituteData] + internal void GetFirstWindow_MinimizedFloatingWindow( + IContext ctx, + IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine, - IWindow window + MutableRootSector root ) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); + // GIVEN a window which is floating + IWindow window = ProxyFloatingLayoutEngineUtils.SetupUpdateInner(ctx, root); + + // (set up the inner layout engine) + innerLayoutEngine.RemoveWindow(Arg.Any()).Returns(innerLayoutEngine); + innerLayoutEngine.GetFirstWindow().Returns((IWindow?)null); + + // (mark the window as floating) + plugin.FloatingWindows.Returns(_ => new HashSet { window.Handle }); + + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); + ProxyFloatingLayoutEngine proxy = (ProxyFloatingLayoutEngine)sut.AddWindow(window); - // When - ILayoutEngine newEngine = engine.MinimizeWindowEnd(window); + // (minimize the window) + proxy = (ProxyFloatingLayoutEngine)proxy.MinimizeWindowStart(window); - // Then - Assert.NotSame(engine, newEngine); + // WHEN getting the first window + IWindow? result = proxy.GetFirstWindow(); + + // THEN the first window should be returned from the proxy. + Assert.Same(window, result); } - [Theory, AutoSubstituteData] - internal void MinimizeWindowEnd_Same( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine, - IWindow window - ) + [Theory, AutoSubstituteData] + internal void GetFirstWindow_NoWindows(IContext ctx, IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine) { - // Given - ProxyFloatingLayoutEngine engine = new(context, plugin, innerLayoutEngine); - innerLayoutEngine.MinimizeWindowEnd(window).Returns(innerLayoutEngine); + // GIVEN no windows + innerLayoutEngine.GetFirstWindow().Returns((IWindow?)null); + + // (set up the sut) + ProxyFloatingLayoutEngine sut = new(ctx, plugin, innerLayoutEngine); - // When - ILayoutEngine newEngine = engine.MinimizeWindowEnd(window); + // WHEN getting the first window + IWindow? result = sut.GetFirstWindow(); - // Then - Assert.Same(engine, newEngine); + // THEN null should be returned. + Assert.Null(result); + innerLayoutEngine.Received(1).GetFirstWindow(); } } diff --git a/src/Whim.FloatingWindow/FloatingUtils.cs b/src/Whim.FloatingWindow/FloatingUtils.cs index 741578784..0f55fc25c 100644 --- a/src/Whim.FloatingWindow/FloatingUtils.cs +++ b/src/Whim.FloatingWindow/FloatingUtils.cs @@ -21,11 +21,6 @@ internal static class FloatingUtils IWindow window ) { - // Try get the old rectangle. - IRectangle? oldRectangle = dict.TryGetValue(window, out IRectangle? rectangle) - ? rectangle - : null; - // Since the window is floating, we update the rectangle, and return. IRectangle? newActualRectangle = context.NativeManager.DwmGetWindowRectangle(window.Handle); if (newActualRectangle == null) @@ -41,6 +36,11 @@ IWindow window return null; } + // Try get the old rectangle. + IRectangle? oldRectangle = dict.TryGetValue(window, out IRectangle? rectangle) + ? rectangle + : null; + IRectangle newUnitSquareRectangle = newMonitor.WorkingArea.NormalizeRectangle(newActualRectangle); if (newUnitSquareRectangle.Equals(oldRectangle)) { diff --git a/src/Whim.FloatingWindow/FloatingWindowPlugin.cs b/src/Whim.FloatingWindow/FloatingWindowPlugin.cs index b300779bc..50bc58cf1 100644 --- a/src/Whim.FloatingWindow/FloatingWindowPlugin.cs +++ b/src/Whim.FloatingWindow/FloatingWindowPlugin.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text.Json; +using Windows.Win32.Foundation; namespace Whim.FloatingWindow; @@ -8,7 +9,7 @@ namespace Whim.FloatingWindow; /// Creates a new instance of the floating window plugin. /// /// -public class FloatingWindowPlugin(IContext context) : IFloatingWindowPlugin, IInternalFloatingWindowPlugin +public class FloatingWindowPlugin(IContext context) : IFloatingWindowPlugin { private readonly IContext _context = context; @@ -17,16 +18,18 @@ public class FloatingWindowPlugin(IContext context) : IFloatingWindowPlugin, IIn /// public string Name => "whim.floating_window"; - private readonly Dictionary> _floatingWindows = []; + private readonly HashSet _floatingWindows = []; - /// - public IReadOnlyDictionary> FloatingWindows => _floatingWindows; + /// + public IReadOnlySet FloatingWindows => _floatingWindows; /// public void PreInitialize() { - _context.WorkspaceManager.AddProxyLayoutEngine(layout => new ProxyFloatingLayoutEngine(_context, this, layout)); - _context.WindowManager.WindowRemoved += WindowManager_WindowRemoved; + _context.Store.Dispatch( + new AddProxyLayoutEngineTransform(layout => new ProxyFloatingLayoutEngine(_context, this, layout)) + ); + _context.Store.WindowEvents.WindowRemoved += WindowEvents_WindowRemoved; } /// @@ -35,102 +38,65 @@ public void PostInitialize() { } /// public IPluginCommands PluginCommands => new FloatingWindowCommands(this); - private void WindowManager_WindowRemoved(object? sender, WindowEventArgs e) => _floatingWindows.Remove(e.Window); + private void WindowEvents_WindowRemoved(object? sender, WindowEventArgs e) => + _floatingWindows.Remove(e.Window.Handle); - private void UpdateWindow(IWindow? window, bool markAsFloating) + /// + public void MarkWindowAsFloating(IWindow? window = null) { - window ??= _context.WorkspaceManager.ActiveWorkspace.LastFocusedWindow; + window ??= _context.Store.Pick(Pickers.PickLastFocusedWindow()).ValueOrDefault; if (window == null) { - Logger.Error("Could not find window"); - return; - } - - if (!markAsFloating && !FloatingWindows.ContainsKey(window)) - { - Logger.Debug($"Window {window} is not floating"); + Logger.Error("No window to mark as floating"); return; } - if (_context.Butler.Pantry.GetWorkspaceForWindow(window) is not IWorkspace workspace) + if (!_context.Store.Pick(Pickers.PickWindowPosition(window.Handle)).TryGet(out WindowPosition windowPosition)) { - Logger.Error($"Window {window} is not in a workspace"); + Logger.Error($"Could not obtain position for floating window {window}"); return; } - if (workspace.TryGetWindowState(window) is not IWindowState windowState) - { - Logger.Error($"Could not get window state for window {window}"); - return; - } + _floatingWindows.Add(window.Handle); + _context.Store.Dispatch(new MoveWindowToPointTransform(window.Handle, windowPosition.LastWindowRectangle)); + } - LayoutEngineIdentity layoutEngineIdentity = workspace.ActiveLayoutEngine.Identity; - ISet layoutEngines = FloatingWindows.TryGetValue( - window, - out ISet? existingLayoutEngines - ) - ? existingLayoutEngines - : new HashSet(); + /// + public void MarkWindowAsDocked(IWindow? window = null) + { + window ??= _context.Store.Pick(Pickers.PickLastFocusedWindow()).ValueOrDefault; - if (markAsFloating) - { - Logger.Debug($"Marking window {window} as floating"); - layoutEngines.Add(layoutEngineIdentity); - } - else + if (window == null) { - Logger.Debug($"Marking window {window} as docked"); - layoutEngines.Remove(layoutEngineIdentity); + Logger.Error("No window to mark as docked"); + return; } - if (layoutEngines.Count == 0) - { - _floatingWindows.Remove(window); - } - else + if (!_context.Store.Pick(Pickers.PickWindowPosition(window.Handle)).TryGet(out WindowPosition windowPosition)) { - _floatingWindows[window] = layoutEngines; + Logger.Error($"Could not obtain position for docked window {window}"); + return; } - // Convert the rectangle to a unit square rectangle. - IMonitor monitor = _context.MonitorManager.GetMonitorAtPoint(windowState.Rectangle); - IRectangle unitSquareRect = monitor.WorkingArea.NormalizeRectangle(windowState.Rectangle); - - workspace.MoveWindowToPoint(window, unitSquareRect); - } - - /// - public void MarkWindowAsDockedInLayoutEngine(IWindow window, LayoutEngineIdentity layoutEngineIdentity) - { - if (_floatingWindows.TryGetValue(window, out ISet? layoutEngines)) + if (_floatingWindows.Remove(window.Handle)) { - layoutEngines.Remove(layoutEngineIdentity); - - if (layoutEngines.Count == 0) - { - _floatingWindows.Remove(window); - } + _context.Store.Dispatch(new MoveWindowToPointTransform(window.Handle, windowPosition.LastWindowRectangle)); } } - /// - public void MarkWindowAsFloating(IWindow? window = null) => UpdateWindow(window, true); - - /// - public void MarkWindowAsDocked(IWindow? window = null) => UpdateWindow(window, false); - /// public void ToggleWindowFloating(IWindow? window = null) { - window ??= _context.WorkspaceManager.ActiveWorkspace.LastFocusedWindow; + window ??= _context.Store.Pick(Pickers.PickLastFocusedWindow()).ValueOrDefault; + if (window == null) { - Logger.Error("Could not find window"); + Logger.Error("No window to toggle floating"); return; } - if (FloatingWindows.ContainsKey(window)) + if (_floatingWindows.Contains(window.Handle)) { MarkWindowAsDocked(window); } diff --git a/src/Whim.FloatingWindow/IFloatingWindowPlugin.cs b/src/Whim.FloatingWindow/IFloatingWindowPlugin.cs index 0823507a1..13c1f0bcf 100644 --- a/src/Whim.FloatingWindow/IFloatingWindowPlugin.cs +++ b/src/Whim.FloatingWindow/IFloatingWindowPlugin.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using Windows.Win32.Foundation; + namespace Whim.FloatingWindow; /// @@ -5,21 +8,26 @@ namespace Whim.FloatingWindow; /// public interface IFloatingWindowPlugin : IPlugin { + /// + /// The floating windows. + /// + IReadOnlySet FloatingWindows { get; } + /// /// Mark the given as a floating window /// /// - public void MarkWindowAsFloating(IWindow? window = null); + void MarkWindowAsFloating(IWindow? window = null); /// /// Mark the given as a docked window /// /// - public void MarkWindowAsDocked(IWindow? window = null); + void MarkWindowAsDocked(IWindow? window = null); /// /// Toggle the floating state of the given . /// /// - public void ToggleWindowFloating(IWindow? window = null); + void ToggleWindowFloating(IWindow? window = null); } diff --git a/src/Whim.FloatingWindow/IInternalFloatingWindowPlugin.cs b/src/Whim.FloatingWindow/IInternalFloatingWindowPlugin.cs deleted file mode 100644 index 3b062a63d..000000000 --- a/src/Whim.FloatingWindow/IInternalFloatingWindowPlugin.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace Whim.FloatingWindow; - -internal interface IInternalFloatingWindowPlugin -{ - /// - /// Mapping of floating windows to the layout engines that they are floating in. - /// This is not exposed outside of this namespace to prevent mutation of the dictionary and - /// sets. - /// - IReadOnlyDictionary> FloatingWindows { get; } - - /// - /// Removes the given layout engine from the given window. - /// - /// - /// - void MarkWindowAsDockedInLayoutEngine(IWindow window, LayoutEngineIdentity layoutEngineIdentity); -} diff --git a/src/Whim.FloatingWindow/ProxyFloatingLayoutEngine.cs b/src/Whim.FloatingWindow/ProxyFloatingLayoutEngine.cs index 954de700d..e7a9f153d 100644 --- a/src/Whim.FloatingWindow/ProxyFloatingLayoutEngine.cs +++ b/src/Whim.FloatingWindow/ProxyFloatingLayoutEngine.cs @@ -7,14 +7,23 @@ namespace Whim.FloatingWindow; /// /// A proxy layout engine to allow windows to be free-floating within another layout. /// -internal record ProxyFloatingLayoutEngine : BaseProxyLayoutEngine +public record ProxyFloatingLayoutEngine : BaseProxyLayoutEngine { private readonly IContext _context; - private readonly IInternalFloatingWindowPlugin _plugin; - private readonly ImmutableDictionary> _floatingWindowRects; + private readonly IFloatingWindowPlugin _plugin; + + /// + /// The positions of the floating windows. + /// + public ImmutableDictionary> FloatingWindowRects { get; } + + /// + /// The former positions of the minimized windows. + /// + public ImmutableDictionary> MinimizedWindowRects { get; } /// - public override int Count => InnerLayoutEngine.Count + _floatingWindowRects.Count; + public override int Count => InnerLayoutEngine.Count + FloatingWindowRects.Count; /// /// Creates a new instance of the proxy layout engine . @@ -22,64 +31,80 @@ internal record ProxyFloatingLayoutEngine : BaseProxyLayoutEngine /// /// /// - public ProxyFloatingLayoutEngine( - IContext context, - IInternalFloatingWindowPlugin plugin, - ILayoutEngine innerLayoutEngine - ) + public ProxyFloatingLayoutEngine(IContext context, IFloatingWindowPlugin plugin, ILayoutEngine innerLayoutEngine) : base(innerLayoutEngine) { _context = context; _plugin = plugin; - _floatingWindowRects = ImmutableDictionary>.Empty; - } - - private ProxyFloatingLayoutEngine(ProxyFloatingLayoutEngine oldEngine, ILayoutEngine newInnerLayoutEngine) - : base(newInnerLayoutEngine) - { - _context = oldEngine._context; - _plugin = oldEngine._plugin; - _floatingWindowRects = oldEngine._floatingWindowRects; + FloatingWindowRects = ImmutableDictionary>.Empty; + MinimizedWindowRects = ImmutableDictionary>.Empty; } private ProxyFloatingLayoutEngine( ProxyFloatingLayoutEngine oldEngine, ILayoutEngine newInnerLayoutEngine, - ImmutableDictionary> floatingWindowRects + ImmutableDictionary> floatingWindowRects, + ImmutableDictionary> minimizedWindows ) - : this(oldEngine, newInnerLayoutEngine) + : base(newInnerLayoutEngine) { - _floatingWindowRects = floatingWindowRects; + _context = oldEngine._context; + _plugin = oldEngine._plugin; + FloatingWindowRects = floatingWindowRects; + MinimizedWindowRects = minimizedWindows; } /// /// Returns a new instance of with the given inner layout engine, - /// if the inner layout engine has changed, or the was floating. + /// if the inner layout engine has changed, or the was floating. /// /// The new inner layout engine. - /// + /// /// The which triggered the update. If a window has triggered an inner /// layout engine update, the window is no longer floating (apart from that one case where we /// couldn't get the window's rectangle). /// /// - private ProxyFloatingLayoutEngine UpdateInner(ILayoutEngine newInnerLayoutEngine, IWindow? gcWindow) + private ProxyFloatingLayoutEngine UpdateInner(ILayoutEngine newInnerLayoutEngine, IWindow? windowToRemoveFromProxy) { ImmutableDictionary> newFloatingWindowRects = - gcWindow != null ? _floatingWindowRects.Remove(gcWindow) : _floatingWindowRects; + windowToRemoveFromProxy != null ? FloatingWindowRects.Remove(windowToRemoveFromProxy) : FloatingWindowRects; + + ImmutableDictionary> newMinimizedWindows = + windowToRemoveFromProxy != null + ? MinimizedWindowRects.Remove(windowToRemoveFromProxy) + : MinimizedWindowRects; - return InnerLayoutEngine == newInnerLayoutEngine && _floatingWindowRects == newFloatingWindowRects + return + InnerLayoutEngine == newInnerLayoutEngine + && FloatingWindowRects == newFloatingWindowRects + && MinimizedWindowRects == newMinimizedWindows ? this - : new ProxyFloatingLayoutEngine(this, newInnerLayoutEngine, newFloatingWindowRects); + : new ProxyFloatingLayoutEngine(this, newInnerLayoutEngine, newFloatingWindowRects, newMinimizedWindows); } /// public override ILayoutEngine AddWindow(IWindow window) { - // If the window is already tracked by this layout engine, or is a new floating window, - // update the rectangle and return. + if (IsWindowFloatingInLayoutEngine(window)) + { + bool shouldDock = !_plugin.FloatingWindows.Contains(window.Handle); + + if (shouldDock) + { + ILayoutEngine newInnerLayoutEngine = InnerLayoutEngine.AddWindow(window); + return new ProxyFloatingLayoutEngine( + this, + newInnerLayoutEngine, + FloatingWindowRects.Remove(window), + MinimizedWindowRects.Remove(window) + ); + } + } + if (IsWindowFloating(window)) { + // If the window is floating, update the rectangle and return. (ProxyFloatingLayoutEngine newEngine, bool error) = UpdateWindowRectangle(window); if (!error) { @@ -91,32 +116,25 @@ public override ILayoutEngine AddWindow(IWindow window) } /// - public override ILayoutEngine RemoveWindow(IWindow window) - { - bool isFloating = IsWindowFloating(window); + public override ILayoutEngine RemoveWindow(IWindow window) => + UpdateInner(InnerLayoutEngine.RemoveWindow(window), window); - // If tracked by this layout engine, remove it. - // Otherwise, pass to the inner layout engine. - if (_floatingWindowRects.ContainsKey(window)) + /// + public override ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) + { + if (IsWindowFloatingInLayoutEngine(window)) { - _plugin.MarkWindowAsDockedInLayoutEngine(window, InnerLayoutEngine.Identity); + bool shouldDock = !_plugin.FloatingWindows.Contains(window.Handle); - // If the window was not supposed to be floating, remove it from the inner layout engine. - if (isFloating) + if (shouldDock) { - return new ProxyFloatingLayoutEngine(this, InnerLayoutEngine, _floatingWindowRects.Remove(window)); + return UpdateInner(InnerLayoutEngine.MoveWindowToPoint(window, point), window); } } - return UpdateInner(InnerLayoutEngine.RemoveWindow(window), window); - } - - /// - public override ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) - { - // If the window is floating, update the rectangle and return. if (IsWindowFloating(window)) { + // If the window is floating, update the rectangle and return. (ProxyFloatingLayoutEngine newEngine, bool error) = UpdateWindowRectangle(window); if (!error) { @@ -143,16 +161,29 @@ public override ILayoutEngine MoveWindowEdgesInDirection(Direction edge, IPoint< return UpdateInner(InnerLayoutEngine.MoveWindowEdgesInDirection(edge, deltas, window), window); } - private bool IsWindowFloating(IWindow? window) => - window != null - && _plugin.FloatingWindows.TryGetValue(window, out ISet? layoutEngines) - && layoutEngines.Contains(InnerLayoutEngine.Identity); + private bool IsWindowFloating(IWindow? window) + { + if (window == null) + { + return false; + } + + if (_plugin.FloatingWindows.Contains(window.Handle)) + { + return true; + } + + return IsWindowFloatingInLayoutEngine(window); + } + + private bool IsWindowFloatingInLayoutEngine(IWindow window) => + FloatingWindowRects.ContainsKey(window) || MinimizedWindowRects.ContainsKey(window); private (ProxyFloatingLayoutEngine, bool error) UpdateWindowRectangle(IWindow window) { ImmutableDictionary>? newDict = FloatingUtils.UpdateWindowRectangle( _context, - _floatingWindowRects, + FloatingWindowRects, window ); @@ -161,20 +192,19 @@ private bool IsWindowFloating(IWindow? window) => return (this, true); } - if (newDict == _floatingWindowRects) + if (newDict == FloatingWindowRects) { return (this, false); } ILayoutEngine innerLayoutEngine = InnerLayoutEngine.RemoveWindow(window); - return (new ProxyFloatingLayoutEngine(this, innerLayoutEngine, newDict), false); + return (new ProxyFloatingLayoutEngine(this, innerLayoutEngine, newDict, MinimizedWindowRects), false); } /// public override IEnumerable DoLayout(IRectangle rectangle, IMonitor monitor) { - // Iterate over all windows in _floatingWindowRects. - foreach ((IWindow window, IRectangle loc) in _floatingWindowRects) + foreach ((IWindow window, IRectangle loc) in FloatingWindowRects) { yield return new WindowState() { @@ -184,6 +214,16 @@ public override IEnumerable DoLayout(IRectangle rectangle, IM }; } + foreach ((IWindow window, IRectangle loc) in MinimizedWindowRects) + { + yield return new WindowState() + { + Window = window, + Rectangle = rectangle.ToMonitor(loc), + WindowSize = WindowSize.Minimized, + }; + } + // Iterate over all windows in the inner layout engine. foreach (IWindowState windowState in InnerLayoutEngine.DoLayout(rectangle, monitor)) { @@ -193,7 +233,9 @@ public override IEnumerable DoLayout(IRectangle rectangle, IM /// public override IWindow? GetFirstWindow() => - InnerLayoutEngine.GetFirstWindow() ?? _floatingWindowRects.Keys.FirstOrDefault(); + InnerLayoutEngine.GetFirstWindow() + ?? FloatingWindowRects.Keys.FirstOrDefault() + ?? MinimizedWindowRects.Keys.FirstOrDefault(); /// public override ILayoutEngine FocusWindowInDirection(Direction direction, IWindow window) @@ -203,7 +245,7 @@ public override ILayoutEngine FocusWindowInDirection(Direction direction, IWindo // At this stage, we don't have a way to get the window in a child layout engine at // a given point. // As a workaround, we just focus the first window. - InnerLayoutEngine.GetFirstWindow()?.Focus(); + GetFirstWindow()?.Focus(); return this; } @@ -226,15 +268,51 @@ public override ILayoutEngine SwapWindowInDirection(Direction direction, IWindow /// public override bool ContainsWindow(IWindow window) => - _floatingWindowRects.ContainsKey(window) || InnerLayoutEngine.ContainsWindow(window); + FloatingWindowRects.ContainsKey(window) + || MinimizedWindowRects.ContainsKey(window) + || InnerLayoutEngine.ContainsWindow(window); /// - public override ILayoutEngine MinimizeWindowStart(IWindow window) => - UpdateInner(InnerLayoutEngine.MinimizeWindowStart(window), window); + public override ILayoutEngine MinimizeWindowStart(IWindow window) + { + if (MinimizedWindowRects.ContainsKey(window)) + { + return this; + } + + if (FloatingWindowRects.TryGetValue(window, out IRectangle? oldPosition)) + { + return new ProxyFloatingLayoutEngine( + this, + InnerLayoutEngine, + FloatingWindowRects.Remove(window), + MinimizedWindowRects.Add(window, oldPosition) + ); + } + + return UpdateInner(InnerLayoutEngine.MinimizeWindowStart(window), window); + } /// - public override ILayoutEngine MinimizeWindowEnd(IWindow window) => - UpdateInner(InnerLayoutEngine.MinimizeWindowEnd(window), window); + public override ILayoutEngine MinimizeWindowEnd(IWindow window) + { + if (FloatingWindowRects.ContainsKey(window)) + { + return this; + } + + if (MinimizedWindowRects.TryGetValue(window, out IRectangle? oldPosition)) + { + return new ProxyFloatingLayoutEngine( + this, + InnerLayoutEngine, + FloatingWindowRects.Add(window, oldPosition), + MinimizedWindowRects.Remove(window) + ); + } + + return UpdateInner(InnerLayoutEngine.MinimizeWindowEnd(window), window); + } /// public override ILayoutEngine PerformCustomAction(LayoutEngineCustomAction action) diff --git a/src/Whim.Gaps.Tests/GapsLayoutEngineTests.cs b/src/Whim.Gaps.Tests/GapsLayoutEngineTests.cs index cd060831b..5d18433ea 100644 --- a/src/Whim.Gaps.Tests/GapsLayoutEngineTests.cs +++ b/src/Whim.Gaps.Tests/GapsLayoutEngineTests.cs @@ -2,6 +2,8 @@ using NSubstitute; using Whim.FloatingWindow; using Whim.TestUtils; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; using Xunit; namespace Whim.Gaps.Tests; @@ -401,35 +403,107 @@ IWindowState[] expectedWindowStates windowStates.Should().Equal(expectedWindowStates); } - [Theory, AutoSubstituteData] - public void DoLayout_WithFloatingLayoutEngine(GapsConfig gapsConfig, IWindow window) + [Theory, AutoSubstituteData] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + internal void DoLayout_WithFloatingLayoutEngine(IContext context, MutableRootSector root) { // Input - Rectangle rect = new(0, 0, -300, -300); - IWindowState[] inputWindowStates = - [ - new WindowState() - { - Window = window, - Rectangle = rect, - WindowSize = WindowSize.Normal, - }, - ]; + GapsConfig gapsConfig = new(); - // Given - ILayoutEngine innerLayoutEngine = Substitute.For(); - innerLayoutEngine - .GetLayoutEngine() - .Returns(new FloatingLayoutEngine(Substitute.For(), _identity)); - innerLayoutEngine.DoLayout(rect, Arg.Any()).Returns(inputWindowStates); + Rectangle rect1 = new(10, 10, 20, 20); + Rectangle rect2 = new(30, 30, 40, 40); + Rectangle rect3 = new(50, 50, 60, 60); - GapsLayoutEngine gapsLayoutEngine = new(gapsConfig, innerLayoutEngine); + IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1); + monitor.WorkingArea.Returns(new Rectangle(0, 0, 100, 100)); + + IWindow window1 = StoreTestUtils.CreateWindow((HWND)1); + IWindow window2 = StoreTestUtils.CreateWindow((HWND)2); + IWindow window3 = StoreTestUtils.CreateWindow((HWND)3); + + Workspace workspace = StoreTestUtils.CreateWorkspace(context); + StoreTestUtils.PopulateThreeWayMap(context, root, monitor, workspace, window1); + StoreTestUtils.PopulateWindowWorkspaceMap(context, root, window2, workspace); + StoreTestUtils.PopulateWindowWorkspaceMap(context, root, window3, workspace); + + IFloatingWindowPlugin floatingWindowPlugin = Substitute.For(); + floatingWindowPlugin.FloatingWindows.Returns(_ => new HashSet() { window1.Handle, window2.Handle }); + + ILayoutEngine inner1 = Substitute.For(); + ILayoutEngine inner2 = Substitute.For(); + + inner1.AddWindow(Arg.Any()).Returns(inner2); + inner1.RemoveWindow(Arg.Any()).Returns(inner1); + inner2.Count.Returns(1); + inner2 + .DoLayout(Arg.Any>(), Arg.Any()) + .Returns( + [ + new WindowState() + { + Rectangle = rect3, + Window = window3, + WindowSize = WindowSize.Normal, + }, + ] + ); + + context.NativeManager.DwmGetWindowRectangle(window1.Handle).Returns(rect1); + context.NativeManager.DwmGetWindowRectangle(window2.Handle).Returns(rect2); + + ProxyFloatingLayoutEngine floatingLayoutEngine = new(context, floatingWindowPlugin, inner1); + + // Given + GapsLayoutEngine gapsLayoutEngine = new(gapsConfig, floatingLayoutEngine); // When - IWindowState[] outputWindowStates = gapsLayoutEngine.DoLayout(rect, Substitute.For()).ToArray(); + GapsLayoutEngine gaps1 = (GapsLayoutEngine)gapsLayoutEngine.AddWindow(window1); + GapsLayoutEngine gaps2 = (GapsLayoutEngine)gaps1.AddWindow(window2); + GapsLayoutEngine gaps3 = (GapsLayoutEngine)gaps2.AddWindow(window3); + + IWindowState[] outputWindowStates = gaps3.DoLayout(monitor.WorkingArea, monitor).ToArray(); // Then - outputWindowStates.Should().Equal(inputWindowStates); + Assert.Equal(3, outputWindowStates.Length); + + Assert.Contains( + outputWindowStates, + ws => + ws.Equals( + new WindowState() + { + Window = window1, + Rectangle = rect1, + WindowSize = WindowSize.Normal, + } + ) + ); + + Assert.Contains( + outputWindowStates, + ws => + ws.Equals( + new WindowState() + { + Window = window2, + Rectangle = rect2, + WindowSize = WindowSize.Normal, + } + ) + ); + + Assert.Contains( + outputWindowStates, + ws => + ws.Equals( + new WindowState() + { + Window = window3, + Rectangle = new Rectangle(60, 60, 40, 40), + WindowSize = WindowSize.Normal, + } + ) + ); } [Theory, AutoSubstituteData] diff --git a/src/Whim.Gaps/GapsLayoutEngine.cs b/src/Whim.Gaps/GapsLayoutEngine.cs index c9044e3cf..8ae723a75 100644 --- a/src/Whim.Gaps/GapsLayoutEngine.cs +++ b/src/Whim.Gaps/GapsLayoutEngine.cs @@ -43,14 +43,27 @@ private GapsLayoutEngine UpdateInner(ILayoutEngine newInnerLayoutEngine) => /// public override IEnumerable DoLayout(IRectangle rectangle, IMonitor monitor) { - if (InnerLayoutEngine.GetLayoutEngine() is not null) + int nonProxiedCount = 0; + if (InnerLayoutEngine is ProxyFloatingLayoutEngine proxy) { - foreach (IWindowState windowState in InnerLayoutEngine.DoLayout(rectangle, monitor)) + nonProxiedCount = proxy.FloatingWindowRects.Count + proxy.MinimizedWindowRects.Count; + + // The InnerLayoutEngine will use the default rectangle for nonProxiedCount. + // The InnerLayoutEngine will use the proxied rectangle for the remaining windows. + // This is brittle and relies on the order of the windows in the ProxyFloatingLayoutEngine. + + IEnumerable windows = InnerLayoutEngine.DoLayout(rectangle, monitor); + using IEnumerator enumerator = windows.GetEnumerator(); + + for (int i = 0; i < nonProxiedCount; i++) { - yield return windowState; - } + if (!enumerator.MoveNext()) + { + yield break; + } - yield break; + yield return enumerator.Current; + } } double scaleFactor = monitor.ScaleFactor; @@ -71,8 +84,15 @@ public override IEnumerable DoLayout(IRectangle rectangle, IM Height = rectangle.Height - doubleOuterGap, }; + int idx = 0; foreach (IWindowState windowState in InnerLayoutEngine.DoLayout(proxiedRect, monitor)) { + if (idx < nonProxiedCount) + { + idx++; + continue; + } + int x = windowState.Rectangle.X + innerGap; int y = windowState.Rectangle.Y + innerGap; int width = windowState.Rectangle.Width - doubleInnerGap; diff --git a/src/Whim.TestUtils/Whim.TestUtils.csproj b/src/Whim.TestUtils/Whim.TestUtils.csproj index 9dfb8fa2d..5f6f6a0a9 100644 --- a/src/Whim.TestUtils/Whim.TestUtils.csproj +++ b/src/Whim.TestUtils/Whim.TestUtils.csproj @@ -40,7 +40,8 @@ - + + \ No newline at end of file diff --git a/src/Whim.Tests/Store/WorkspaceSector/WorkspacePickersTests.cs b/src/Whim.Tests/Store/WorkspaceSector/WorkspacePickersTests.cs index 80aa47a34..c925a9984 100644 --- a/src/Whim.Tests/Store/WorkspaceSector/WorkspacePickersTests.cs +++ b/src/Whim.Tests/Store/WorkspaceSector/WorkspacePickersTests.cs @@ -383,6 +383,38 @@ internal void PickWindowPosition_Success(IContext ctx, MutableRootSector root) Assert.True(result.IsSuccessful); } + [Theory, AutoSubstituteData] + internal void PickWindowPositionHandle_WorkspaceNotFound(IContext ctx, MutableRootSector root) + { + // Given the workspaces and windows exist, but the workspace to search for doesn't exist + Workspace workspace = CreateWorkspace(ctx); + IWindow window = Setup_WindowPosition(ctx, root, workspace); + + Guid workspaceToSearchFor = Guid.NewGuid(); + + // When we get the window position handle + Result result = ctx.Store.Pick(Pickers.PickWindowPosition(workspaceToSearchFor, window.Handle)); + + // Then we get an error + Assert.False(result.IsSuccessful); + } + + [Theory, AutoSubstituteData] + internal void PickWindowPositionHandle_WindowNotFound(IContext ctx, MutableRootSector root) + { + // Given the workspaces and windows exist, but the window to search for doesn't exist + Workspace workspace = CreateWorkspace(ctx); + IWindow window = Setup_WindowPosition(ctx, root, workspace); + + HWND hwndToSearchFor = (HWND)987; + + // When we get the window position handle + Result result = ctx.Store.Pick(Pickers.PickWindowPosition(workspace.Id, hwndToSearchFor)); + + // Then we get an error + Assert.False(result.IsSuccessful); + } + [Theory, AutoSubstituteData] internal void PickCreateLeafLayoutEngines(IContext ctx, MutableRootSector root) { diff --git a/src/Whim.Yaml.Tests/Plugins/YamlLoader_LoadFloatingWindowPluginTests.cs b/src/Whim.Yaml.Tests/Plugins/YamlLoader_LoadFloatingWindowPluginTests.cs index e9ea024dd..c6de2da9f 100644 --- a/src/Whim.Yaml.Tests/Plugins/YamlLoader_LoadFloatingWindowPluginTests.cs +++ b/src/Whim.Yaml.Tests/Plugins/YamlLoader_LoadFloatingWindowPluginTests.cs @@ -1,7 +1,5 @@ -using DotNext; using NSubstitute; using Whim.FloatingWindow; -using Whim.Gaps; using Whim.TestUtils; using Xunit; @@ -112,62 +110,4 @@ public void LoadDisabledFloatingWindowConfig(string config, bool isYaml, IContex Assert.True(result); ctx.PluginManager.DidNotReceive().AddPlugin(Arg.Any()); } - - public static TheoryData FloatingWrapsGapsConfig => - new() - { - // YAML - { - """ - layout_engines: - entries: - - type: focus - plugins: - floating_window: - is_enabled: true - gaps: - is_enabled: true - """, - true - }, - // JSON - { - """ - { - "layout_engines": { - "entries": [ - { - "type": "focus" - } - ] - }, - "plugins": { - "floating_window": { - "is_enabled": true - }, - "gaps": { - "is_enabled": true - } - } - } - """, - false - }, - }; - - [Theory, MemberAutoSubstituteData(nameof(FloatingWrapsGapsConfig))] - public void LoadFloatingWrapsGapsConfig(string config, bool isYaml, IContext ctx) - { - // Given a floating window and gap plugin configuration - YamlLoaderTestUtils.SetupFileConfig(ctx, config, isYaml); - - // When loading the config - YamlLoader.Load(ctx); - - // Then the floating layout plugin is loaded first, followed by the gaps plugin - var calls = ctx.PluginManager.ReceivedCalls().ToArray(); - Assert.Equal(2, calls.Length); - Assert.IsType(calls[0].GetArguments()[0]); - Assert.IsType(calls[1].GetArguments()[0]); - } } diff --git a/src/Whim.Yaml/YamlPluginLoader.cs b/src/Whim.Yaml/YamlPluginLoader.cs index 9ada66b3a..0778019ae 100644 --- a/src/Whim.Yaml/YamlPluginLoader.cs +++ b/src/Whim.Yaml/YamlPluginLoader.cs @@ -25,10 +25,6 @@ internal static class YamlPluginLoader { public static void LoadPlugins(IContext ctx, Schema schema) { - // NOTE: FloatingWindowPlugin must be loaded prior to GapsPlugin. Otherwise, moving floating - // windows will cause shifting. - LoadFloatingWindowPlugin(ctx, schema); - LoadGapsPlugin(ctx, schema); LoadCommandPalettePlugin(ctx, schema); LoadFocusIndicatorPlugin(ctx, schema); LoadLayoutPreviewPlugin(ctx, schema); @@ -36,8 +32,14 @@ public static void LoadPlugins(IContext ctx, Schema schema) LoadSliceLayoutPlugin(ctx, schema); LoadTreeLayoutPlugin(ctx, schema); - // Load the bar plugin last, since it has dependencies on the TreeLayout plugin. + // Load the bar plugin after the TreeLayoutPlugin, as it has dependencies on the TreeLayout plugin. YamlBarPluginLoader.LoadBarPlugin(ctx, schema); + + // It's important for FloatingWindowPlugin to immediately precede GapsPlugin in the plugin loading order. + // This ensures that the GapsLayoutEngine will immediately contain the ProxyFloatingLayoutEngine, which + // is required for preventing gaps for floating windows. + LoadFloatingWindowPlugin(ctx, schema); + LoadGapsPlugin(ctx, schema); } private static void LoadGapsPlugin(IContext ctx, Schema schema) diff --git a/src/Whim/Commands/CoreCommands.cs b/src/Whim/Commands/CoreCommands.cs index 598562d9d..478d68d25 100644 --- a/src/Whim/Commands/CoreCommands.cs +++ b/src/Whim/Commands/CoreCommands.cs @@ -244,7 +244,7 @@ public CoreCommands(IContext context) callback: () => { IWorkspace workspace = _context.Store.Pick(PickActiveWorkspace()); - ILayoutEngine activeLayoutEngine = workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex]; + ILayoutEngine activeLayoutEngine = workspace.GetActiveLayoutEngine(); if ( activeLayoutEngine.GetLayoutEngine() diff --git a/src/Whim/Store/MapSector/Transforms/MoveWindowToPointTransform.cs b/src/Whim/Store/MapSector/Transforms/MoveWindowToPointTransform.cs index 515921d05..091ecdb5d 100644 --- a/src/Whim/Store/MapSector/Transforms/MoveWindowToPointTransform.cs +++ b/src/Whim/Store/MapSector/Transforms/MoveWindowToPointTransform.cs @@ -50,11 +50,11 @@ internal override Result Execute(IContext ctx, IInternalContext internalCt WindowHandle, targetWorkspace.Id ); - oldWorkspace.RemoveWindow(window: window); - oldWorkspace.DoLayout(); + + ctx.Store.Dispatch(new RemoveWindowFromWorkspaceTransform(oldWorkspace.Id, window)); } - targetWorkspace.MoveWindowToPoint(window, normalized, deferLayout: false); + ctx.Store.Dispatch(new MoveWindowToPointInWorkspaceTransform(targetWorkspace.Id, WindowHandle, normalized)); rootSector.WorkspaceSector.WindowHandleToFocus = window.Handle; diff --git a/src/Whim/Store/WorkspaceSector/Transforms/FocusWindowInDirectionTransform.cs b/src/Whim/Store/WorkspaceSector/Transforms/FocusWindowInDirectionTransform.cs index 359592564..688f2448a 100644 --- a/src/Whim/Store/WorkspaceSector/Transforms/FocusWindowInDirectionTransform.cs +++ b/src/Whim/Store/WorkspaceSector/Transforms/FocusWindowInDirectionTransform.cs @@ -49,7 +49,7 @@ private protected override Result WindowOperation( IWindow window ) { - ILayoutEngine layoutEngine = workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex]; + ILayoutEngine layoutEngine = workspace.GetActiveLayoutEngine(); ILayoutEngine newLayoutEngine = layoutEngine.FocusWindowInDirection(Direction, window); return newLayoutEngine == layoutEngine diff --git a/src/Whim/Store/WorkspaceSector/Transforms/MinimizeWindowEndTransform.cs b/src/Whim/Store/WorkspaceSector/Transforms/MinimizeWindowEndTransform.cs index affa8d4bd..a2033c5de 100644 --- a/src/Whim/Store/WorkspaceSector/Transforms/MinimizeWindowEndTransform.cs +++ b/src/Whim/Store/WorkspaceSector/Transforms/MinimizeWindowEndTransform.cs @@ -25,10 +25,8 @@ private protected override Result WindowOperation( IWindow window ) { - ILayoutEngine layoutEngine = workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex]; - ILayoutEngine newLayoutEngine = workspace - .LayoutEngines[workspace.ActiveLayoutEngineIndex] - .MinimizeWindowEnd(window); + ILayoutEngine layoutEngine = workspace.GetActiveLayoutEngine(); + ILayoutEngine newLayoutEngine = layoutEngine.MinimizeWindowEnd(window); return (newLayoutEngine == layoutEngine) ? workspace diff --git a/src/Whim/Store/WorkspaceSector/Transforms/MinimizeWindowStartTransform.cs b/src/Whim/Store/WorkspaceSector/Transforms/MinimizeWindowStartTransform.cs index af18a2ddd..f4412a92e 100644 --- a/src/Whim/Store/WorkspaceSector/Transforms/MinimizeWindowStartTransform.cs +++ b/src/Whim/Store/WorkspaceSector/Transforms/MinimizeWindowStartTransform.cs @@ -29,7 +29,7 @@ IWindow window // If it isn't, then we assume it was provided during startup and minimize it in all layouts. if (workspace.WindowPositions.ContainsKey(window.Handle)) { - ILayoutEngine activeLayoutEngine = workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex]; + ILayoutEngine activeLayoutEngine = workspace.GetActiveLayoutEngine(); return workspace with { LayoutEngines = workspace.LayoutEngines.SetItem( diff --git a/src/Whim/Store/WorkspaceSector/Transforms/MoveWindowEdgesInDirectionWorkspaceTransform.cs b/src/Whim/Store/WorkspaceSector/Transforms/MoveWindowEdgesInDirectionWorkspaceTransform.cs index fac70d600..3d192b377 100644 --- a/src/Whim/Store/WorkspaceSector/Transforms/MoveWindowEdgesInDirectionWorkspaceTransform.cs +++ b/src/Whim/Store/WorkspaceSector/Transforms/MoveWindowEdgesInDirectionWorkspaceTransform.cs @@ -22,7 +22,7 @@ private protected override Result WindowOperation( IWindow window ) { - ILayoutEngine oldEngine = workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex]; + ILayoutEngine oldEngine = workspace.GetActiveLayoutEngine(); ILayoutEngine newEngine = oldEngine.MoveWindowEdgesInDirection(Edges, Deltas, window); return oldEngine == newEngine diff --git a/src/Whim/Store/WorkspaceSector/Transforms/MoveWindowToPointInWorkspaceTransform.cs b/src/Whim/Store/WorkspaceSector/Transforms/MoveWindowToPointInWorkspaceTransform.cs index c802972fb..07637197f 100644 --- a/src/Whim/Store/WorkspaceSector/Transforms/MoveWindowToPointInWorkspaceTransform.cs +++ b/src/Whim/Store/WorkspaceSector/Transforms/MoveWindowToPointInWorkspaceTransform.cs @@ -24,7 +24,7 @@ IWindow window { LayoutEngines = workspace.LayoutEngines.SetItem( workspace.ActiveLayoutEngineIndex, - workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex].MoveWindowToPoint(window, Point) + workspace.GetActiveLayoutEngine().MoveWindowToPoint(window, Point) ), }; } diff --git a/src/Whim/Store/WorkspaceSector/Transforms/SwapWindowInDirectionTransform.cs b/src/Whim/Store/WorkspaceSector/Transforms/SwapWindowInDirectionTransform.cs index e1fa5b18f..67088c06b 100644 --- a/src/Whim/Store/WorkspaceSector/Transforms/SwapWindowInDirectionTransform.cs +++ b/src/Whim/Store/WorkspaceSector/Transforms/SwapWindowInDirectionTransform.cs @@ -35,7 +35,7 @@ private protected override Result WindowOperation( IWindow window ) { - ILayoutEngine oldEngine = workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex]; + ILayoutEngine oldEngine = workspace.GetActiveLayoutEngine(); ILayoutEngine newEngine = oldEngine.SwapWindowInDirection(Direction, window); return oldEngine == newEngine diff --git a/src/Whim/Store/WorkspaceSector/WorkspacePickers.cs b/src/Whim/Store/WorkspaceSector/WorkspacePickers.cs index afd315e06..8e982d2ed 100644 --- a/src/Whim/Store/WorkspaceSector/WorkspacePickers.cs +++ b/src/Whim/Store/WorkspaceSector/WorkspacePickers.cs @@ -139,11 +139,7 @@ public static PurePicker PickActiveWorkspaceId() => /// public static PurePicker> PickActiveLayoutEngine(WorkspaceId workspaceId) => (IRootSector rootSector) => - BaseWorkspacePicker( - workspaceId, - rootSector, - static workspace => workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex] - ); + BaseWorkspacePicker(workspaceId, rootSector, static workspace => workspace.GetActiveLayoutEngine()); /// /// Get the active layout engine in the active workspace. @@ -260,6 +256,28 @@ public static PurePicker> PickWindowPosition(WorkspaceId } ); + /// + /// Get the window position for the given . + /// + /// + /// The window handle to get the position for. + /// + /// + /// The window position for the given , when passed to . + /// If the window is not found, then will be returned. + /// + public static PurePicker> PickWindowPosition(HWND windowHandle) => + (IRootSector rootSector) => + { + Result workspaceResult = PickWorkspaceByWindow(windowHandle)(rootSector); + if (workspaceResult.TryGet(out IWorkspace workspace)) + { + return PickWindowPosition(workspace.Id, windowHandle)(rootSector); + } + + return Result.FromException(workspaceResult.Error!); + }; + /// /// Picks the function used to create the default layout engines to add to a workspace. /// diff --git a/src/Whim/Store/WorkspaceSector/WorkspaceSector.cs b/src/Whim/Store/WorkspaceSector/WorkspaceSector.cs index 1668e5a67..0abd851e7 100644 --- a/src/Whim/Store/WorkspaceSector/WorkspaceSector.cs +++ b/src/Whim/Store/WorkspaceSector/WorkspaceSector.cs @@ -188,7 +188,7 @@ private void SetWindowPositions(Workspace workspace, IMonitor monitor) using DeferWindowPosHandle handle = _ctx.NativeManager.DeferWindowPos(); - foreach (IWindowState loc in workspace.ActiveLayoutEngine.DoLayout(monitor.WorkingArea, monitor)) + foreach (IWindowState loc in workspace.GetActiveLayoutEngine().DoLayout(monitor.WorkingArea, monitor)) { HWND hwnd = loc.Window.Handle; IRectangle rect = loc.Rectangle; diff --git a/src/Whim/Store/WorkspaceSector/WorkspaceUtils.cs b/src/Whim/Store/WorkspaceSector/WorkspaceUtils.cs index 9a13da7c4..886f912dc 100644 --- a/src/Whim/Store/WorkspaceSector/WorkspaceUtils.cs +++ b/src/Whim/Store/WorkspaceSector/WorkspaceUtils.cs @@ -1,8 +1,11 @@ namespace Whim; -internal static class WorkspaceUtils +/// +/// Utilities for working with the updated immutable type. +/// +public static class WorkspaceUtils { - public static WorkspaceId OrActiveWorkspace(this WorkspaceId WorkspaceId, IContext ctx) => + internal static WorkspaceId OrActiveWorkspace(this WorkspaceId WorkspaceId, IContext ctx) => WorkspaceId == default ? ctx.Store.Pick(PickActiveWorkspaceId()) : WorkspaceId; /// @@ -12,7 +15,7 @@ public static WorkspaceId OrActiveWorkspace(this WorkspaceId WorkspaceId, IConte /// /// /// - public static Workspace SetActiveLayoutEngine(WorkspaceSector sector, Workspace workspace, int layoutEngineIdx) + internal static Workspace SetActiveLayoutEngine(WorkspaceSector sector, Workspace workspace, int layoutEngineIdx) { int previousLayoutEngineIdx = workspace.ActiveLayoutEngineIndex; if (previousLayoutEngineIdx == layoutEngineIdx) @@ -50,7 +53,7 @@ public static Workspace SetActiveLayoutEngine(WorkspaceSector sector, Workspace /// When , the window must be in the workspace. /// /// - public static Result GetValidWorkspaceWindow( + internal static Result GetValidWorkspaceWindow( IContext ctx, Workspace workspace, HWND windowHandle, @@ -80,4 +83,12 @@ bool isWindowRequiredInWorkspace return ctx.Store.Pick(PickWindowByHandle(windowHandle)); } + + /// + /// Get the active layout engine in the workspace. + /// + /// + /// + public static ILayoutEngine GetActiveLayoutEngine(this IWorkspace workspace) => + workspace.LayoutEngines[workspace.ActiveLayoutEngineIndex]; }