From 98fe2442d077f889bc3997abfb51d154649f3ee5 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Wed, 20 Dec 2023 14:21:45 +1300 Subject: [PATCH 01/64] Project structure --- Whim.sln | 14 ++++++ .../ILeaderStackLayoutPlugin.cs | 6 +++ .../LeaderStackLayoutPlugin.cs | 32 +++++++++++++ .../Whim.LeaderStackLayout.csproj | 48 +++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 src/Whim.LeaderStackLayout/ILeaderStackLayoutPlugin.cs create mode 100644 src/Whim.LeaderStackLayout/LeaderStackLayoutPlugin.cs create mode 100644 src/Whim.LeaderStackLayout/Whim.LeaderStackLayout.csproj diff --git a/Whim.sln b/Whim.sln index 45277b656..4c643c432 100644 --- a/Whim.sln +++ b/Whim.sln @@ -62,6 +62,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Updater", "src\Whim.Up EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Updater.Tests", "src\Whim.Updater.Tests\Whim.Updater.Tests.csproj", "{741677F5-5C83-474E-83B7-08F7A84DE11D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Whim.LeaderStackLayout", "src\Whim.LeaderStackLayout\Whim.LeaderStackLayout.csproj", "{38946E3C-F336-450F-8159-164AD14C025B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -352,6 +354,18 @@ Global {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|arm64.Build.0 = Release|Any CPU {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|x64.ActiveCfg = Release|Any CPU {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|x64.Build.0 = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.ActiveCfg = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.Build.0 = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.ActiveCfg = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.Build.0 = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.Build.0 = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.ActiveCfg = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.Build.0 = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.ActiveCfg = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Whim.LeaderStackLayout/ILeaderStackLayoutPlugin.cs b/src/Whim.LeaderStackLayout/ILeaderStackLayoutPlugin.cs new file mode 100644 index 000000000..f9b60fac0 --- /dev/null +++ b/src/Whim.LeaderStackLayout/ILeaderStackLayoutPlugin.cs @@ -0,0 +1,6 @@ +namespace Whim.LeaderStackLayout; + +public interface ILeaderStackLayoutPlugin : IPlugin +{ + // TODO +} diff --git a/src/Whim.LeaderStackLayout/LeaderStackLayoutPlugin.cs b/src/Whim.LeaderStackLayout/LeaderStackLayoutPlugin.cs new file mode 100644 index 000000000..3db3bacbc --- /dev/null +++ b/src/Whim.LeaderStackLayout/LeaderStackLayoutPlugin.cs @@ -0,0 +1,32 @@ +using System.Text.Json; + +namespace Whim.LeaderStackLayout; + +public class LeaderStackLayoutPlugin : ILeaderStackLayoutPlugin +{ + public string Name => "whim.leader_stack_layout"; + + // TODO + public IPluginCommands PluginCommands => new PluginCommands(Name); + + public void PreInitialize() + { + // TODO + } + + public void PostInitialize() + { + // TODO + } + + public void LoadState(JsonElement state) + { + // TODO + } + + public JsonElement? SaveState() + { + // TODO + return null; + } +} diff --git a/src/Whim.LeaderStackLayout/Whim.LeaderStackLayout.csproj b/src/Whim.LeaderStackLayout/Whim.LeaderStackLayout.csproj new file mode 100644 index 000000000..e024156e2 --- /dev/null +++ b/src/Whim.LeaderStackLayout/Whim.LeaderStackLayout.csproj @@ -0,0 +1,48 @@ + + + + All + All + None + All + All + All + All + All + All + All + All + All + Isaac Daly + true + An extensible window manager for Windows. + true + true + true + enable + x64;arm64;Any CPU + Whim.LeaderStackLayout + win10-x64;win10-arm64 + net7.0-windows10.0.19041.0 + 10.0.17763.0 + true + 0.2.0 + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 9b73f6d32f750e390ff74d9d9d4de16cd198f70f Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Wed, 20 Dec 2023 15:58:07 +1300 Subject: [PATCH 02/64] Rename to `SliceLayout`, and implement in-order traversal --- Whim.sln | 6 +- .../ILeaderStackLayoutPlugin.cs | 6 - src/Whim.Runner/Whim.Runner.csproj | 6 + src/Whim.SliceLayout/Area.cs | 44 +++++ src/Whim.SliceLayout/ISliceLayoutPlugin.cs | 6 + src/Whim.SliceLayout/SliceLayoutEngine.cs | 172 ++++++++++++++++++ .../SliceLayoutPlugin.cs} | 4 +- .../Whim.SliceLayout.csproj} | 6 +- 8 files changed, 236 insertions(+), 14 deletions(-) delete mode 100644 src/Whim.LeaderStackLayout/ILeaderStackLayoutPlugin.cs create mode 100644 src/Whim.SliceLayout/Area.cs create mode 100644 src/Whim.SliceLayout/ISliceLayoutPlugin.cs create mode 100644 src/Whim.SliceLayout/SliceLayoutEngine.cs rename src/{Whim.LeaderStackLayout/LeaderStackLayoutPlugin.cs => Whim.SliceLayout/SliceLayoutPlugin.cs} (79%) rename src/{Whim.LeaderStackLayout/Whim.LeaderStackLayout.csproj => Whim.SliceLayout/Whim.SliceLayout.csproj} (90%) diff --git a/Whim.sln b/Whim.sln index 4c643c432..686569c97 100644 --- a/Whim.sln +++ b/Whim.sln @@ -62,7 +62,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Updater", "src\Whim.Up EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Updater.Tests", "src\Whim.Updater.Tests\Whim.Updater.Tests.csproj", "{741677F5-5C83-474E-83B7-08F7A84DE11D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Whim.LeaderStackLayout", "src\Whim.LeaderStackLayout\Whim.LeaderStackLayout.csproj", "{38946E3C-F336-450F-8159-164AD14C025B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.SliceLayout", "src\Whim.SliceLayout\Whim.SliceLayout.csproj", "{38946E3C-F336-450F-8159-164AD14C025B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -358,8 +358,8 @@ Global {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.Build.0 = Debug|Any CPU {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.ActiveCfg = Debug|Any CPU {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.Build.0 = Debug|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.ActiveCfg = Debug|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.Build.0 = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.ActiveCfg = Debug|x64 + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.Build.0 = Debug|x64 {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.Build.0 = Release|Any CPU {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.ActiveCfg = Release|Any CPU diff --git a/src/Whim.LeaderStackLayout/ILeaderStackLayoutPlugin.cs b/src/Whim.LeaderStackLayout/ILeaderStackLayoutPlugin.cs deleted file mode 100644 index f9b60fac0..000000000 --- a/src/Whim.LeaderStackLayout/ILeaderStackLayoutPlugin.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Whim.LeaderStackLayout; - -public interface ILeaderStackLayoutPlugin : IPlugin -{ - // TODO -} diff --git a/src/Whim.Runner/Whim.Runner.csproj b/src/Whim.Runner/Whim.Runner.csproj index a29155334..9b9159b68 100644 --- a/src/Whim.Runner/Whim.Runner.csproj +++ b/src/Whim.Runner/Whim.Runner.csproj @@ -191,6 +191,12 @@ DestinationFolder="$(TargetDir)plugins\Whim.LayoutPreview\" SkipUnchangedFiles="true" /> + + + + + + /// When , the are arranged horizontally. + /// Otherwise, they are arranged vertically. + /// + bool IsHorizontal { get; } +} + +public record BaseArea : IArea +{ + public bool IsHorizontal { get; init; } +} + +internal record SliceArea : BaseArea +{ + public uint Priority { get; init; } + + public uint MaxChildren { get; init; } + + public ImmutableList Weights { get; } + + public ImmutableList Children { get; } + + public SliceArea(params (double Weight, IArea Child)[] children) + { + ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); + ImmutableList.Builder childrenBuilder = ImmutableList.CreateBuilder(); + + foreach ((double weight, IArea child) in children) + { + weightsBuilder.Add(weight); + childrenBuilder.Add(child); + } + + Weights = weightsBuilder.ToImmutable(); + Children = childrenBuilder.ToImmutable(); + } +} diff --git a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs new file mode 100644 index 000000000..983a85f93 --- /dev/null +++ b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs @@ -0,0 +1,6 @@ +namespace Whim.SliceLayout; + +public interface ISliceLayoutPlugin : IPlugin +{ + // TODO +} diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs new file mode 100644 index 000000000..e1a7766ec --- /dev/null +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Whim.SliceLayout; + +public record SliceLayoutEngine : ILayoutEngine +{ + private readonly ImmutableList _windows; + private readonly IArea _rootArea; + + public string Name { get; init; } = "Leader Stack"; + + public int Count => _windows.Count; + + public LayoutEngineIdentity Identity { get; } + + private SliceLayoutEngine(LayoutEngineIdentity identity, ImmutableList windows, IArea rootArea) + { + Identity = identity; + _windows = windows; + _rootArea = rootArea; + } + + public ILayoutEngine AddWindow(IWindow window) + { + Logger.Debug($"Adding {window}"); + return new SliceLayoutEngine(Identity, _windows.Add(window), _rootArea); + } + + public ILayoutEngine RemoveWindow(IWindow window) + { + Logger.Debug($"Removing {window}"); + return new SliceLayoutEngine(Identity, _windows.Remove(window), _rootArea); + } + + public bool ContainsWindow(IWindow window) + { + Logger.Debug($"Checking if {window} is contained"); + return _windows.Contains(window); + } + + public IEnumerable DoLayout(IRectangle rectangle, IMonitor monitor) + { + Logger.Debug($"Doing layout on {rectangle} on {monitor}"); + + if (_rootArea is SliceArea sliceArea) + { + return DoLayoutSlice(rectangle, sliceArea, 0); + } + else if (_rootArea is BaseArea baseArea) + { + return DoLayoutBase(rectangle, baseArea, 0); + } + else + { + Logger.Error($"Unknown area type: {_rootArea.GetType()}"); + return Array.Empty(); + } + } + + private IEnumerable DoLayoutSlice(IRectangle rectangle, SliceArea area, int startIdx) + { + int x = rectangle.X; + int y = rectangle.Y; + int width = rectangle.Width; + int height = rectangle.Height; + + for (int idx = 0; idx < area.Children.Count; idx++) + { + IArea childArea = area.Children[idx]; + + if (area.IsHorizontal) + { + width = Convert.ToInt32(rectangle.Width * area.Weights[idx]); + } + else + { + height = Convert.ToInt32(rectangle.Height * area.Weights[idx]); + } + + IRectangle childRectangle = new Rectangle(x, y, width, height); + + if (childArea is BaseArea baseArea) + { + foreach (IWindowState windowState in DoLayoutBase(childRectangle, baseArea, startIdx)) + { + yield return windowState; + } + } + else if (childArea is SliceArea sliceArea) + { + foreach (IWindowState windowState in DoLayoutSlice(childRectangle, sliceArea, startIdx)) + { + yield return windowState; + } + } + + if (area.IsHorizontal) + { + x += width; + } + else + { + y += height; + } + } + } + + private IEnumerable DoLayoutBase(IRectangle rectangle, BaseArea area, int startIdx) + { + int x = rectangle.X; + int y = rectangle.Y; + int width = rectangle.Width; + int height = rectangle.Height; + + int deltaX = 0; + int deltaY = 0; + + int baseItemsCount = _windows.Count - startIdx; + + if (area.IsHorizontal) + { + deltaX = rectangle.Width / baseItemsCount; + width = deltaX; + } + else + { + deltaY = rectangle.Height / baseItemsCount; + height = deltaY; + } + + for (int i = startIdx; i < _windows.Count; i++) + { + IWindow window = _windows[i]; + + yield return new WindowState() + { + Window = window, + Rectangle = new Rectangle(x, y, width, height), + WindowSize = WindowSize.Normal + }; + + if (area.IsHorizontal) + { + x += deltaX; + } + else + { + y += deltaY; + } + } + } + + public void FocusWindowInDirection(Direction direction, IWindow window) => + throw new System.NotImplementedException(); + + public IWindow? GetFirstWindow() + { + Logger.Debug($"Getting first window"); + return _windows.Count > 0 ? _windows[0] : null; + } + + public ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint deltas, IWindow window) => + throw new System.NotImplementedException(); + + public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) => + throw new System.NotImplementedException(); + + public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) => + throw new System.NotImplementedException(); +} diff --git a/src/Whim.LeaderStackLayout/LeaderStackLayoutPlugin.cs b/src/Whim.SliceLayout/SliceLayoutPlugin.cs similarity index 79% rename from src/Whim.LeaderStackLayout/LeaderStackLayoutPlugin.cs rename to src/Whim.SliceLayout/SliceLayoutPlugin.cs index 3db3bacbc..f5024ea1e 100644 --- a/src/Whim.LeaderStackLayout/LeaderStackLayoutPlugin.cs +++ b/src/Whim.SliceLayout/SliceLayoutPlugin.cs @@ -1,8 +1,8 @@ using System.Text.Json; -namespace Whim.LeaderStackLayout; +namespace Whim.SliceLayout; -public class LeaderStackLayoutPlugin : ILeaderStackLayoutPlugin +public class SliceLayoutPlugin : ISliceLayoutPlugin { public string Name => "whim.leader_stack_layout"; diff --git a/src/Whim.LeaderStackLayout/Whim.LeaderStackLayout.csproj b/src/Whim.SliceLayout/Whim.SliceLayout.csproj similarity index 90% rename from src/Whim.LeaderStackLayout/Whim.LeaderStackLayout.csproj rename to src/Whim.SliceLayout/Whim.SliceLayout.csproj index e024156e2..ce332b3f0 100644 --- a/src/Whim.LeaderStackLayout/Whim.LeaderStackLayout.csproj +++ b/src/Whim.SliceLayout/Whim.SliceLayout.csproj @@ -21,7 +21,7 @@ true enable x64;arm64;Any CPU - Whim.LeaderStackLayout + Whim.SliceLayout win10-x64;win10-arm64 net7.0-windows10.0.19041.0 10.0.17763.0 @@ -36,8 +36,8 @@ - - + + From d26d7580e8d85962f9b854b9c244dfc79e58733b Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Wed, 20 Dec 2023 16:57:55 +1300 Subject: [PATCH 03/64] Iterate over the order --- src/Whim.SliceLayout/Area.cs | 2 +- src/Whim.SliceLayout/SliceLayoutEngine.cs | 49 ++++++++++++++--------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 3c1cffef7..555b38bdf 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -17,7 +17,7 @@ public record BaseArea : IArea public bool IsHorizontal { get; init; } } -internal record SliceArea : BaseArea +public record SliceArea : BaseArea { public uint Priority { get; init; } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index e1a7766ec..2f92ed139 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Whim.SliceLayout; +internal record SliceItem(int Index, Rectangle Rectangle); + public record SliceLayoutEngine : ILayoutEngine { private readonly ImmutableList _windows; @@ -44,22 +47,38 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo { Logger.Debug($"Doing layout on {rectangle} on {monitor}"); + // Construct an ordered list of the rectangles to be laid out + SliceItem[] items; if (_rootArea is SliceArea sliceArea) { - return DoLayoutSlice(rectangle, sliceArea, 0); + items = DoLayoutSlice(rectangle, sliceArea, 0).ToArray(); } else if (_rootArea is BaseArea baseArea) { - return DoLayoutBase(rectangle, baseArea, 0); + items = DoLayoutBase(rectangle, baseArea, 0).ToArray(); } else { Logger.Error($"Unknown area type: {_rootArea.GetType()}"); return Array.Empty(); } + + // Assign the windows, in order + IWindowState[] windowStates = new IWindowState[_windows.Count]; + for (int idx = 0; idx < _windows.Count; idx++) + { + windowStates[idx] = new WindowState() + { + Rectangle = items[idx].Rectangle, + Window = _windows[idx], + WindowSize = WindowSize.Normal + }; + } + + return windowStates; } - private IEnumerable DoLayoutSlice(IRectangle rectangle, SliceArea area, int startIdx) + private IEnumerable DoLayoutSlice(IRectangle rectangle, SliceArea area, int startIdx) { int x = rectangle.X; int y = rectangle.Y; @@ -79,20 +98,21 @@ private IEnumerable DoLayoutSlice(IRectangle rectangle, Slice height = Convert.ToInt32(rectangle.Height * area.Weights[idx]); } - IRectangle childRectangle = new Rectangle(x, y, width, height); + Rectangle childRectangle = new(x, y, width, height); + int currentStartIdx = startIdx + idx; if (childArea is BaseArea baseArea) { - foreach (IWindowState windowState in DoLayoutBase(childRectangle, baseArea, startIdx)) + foreach (SliceItem sliceItem in DoLayoutBase(childRectangle, baseArea, currentStartIdx)) { - yield return windowState; + yield return sliceItem; } } else if (childArea is SliceArea sliceArea) { - foreach (IWindowState windowState in DoLayoutSlice(childRectangle, sliceArea, startIdx)) + foreach (SliceItem sliceItem in DoLayoutSlice(childRectangle, sliceArea, currentStartIdx)) { - yield return windowState; + yield return sliceItem; } } @@ -107,7 +127,7 @@ private IEnumerable DoLayoutSlice(IRectangle rectangle, Slice } } - private IEnumerable DoLayoutBase(IRectangle rectangle, BaseArea area, int startIdx) + private IEnumerable DoLayoutBase(IRectangle rectangle, BaseArea area, int startIdx) { int x = rectangle.X; int y = rectangle.Y; @@ -130,16 +150,9 @@ private IEnumerable DoLayoutBase(IRectangle rectangle, BaseAr height = deltaY; } - for (int i = startIdx; i < _windows.Count; i++) + for (int idx = startIdx; idx < _windows.Count; idx++) { - IWindow window = _windows[i]; - - yield return new WindowState() - { - Window = window, - Rectangle = new Rectangle(x, y, width, height), - WindowSize = WindowSize.Normal - }; + yield return new SliceItem(idx, new Rectangle(x, y, width, height)); if (area.IsHorizontal) { From b6f8db463ef0e72b8c0d2e1427fb44f8b288d329 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Wed, 20 Dec 2023 17:22:33 +1300 Subject: [PATCH 04/64] Added two sample layouts --- src/Whim.SliceLayout/Area.cs | 56 +++++++++++++++++++++++ src/Whim.SliceLayout/SliceLayoutEngine.cs | 3 ++ 2 files changed, 59 insertions(+) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 555b38bdf..5e0f92394 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Immutable; namespace Whim.SliceLayout; @@ -41,4 +42,59 @@ public SliceArea(params (double Weight, IArea Child)[] children) Weights = weightsBuilder.ToImmutable(); Children = childrenBuilder.ToImmutable(); } + + public static SliceArea SingleWindowArea => new((1, new BaseArea())); +} + +public static class SliceLayouts +{ + /// + /// Creates a primary stack layout, where the first window takes up half the screen, and the + /// remaining windows are stacked vertically on the other half. + /// + /// + /// + public static ILayoutEngine PrimaryStackLayout(LayoutEngineIdentity identity) => + new SliceLayoutEngine( + identity, + new SliceArea((0.5, SliceArea.SingleWindowArea), (0.5, new BaseArea() { IsHorizontal = false })) + ); + + /// + /// Creates a multi-column layout with the given number of columns. + ///
+ /// For example, new uint[] { 2, 1, 0 } will create a layout with 3 columns, where the + /// first column has 2 rows, the second column has 1 row, and the third column has infinite rows. + ///
+ /// The identity of the layout engine + /// The number of rows in each column + /// + /// + public static ILayoutEngine CreateMultiColumnLayout(LayoutEngineIdentity identity, params uint[] capacities) + { + double weight = 1.0 / capacities.Length; + (double, IArea)[] areas = new (double, IArea)[capacities.Length]; + + bool createdBaseArea = false; + for (int idx = 0; idx < capacities.Length; idx++) + { + uint capacity = capacities[idx]; + if (capacity == 0) + { + if (createdBaseArea) + { + throw new ArgumentException("Cannot have multiple base areas"); + } + + areas[idx] = (weight, new BaseArea()); + createdBaseArea = true; + } + else + { + areas[idx] = (weight, new SliceArea((1.0 / capacity, SliceArea.SingleWindowArea))); + } + } + + return new SliceLayoutEngine(identity, new SliceArea(areas)); + } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 2f92ed139..9bbee005d 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -25,6 +25,9 @@ private SliceLayoutEngine(LayoutEngineIdentity identity, ImmutableList _rootArea = rootArea; } + public SliceLayoutEngine(LayoutEngineIdentity identity, IArea rootArea) + : this(identity, ImmutableList.Empty, rootArea) { } + public ILayoutEngine AddWindow(IWindow window) { Logger.Debug($"Adding {window}"); From 7a54ef7803c3108a8bf766e0f5dfd63565baba05 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Wed, 20 Dec 2023 20:33:12 +1300 Subject: [PATCH 05/64] Respect the ordering --- src/Whim.SliceLayout/Area.cs | 36 ++++++---- src/Whim.SliceLayout/SliceLayoutEngine.cs | 85 ++++++++++++++++++++--- 2 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 5e0f92394..151eb6646 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -15,21 +15,18 @@ public interface IArea public record BaseArea : IArea { - public bool IsHorizontal { get; init; } + public bool IsHorizontal { get; protected set; } } -public record SliceArea : BaseArea +public record ParentArea : BaseArea { - public uint Priority { get; init; } - - public uint MaxChildren { get; init; } - public ImmutableList Weights { get; } public ImmutableList Children { get; } - public SliceArea(params (double Weight, IArea Child)[] children) + public ParentArea(bool isHorizontal = true, params (double Weight, IArea Child)[] children) { + IsHorizontal = isHorizontal; ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); ImmutableList.Builder childrenBuilder = ImmutableList.CreateBuilder(); @@ -42,8 +39,23 @@ public SliceArea(params (double Weight, IArea Child)[] children) Weights = weightsBuilder.ToImmutable(); Children = childrenBuilder.ToImmutable(); } +} + +public record SliceArea : BaseArea +{ + /// + /// 0-indexed order of this area in the layout engine. + /// + public uint Order { get; } - public static SliceArea SingleWindowArea => new((1, new BaseArea())); + public uint MaxChildren { get; } + + public SliceArea(uint order = 0, uint maxChildren = 1, bool isHorizontal = true) + { + Order = order; + MaxChildren = maxChildren; + IsHorizontal = isHorizontal; + } } public static class SliceLayouts @@ -54,10 +66,10 @@ public static class SliceLayouts /// /// /// - public static ILayoutEngine PrimaryStackLayout(LayoutEngineIdentity identity) => + public static ILayoutEngine CreatePrimaryStackLayout(LayoutEngineIdentity identity) => new SliceLayoutEngine( identity, - new SliceArea((0.5, SliceArea.SingleWindowArea), (0.5, new BaseArea() { IsHorizontal = false })) + new ParentArea(isHorizontal: true, (0.5, new SliceArea(maxChildren: 1, order: 0)), (0.5, new BaseArea())) ); /// @@ -91,10 +103,10 @@ public static ILayoutEngine CreateMultiColumnLayout(LayoutEngineIdentity identit } else { - areas[idx] = (weight, new SliceArea((1.0 / capacity, SliceArea.SingleWindowArea))); + areas[idx] = (weight, new SliceArea(maxChildren: capacity, order: (uint)idx)); } } - return new SliceLayoutEngine(identity, new SliceArea(areas)); + return new SliceLayoutEngine(identity, new ParentArea(isHorizontal: true, areas)); } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 9bbee005d..248b75b91 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -5,7 +5,7 @@ namespace Whim.SliceLayout; -internal record SliceItem(int Index, Rectangle Rectangle); +internal record SliceRectangleItem(int Index, Rectangle Rectangle); public record SliceLayoutEngine : ILayoutEngine { @@ -50,9 +50,18 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo { Logger.Debug($"Doing layout on {rectangle} on {monitor}"); + if (_windows.Count == 0) + { + return Enumerable.Empty(); + } + // Construct an ordered list of the rectangles to be laid out - SliceItem[] items; - if (_rootArea is SliceArea sliceArea) + SliceRectangleItem[] items; + if (_rootArea is ParentArea parentArea) + { + items = DoLayoutParent(rectangle, parentArea, 0).ToArray(); + } + else if (_rootArea is SliceArea sliceArea) { items = DoLayoutSlice(rectangle, sliceArea, 0).ToArray(); } @@ -81,7 +90,7 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo return windowStates; } - private IEnumerable DoLayoutSlice(IRectangle rectangle, SliceArea area, int startIdx) + private IEnumerable DoLayoutParent(IRectangle rectangle, ParentArea area, int startIdx) { int x = rectangle.X; int y = rectangle.Y; @@ -106,14 +115,21 @@ private IEnumerable DoLayoutSlice(IRectangle rectangle, SliceAre int currentStartIdx = startIdx + idx; if (childArea is BaseArea baseArea) { - foreach (SliceItem sliceItem in DoLayoutBase(childRectangle, baseArea, currentStartIdx)) + foreach (SliceRectangleItem sliceItem in DoLayoutBase(childRectangle, baseArea, currentStartIdx)) { yield return sliceItem; } } else if (childArea is SliceArea sliceArea) { - foreach (SliceItem sliceItem in DoLayoutSlice(childRectangle, sliceArea, currentStartIdx)) + foreach (SliceRectangleItem sliceItem in DoLayoutSlice(childRectangle, sliceArea, currentStartIdx)) + { + yield return sliceItem; + } + } + else if (childArea is ParentArea parentArea) + { + foreach (SliceRectangleItem sliceItem in DoLayoutParent(childRectangle, parentArea, currentStartIdx)) { yield return sliceItem; } @@ -130,8 +146,61 @@ private IEnumerable DoLayoutSlice(IRectangle rectangle, SliceAre } } - private IEnumerable DoLayoutBase(IRectangle rectangle, BaseArea area, int startIdx) + private IEnumerable DoLayoutSlice(IRectangle rectangle, SliceArea area, int startIdx) { + if (area.MaxChildren == 0 || startIdx >= _windows.Count) + { + yield break; + } + + int x = rectangle.X; + int y = rectangle.Y; + int width = rectangle.Width; + int height = rectangle.Height; + + int deltaX = 0; + int deltaY = 0; + + int sliceItemsCount = _windows.Count - startIdx; + + if (area.IsHorizontal) + { + deltaX = rectangle.Width / sliceItemsCount; + width = deltaX; + } + else + { + deltaY = rectangle.Height / sliceItemsCount; + height = deltaY; + } + + for (int idx = startIdx; idx < area.MaxChildren; idx++) + { + if (idx >= _windows.Count) + { + break; + } + + yield return new SliceRectangleItem(idx, new Rectangle(x, y, width, height)); + + if (area.IsHorizontal) + { + x += deltaX; + } + else + { + y += deltaY; + } + } + } + + private IEnumerable DoLayoutBase(IRectangle rectangle, BaseArea area, int startIdx) + { + if (startIdx >= _windows.Count) + { + yield break; + } + int x = rectangle.X; int y = rectangle.Y; int width = rectangle.Width; @@ -155,7 +224,7 @@ private IEnumerable DoLayoutBase(IRectangle rectangle, BaseArea for (int idx = startIdx; idx < _windows.Count; idx++) { - yield return new SliceItem(idx, new Rectangle(x, y, width, height)); + yield return new SliceRectangleItem(idx, new Rectangle(x, y, width, height)); if (area.IsHorizontal) { From c4c8c8167032a3a448c746526c5895ca026a6964 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Wed, 20 Dec 2023 20:57:13 +1300 Subject: [PATCH 06/64] Add insertion and swapping --- src/Whim.SliceLayout/Area.cs | 13 +++- src/Whim.SliceLayout/ISliceLayoutPlugin.cs | 15 +++- src/Whim.SliceLayout/SliceLayoutEngine.cs | 87 +++++++++++++++++++--- src/Whim.SliceLayout/SliceLayoutPlugin.cs | 2 + 4 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 151eb6646..f987a3a8f 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -64,10 +64,12 @@ public static class SliceLayouts /// Creates a primary stack layout, where the first window takes up half the screen, and the /// remaining windows are stacked vertically on the other half. /// + /// /// /// - public static ILayoutEngine CreatePrimaryStackLayout(LayoutEngineIdentity identity) => + public static ILayoutEngine CreatePrimaryStackLayout(ISliceLayoutPlugin plugin, LayoutEngineIdentity identity) => new SliceLayoutEngine( + plugin, identity, new ParentArea(isHorizontal: true, (0.5, new SliceArea(maxChildren: 1, order: 0)), (0.5, new BaseArea())) ); @@ -78,11 +80,16 @@ public static ILayoutEngine CreatePrimaryStackLayout(LayoutEngineIdentity identi /// For example, new uint[] { 2, 1, 0 } will create a layout with 3 columns, where the /// first column has 2 rows, the second column has 1 row, and the third column has infinite rows. /// + /// The to use /// The identity of the layout engine /// The number of rows in each column /// /// - public static ILayoutEngine CreateMultiColumnLayout(LayoutEngineIdentity identity, params uint[] capacities) + public static ILayoutEngine CreateMultiColumnLayout( + ISliceLayoutPlugin plugin, + LayoutEngineIdentity identity, + params uint[] capacities + ) { double weight = 1.0 / capacities.Length; (double, IArea)[] areas = new (double, IArea)[capacities.Length]; @@ -107,6 +114,6 @@ public static ILayoutEngine CreateMultiColumnLayout(LayoutEngineIdentity identit } } - return new SliceLayoutEngine(identity, new ParentArea(isHorizontal: true, areas)); + return new SliceLayoutEngine(plugin, identity, new ParentArea(isHorizontal: true, areas)); } } diff --git a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs index 983a85f93..bbcfd57b1 100644 --- a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs @@ -1,6 +1,19 @@ namespace Whim.SliceLayout; +public enum WindowInsertionType +{ + /// + /// Swap the window with the existing window in the slice. + /// + Swap, + + /// + /// Insert the window into the slice, pushing the existing window down the stack. + /// + Rotate +} + public interface ISliceLayoutPlugin : IPlugin { - // TODO + WindowInsertionType WindowInsertionType { get; set; } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 248b75b91..b5f7b475a 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -11,6 +11,7 @@ public record SliceLayoutEngine : ILayoutEngine { private readonly ImmutableList _windows; private readonly IArea _rootArea; + private readonly ISliceLayoutPlugin _plugin; public string Name { get; init; } = "Leader Stack"; @@ -18,26 +19,32 @@ public record SliceLayoutEngine : ILayoutEngine public LayoutEngineIdentity Identity { get; } - private SliceLayoutEngine(LayoutEngineIdentity identity, ImmutableList windows, IArea rootArea) + private SliceLayoutEngine( + ISliceLayoutPlugin plugin, + LayoutEngineIdentity identity, + ImmutableList windows, + IArea rootArea + ) { + _plugin = plugin; Identity = identity; _windows = windows; _rootArea = rootArea; } - public SliceLayoutEngine(LayoutEngineIdentity identity, IArea rootArea) - : this(identity, ImmutableList.Empty, rootArea) { } + public SliceLayoutEngine(ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, IArea rootArea) + : this(plugin, identity, ImmutableList.Empty, rootArea) { } public ILayoutEngine AddWindow(IWindow window) { Logger.Debug($"Adding {window}"); - return new SliceLayoutEngine(Identity, _windows.Add(window), _rootArea); + return new SliceLayoutEngine(_plugin, Identity, _windows.Add(window), _rootArea); } public ILayoutEngine RemoveWindow(IWindow window) { Logger.Debug($"Removing {window}"); - return new SliceLayoutEngine(Identity, _windows.Remove(window), _rootArea); + return new SliceLayoutEngine(_plugin, Identity, _windows.Remove(window), _rootArea); } public bool ContainsWindow(IWindow window) @@ -237,8 +244,11 @@ private IEnumerable DoLayoutBase(IRectangle rectangle, } } - public void FocusWindowInDirection(Direction direction, IWindow window) => + public void FocusWindowInDirection(Direction direction, IWindow window) + { + // TODO throw new System.NotImplementedException(); + } public IWindow? GetFirstWindow() { @@ -246,12 +256,69 @@ public void FocusWindowInDirection(Direction direction, IWindow window) => return _windows.Count > 0 ? _windows[0] : null; } - public ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint deltas, IWindow window) => + public ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint deltas, IWindow window) + { + // TODO throw new System.NotImplementedException(); + } - public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) => - throw new System.NotImplementedException(); + public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) + { + Logger.Debug($"Moving {window} to {point}"); + + // TODO: Get the target rectangle from the point + // TODO: Get the target area from the rectangle + // TODO: Get the target index from the area + // TODO: Swap or rotate + return this; + } - public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) => + public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) + { + // TODO throw new System.NotImplementedException(); + } + + private ILayoutEngine MoveWindowToIndex(int currentIndex, int targetIndex) + { + if (_plugin.WindowInsertionType == WindowInsertionType.Swap) + { + return SwapWindowIndices(currentIndex, targetIndex); + } + + return RotateWindowIndices(currentIndex, targetIndex); + } + + private ILayoutEngine SwapWindowIndices(int currentIndex, int targetIndex) + { + Logger.Debug($"Swapping {currentIndex} and {targetIndex}"); + + if (currentIndex == targetIndex) + { + return this; + } + + IWindow currentWindow = _windows[currentIndex]; + IWindow targetWindow = _windows[targetIndex]; + + ImmutableList newWindows = _windows + .SetItem(currentIndex, targetWindow) + .SetItem(targetIndex, currentWindow); + + return new SliceLayoutEngine(_plugin, Identity, newWindows, _rootArea); + } + + private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) + { + Logger.Debug($"Rotating {currentIndex} and {targetIndex}"); + + if (currentIndex == targetIndex) + { + return this; + } + + IWindow currentWindow = _windows[currentIndex]; + ImmutableList newWindows = _windows.Insert(targetIndex, currentWindow).RemoveAt(currentIndex); + return new SliceLayoutEngine(_plugin, Identity, newWindows, _rootArea); + } } diff --git a/src/Whim.SliceLayout/SliceLayoutPlugin.cs b/src/Whim.SliceLayout/SliceLayoutPlugin.cs index f5024ea1e..6bffb8284 100644 --- a/src/Whim.SliceLayout/SliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/SliceLayoutPlugin.cs @@ -9,6 +9,8 @@ public class SliceLayoutPlugin : ISliceLayoutPlugin // TODO public IPluginCommands PluginCommands => new PluginCommands(Name); + public WindowInsertionType WindowInsertionType { get; set; } + public void PreInitialize() { // TODO From 7a279acd05c88a3bb23fa17df59a7dc9d0bbcc56 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Wed, 20 Dec 2023 23:42:19 +1300 Subject: [PATCH 07/64] Prune the tree of empty areas before doing a layout --- src/Whim.SliceLayout/Area.cs | 193 ++++++++++++++++----- src/Whim.SliceLayout/SliceLayoutEngine.cs | 196 +++------------------- src/Whim.SliceLayout/SliceLayouts.cs | 63 +++++++ 3 files changed, 239 insertions(+), 213 deletions(-) create mode 100644 src/Whim.SliceLayout/SliceLayouts.cs diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index f987a3a8f..1d4f97996 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -13,7 +13,7 @@ public interface IArea bool IsHorizontal { get; } } -public record BaseArea : IArea +public abstract record BaseArea : IArea { public bool IsHorizontal { get; protected set; } } @@ -39,9 +39,21 @@ public ParentArea(bool isHorizontal = true, params (double Weight, IArea Child)[ Weights = weightsBuilder.ToImmutable(); Children = childrenBuilder.ToImmutable(); } + + internal ParentArea(bool isHorizontal, ImmutableList weights, ImmutableList children) + { + IsHorizontal = isHorizontal; + Weights = weights; + Children = children; + } +} + +public record BaseSliceArea : BaseArea +{ + public uint StartIndex { get; } } -public record SliceArea : BaseArea +public record SliceArea : BaseSliceArea { /// /// 0-indexed order of this area in the layout engine. @@ -58,62 +70,159 @@ public SliceArea(uint order = 0, uint maxChildren = 1, bool isHorizontal = true) } } -public static class SliceLayouts -{ - /// - /// Creates a primary stack layout, where the first window takes up half the screen, and the - /// remaining windows are stacked vertically on the other half. - /// - /// - /// - /// - public static ILayoutEngine CreatePrimaryStackLayout(ISliceLayoutPlugin plugin, LayoutEngineIdentity identity) => - new SliceLayoutEngine( - plugin, - identity, - new ParentArea(isHorizontal: true, (0.5, new SliceArea(maxChildren: 1, order: 0)), (0.5, new BaseArea())) - ); +internal record OverflowArea : BaseSliceArea { } +internal static class AreaHelpers +{ /// - /// Creates a multi-column layout with the given number of columns. - ///
- /// For example, new uint[] { 2, 1, 0 } will create a layout with 3 columns, where the - /// first column has 2 rows, the second column has 1 row, and the third column has infinite rows. + /// Prune the tree of empty areas. ///
- /// The to use - /// The identity of the layout engine - /// The number of rows in each column + /// + /// /// - /// - public static ILayoutEngine CreateMultiColumnLayout( - ISliceLayoutPlugin plugin, - LayoutEngineIdentity identity, - params uint[] capacities - ) + public static ParentArea Prune(this ParentArea area, int windowCount) { - double weight = 1.0 / capacities.Length; - (double, IArea)[] areas = new (double, IArea)[capacities.Length]; + ImmutableList.Builder childrenBuilder = ImmutableList.CreateBuilder(); + ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); - bool createdBaseArea = false; - for (int idx = 0; idx < capacities.Length; idx++) + for (int i = 0; i < area.Children.Count; i++) { - uint capacity = capacities[idx]; - if (capacity == 0) + IArea child = area.Children[i]; + if (child is ParentArea parentArea) + { + parentArea = parentArea.Prune(windowCount); + if (parentArea.Children.Count == 0) + { + continue; + } + } + else if (child is BaseSliceArea baseSliceArea) { - if (createdBaseArea) + if (baseSliceArea.StartIndex >= windowCount) { - throw new ArgumentException("Cannot have multiple base areas"); + continue; } - areas[idx] = (weight, new BaseArea()); - createdBaseArea = true; + if (baseSliceArea is SliceArea sliceArea && sliceArea.MaxChildren == 0) + { + continue; + } + + childrenBuilder.Add(child); + weightsBuilder.Add(area.Weights[i]); + } + + childrenBuilder.Add(child); + weightsBuilder.Add(area.Weights[i]); + } + + return new ParentArea(area.IsHorizontal, weightsBuilder.ToImmutable(), childrenBuilder.ToImmutable()); + } + + public static void DoParentLayout( + this SliceRectangleItem[] items, + int startIdx, + IRectangle rectangle, + ParentArea area + ) + { + if (startIdx >= items.Length) + { + return; + } + + int x = rectangle.X; + int y = rectangle.Y; + int width = rectangle.Width; + int height = rectangle.Height; + + for (int currIdx = 0; currIdx < area.Children.Count; currIdx++) + { + double weight = area.Weights[currIdx]; + IArea childArea = area.Children[currIdx]; + + if (area.IsHorizontal) + { + width = Convert.ToInt32(rectangle.Width * weight); } else { - areas[idx] = (weight, new SliceArea(maxChildren: capacity, order: (uint)idx)); + height = Convert.ToInt32(rectangle.Height * weight); + } + + Rectangle childRectangle = new(x, y, width, height); + + if (childArea is ParentArea parentArea) + { + items.DoParentLayout(startIdx + currIdx, childRectangle, parentArea); + } + else if (childArea is BaseSliceArea sliceArea) + { + items.DoSliceLayout(startIdx + currIdx, childRectangle, sliceArea); + } + + if (area.IsHorizontal) + { + x += width; } + else + { + y += height; + } + } + } + + public static void DoSliceLayout( + this SliceRectangleItem[] items, + int startIdx, + IRectangle rectangle, + BaseSliceArea area + ) + { + if (startIdx >= items.Length) + { + return; } - return new SliceLayoutEngine(plugin, identity, new ParentArea(isHorizontal: true, areas)); + int x = rectangle.X; + int y = rectangle.Y; + int width = rectangle.Width; + int height = rectangle.Height; + + int deltaX = 0; + int deltaY = 0; + + int remainingItemsCount = items.Length - startIdx; + int sliceItemsCount = remainingItemsCount; + if (area is SliceArea sliceArea) + { + sliceItemsCount = Convert.ToInt32(Math.Min(sliceArea.MaxChildren, remainingItemsCount)); + } + int maxIdx = startIdx + sliceItemsCount; + + if (area.IsHorizontal) + { + deltaX = rectangle.Width / sliceItemsCount; + width = deltaX; + } + else + { + deltaY = rectangle.Height / sliceItemsCount; + height = deltaY; + } + + for (int currIdx = startIdx; currIdx < maxIdx; currIdx++) + { + items[currIdx] = new SliceRectangleItem(currIdx, new Rectangle(x, y, width, height)); + + if (area.IsHorizontal) + { + x += deltaX; + } + else + { + y += deltaY; + } + } } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index b5f7b475a..cd133f59b 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -10,7 +9,7 @@ internal record SliceRectangleItem(int Index, Rectangle Rectangle); public record SliceLayoutEngine : ILayoutEngine { private readonly ImmutableList _windows; - private readonly IArea _rootArea; + private readonly ParentArea _rootArea; private readonly ISliceLayoutPlugin _plugin; public string Name { get; init; } = "Leader Stack"; @@ -23,7 +22,7 @@ private SliceLayoutEngine( ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, ImmutableList windows, - IArea rootArea + ParentArea rootArea ) { _plugin = plugin; @@ -32,7 +31,7 @@ IArea rootArea _rootArea = rootArea; } - public SliceLayoutEngine(ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, IArea rootArea) + public SliceLayoutEngine(ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, ParentArea rootArea) : this(plugin, identity, ImmutableList.Empty, rootArea) { } public ILayoutEngine AddWindow(IWindow window) @@ -53,6 +52,7 @@ public bool ContainsWindow(IWindow window) return _windows.Contains(window); } + // TODO: Handle when areas are partially or completely empty. public IEnumerable DoLayout(IRectangle rectangle, IMonitor monitor) { Logger.Debug($"Doing layout on {rectangle} on {monitor}"); @@ -62,27 +62,14 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo return Enumerable.Empty(); } - // Construct an ordered list of the rectangles to be laid out - SliceRectangleItem[] items; - if (_rootArea is ParentArea parentArea) - { - items = DoLayoutParent(rectangle, parentArea, 0).ToArray(); - } - else if (_rootArea is SliceArea sliceArea) - { - items = DoLayoutSlice(rectangle, sliceArea, 0).ToArray(); - } - else if (_rootArea is BaseArea baseArea) - { - items = DoLayoutBase(rectangle, baseArea, 0).ToArray(); - } - else - { - Logger.Error($"Unknown area type: {_rootArea.GetType()}"); - return Array.Empty(); - } + // Prune the empty areas from the tree + ParentArea prunedRootArea = _rootArea.Prune(_windows.Count); - // Assign the windows, in order + // Get the rectangles for each window + SliceRectangleItem[] items = new SliceRectangleItem[_windows.Count]; + items.DoParentLayout(0, rectangle, prunedRootArea); + + // Get the window states IWindowState[] windowStates = new IWindowState[_windows.Count]; for (int idx = 0; idx < _windows.Count; idx++) { @@ -97,153 +84,6 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo return windowStates; } - private IEnumerable DoLayoutParent(IRectangle rectangle, ParentArea area, int startIdx) - { - int x = rectangle.X; - int y = rectangle.Y; - int width = rectangle.Width; - int height = rectangle.Height; - - for (int idx = 0; idx < area.Children.Count; idx++) - { - IArea childArea = area.Children[idx]; - - if (area.IsHorizontal) - { - width = Convert.ToInt32(rectangle.Width * area.Weights[idx]); - } - else - { - height = Convert.ToInt32(rectangle.Height * area.Weights[idx]); - } - - Rectangle childRectangle = new(x, y, width, height); - - int currentStartIdx = startIdx + idx; - if (childArea is BaseArea baseArea) - { - foreach (SliceRectangleItem sliceItem in DoLayoutBase(childRectangle, baseArea, currentStartIdx)) - { - yield return sliceItem; - } - } - else if (childArea is SliceArea sliceArea) - { - foreach (SliceRectangleItem sliceItem in DoLayoutSlice(childRectangle, sliceArea, currentStartIdx)) - { - yield return sliceItem; - } - } - else if (childArea is ParentArea parentArea) - { - foreach (SliceRectangleItem sliceItem in DoLayoutParent(childRectangle, parentArea, currentStartIdx)) - { - yield return sliceItem; - } - } - - if (area.IsHorizontal) - { - x += width; - } - else - { - y += height; - } - } - } - - private IEnumerable DoLayoutSlice(IRectangle rectangle, SliceArea area, int startIdx) - { - if (area.MaxChildren == 0 || startIdx >= _windows.Count) - { - yield break; - } - - int x = rectangle.X; - int y = rectangle.Y; - int width = rectangle.Width; - int height = rectangle.Height; - - int deltaX = 0; - int deltaY = 0; - - int sliceItemsCount = _windows.Count - startIdx; - - if (area.IsHorizontal) - { - deltaX = rectangle.Width / sliceItemsCount; - width = deltaX; - } - else - { - deltaY = rectangle.Height / sliceItemsCount; - height = deltaY; - } - - for (int idx = startIdx; idx < area.MaxChildren; idx++) - { - if (idx >= _windows.Count) - { - break; - } - - yield return new SliceRectangleItem(idx, new Rectangle(x, y, width, height)); - - if (area.IsHorizontal) - { - x += deltaX; - } - else - { - y += deltaY; - } - } - } - - private IEnumerable DoLayoutBase(IRectangle rectangle, BaseArea area, int startIdx) - { - if (startIdx >= _windows.Count) - { - yield break; - } - - int x = rectangle.X; - int y = rectangle.Y; - int width = rectangle.Width; - int height = rectangle.Height; - - int deltaX = 0; - int deltaY = 0; - - int baseItemsCount = _windows.Count - startIdx; - - if (area.IsHorizontal) - { - deltaX = rectangle.Width / baseItemsCount; - width = deltaX; - } - else - { - deltaY = rectangle.Height / baseItemsCount; - height = deltaY; - } - - for (int idx = startIdx; idx < _windows.Count; idx++) - { - yield return new SliceRectangleItem(idx, new Rectangle(x, y, width, height)); - - if (area.IsHorizontal) - { - x += deltaX; - } - else - { - y += deltaY; - } - } - } - public void FocusWindowInDirection(Direction direction, IWindow window) { // TODO @@ -321,4 +161,18 @@ private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) ImmutableList newWindows = _windows.Insert(targetIndex, currentWindow).RemoveAt(currentIndex); return new SliceLayoutEngine(_plugin, Identity, newWindows, _rootArea); } + + private (int Index, IWindow Window)? GetWindowAtPoint(IPoint point) + { + Rectangle parentRect = Rectangle.UnitSquare(); + if (!parentRect.ContainsPoint(point)) + { + return null; + } + + // TODO + + + throw new System.NotImplementedException(); + } } diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs new file mode 100644 index 000000000..c6903007a --- /dev/null +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -0,0 +1,63 @@ +using System; + +namespace Whim.SliceLayout; + +public static class SliceLayouts +{ + /// + /// Creates a primary stack layout, where the first window takes up half the screen, and the + /// remaining windows are stacked vertically on the other half. + /// + /// + /// + /// + public static ILayoutEngine CreatePrimaryStackLayout(ISliceLayoutPlugin plugin, LayoutEngineIdentity identity) => + new SliceLayoutEngine( + plugin, + identity, + new ParentArea(isHorizontal: true, (0.5, new SliceArea(maxChildren: 1, order: 0)), (0.5, new BaseArea())) + ); + + /// + /// Creates a multi-column layout with the given number of columns. + ///
+ /// For example, new uint[] { 2, 1, 0 } will create a layout with 3 columns, where the + /// first column has 2 rows, the second column has 1 row, and the third column has infinite rows. + ///
+ /// The to use + /// The identity of the layout engine + /// The number of rows in each column + /// + /// + public static ILayoutEngine CreateMultiColumnLayout( + ISliceLayoutPlugin plugin, + LayoutEngineIdentity identity, + params uint[] capacities + ) + { + double weight = 1.0 / capacities.Length; + (double, IArea)[] areas = new (double, IArea)[capacities.Length]; + + bool createdBaseArea = false; + for (int idx = 0; idx < capacities.Length; idx++) + { + uint capacity = capacities[idx]; + if (capacity == 0) + { + if (createdBaseArea) + { + throw new ArgumentException("Cannot have multiple base areas"); + } + + areas[idx] = (weight, new BaseArea()); + createdBaseArea = true; + } + else + { + areas[idx] = (weight, new SliceArea(maxChildren: capacity, order: (uint)idx)); + } + } + + return new SliceLayoutEngine(plugin, identity, new ParentArea(isHorizontal: true, areas)); + } +} From 9e46121855e31bd8627ccc086964c8de1466b946 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 00:02:28 +1300 Subject: [PATCH 08/64] Implement an easy `GetWindowAtPoint` --- src/Whim.SliceLayout/SliceLayoutEngine.cs | 55 +++++++++++++++-------- src/Whim.SliceLayout/SliceLayouts.cs | 26 ++++++++--- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index cd133f59b..74594c0f6 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -8,6 +8,7 @@ internal record SliceRectangleItem(int Index, Rectangle Rectangle); public record SliceLayoutEngine : ILayoutEngine { + private readonly IContext _context; private readonly ImmutableList _windows; private readonly ParentArea _rootArea; private readonly ISliceLayoutPlugin _plugin; @@ -19,31 +20,38 @@ public record SliceLayoutEngine : ILayoutEngine public LayoutEngineIdentity Identity { get; } private SliceLayoutEngine( + IContext context, ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, ImmutableList windows, ParentArea rootArea ) { + _context = context; _plugin = plugin; Identity = identity; _windows = windows; _rootArea = rootArea; } - public SliceLayoutEngine(ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, ParentArea rootArea) - : this(plugin, identity, ImmutableList.Empty, rootArea) { } + public SliceLayoutEngine( + IContext context, + ISliceLayoutPlugin plugin, + LayoutEngineIdentity identity, + ParentArea rootArea + ) + : this(context, plugin, identity, ImmutableList.Empty, rootArea) { } public ILayoutEngine AddWindow(IWindow window) { Logger.Debug($"Adding {window}"); - return new SliceLayoutEngine(_plugin, Identity, _windows.Add(window), _rootArea); + return new SliceLayoutEngine(_context, _plugin, Identity, _windows.Add(window), _rootArea); } public ILayoutEngine RemoveWindow(IWindow window) { Logger.Debug($"Removing {window}"); - return new SliceLayoutEngine(_plugin, Identity, _windows.Remove(window), _rootArea); + return new SliceLayoutEngine(_context, _plugin, Identity, _windows.Remove(window), _rootArea); } public bool ContainsWindow(IWindow window) @@ -106,11 +114,12 @@ public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) { Logger.Debug($"Moving {window} to {point}"); - // TODO: Get the target rectangle from the point - // TODO: Get the target area from the rectangle - // TODO: Get the target index from the area - // TODO: Swap or rotate - return this; + if (GetWindowAtPoint(point) is not (int, IWindow) windowAtPoint) + { + return this; + } + + return MoveWindowToIndex(_windows.IndexOf(window), windowAtPoint.Index); } public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) @@ -145,7 +154,7 @@ private ILayoutEngine SwapWindowIndices(int currentIndex, int targetIndex) .SetItem(currentIndex, targetWindow) .SetItem(targetIndex, currentWindow); - return new SliceLayoutEngine(_plugin, Identity, newWindows, _rootArea); + return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); } private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) @@ -159,20 +168,30 @@ private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) IWindow currentWindow = _windows[currentIndex]; ImmutableList newWindows = _windows.Insert(targetIndex, currentWindow).RemoveAt(currentIndex); - return new SliceLayoutEngine(_plugin, Identity, newWindows, _rootArea); + return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); } private (int Index, IWindow Window)? GetWindowAtPoint(IPoint point) { - Rectangle parentRect = Rectangle.UnitSquare(); - if (!parentRect.ContainsPoint(point)) - { - return null; - } + Logger.Debug($"Getting window at {point}"); - // TODO + // This is the easy way - we DoLayout with fake coordinates, then linearly search for the + // window at the point, then swap or rotate. + Point scaledPoint = new((int)(point.X * 10000), (int)(point.Y * 10000)); + Rectangle rectangle = new(0, 0, 10000, 10000); - throw new System.NotImplementedException(); + int idx = 0; + foreach (IWindowState windowState in DoLayout(rectangle, _context.MonitorManager.PrimaryMonitor)) + { + if (windowState.Rectangle.ContainsPoint(scaledPoint)) + { + return (idx, windowState.Window); + } + + idx++; + } + + return null; } } diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index c6903007a..69ae27230 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -8,14 +8,24 @@ public static class SliceLayouts /// Creates a primary stack layout, where the first window takes up half the screen, and the /// remaining windows are stacked vertically on the other half. ///
+ /// /// /// /// - public static ILayoutEngine CreatePrimaryStackLayout(ISliceLayoutPlugin plugin, LayoutEngineIdentity identity) => + public static ILayoutEngine CreatePrimaryStackLayout( + IContext context, + ISliceLayoutPlugin plugin, + LayoutEngineIdentity identity + ) => new SliceLayoutEngine( + context, plugin, identity, - new ParentArea(isHorizontal: true, (0.5, new SliceArea(maxChildren: 1, order: 0)), (0.5, new BaseArea())) + new ParentArea( + isHorizontal: true, + (0.5, new SliceArea(maxChildren: 1, order: 0)), + (0.5, new OverflowArea()) + ) ); /// @@ -24,12 +34,14 @@ public static ILayoutEngine CreatePrimaryStackLayout(ISliceLayoutPlugin plugin, /// For example, new uint[] { 2, 1, 0 } will create a layout with 3 columns, where the /// first column has 2 rows, the second column has 1 row, and the third column has infinite rows. /// + /// The to use /// The to use /// The identity of the layout engine /// The number of rows in each column /// /// public static ILayoutEngine CreateMultiColumnLayout( + IContext context, ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, params uint[] capacities @@ -38,19 +50,19 @@ params uint[] capacities double weight = 1.0 / capacities.Length; (double, IArea)[] areas = new (double, IArea)[capacities.Length]; - bool createdBaseArea = false; + bool createdOverflow = false; for (int idx = 0; idx < capacities.Length; idx++) { uint capacity = capacities[idx]; if (capacity == 0) { - if (createdBaseArea) + if (createdOverflow) { throw new ArgumentException("Cannot have multiple base areas"); } - areas[idx] = (weight, new BaseArea()); - createdBaseArea = true; + areas[idx] = (weight, new OverflowArea()); + createdOverflow = true; } else { @@ -58,6 +70,6 @@ params uint[] capacities } } - return new SliceLayoutEngine(plugin, identity, new ParentArea(isHorizontal: true, areas)); + return new SliceLayoutEngine(context, plugin, identity, new ParentArea(isHorizontal: true, areas)); } } From 9682f3031546bce42ebf9f017507e876adbfedf2 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 11:27:29 +1300 Subject: [PATCH 09/64] Set `OverflowArea.IsHorizontal` --- src/Whim.SliceLayout/Area.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 1d4f97996..80edefbfd 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -70,7 +70,13 @@ public SliceArea(uint order = 0, uint maxChildren = 1, bool isHorizontal = true) } } -internal record OverflowArea : BaseSliceArea { } +internal record OverflowArea : BaseSliceArea +{ + public OverflowArea(bool isHorizontal = true) + { + IsHorizontal = isHorizontal; + } +} internal static class AreaHelpers { From 55043d033bb5ae4d76eb617f49a86e5043e033a8 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 17:11:07 +1300 Subject: [PATCH 10/64] Working PrimaryStackLayout --- src/Whim.SliceLayout/Area.cs | 171 +++++++++++++++++++--- src/Whim.SliceLayout/SliceLayoutEngine.cs | 1 + src/Whim.SliceLayout/SliceLayouts.cs | 8 +- 3 files changed, 151 insertions(+), 29 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 80edefbfd..7dac7566e 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; namespace Whim.SliceLayout; @@ -10,12 +11,12 @@ public interface IArea /// When , the are arranged horizontally. /// Otherwise, they are arranged vertically. /// - bool IsHorizontal { get; } + bool IsRow { get; } } public abstract record BaseArea : IArea { - public bool IsHorizontal { get; protected set; } + public bool IsRow { get; protected set; } } public record ParentArea : BaseArea @@ -24,9 +25,9 @@ public record ParentArea : BaseArea public ImmutableList Children { get; } - public ParentArea(bool isHorizontal = true, params (double Weight, IArea Child)[] children) + public ParentArea(bool isRow, params (double Weight, IArea Child)[] children) { - IsHorizontal = isHorizontal; + IsRow = isRow; ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); ImmutableList.Builder childrenBuilder = ImmutableList.CreateBuilder(); @@ -40,9 +41,9 @@ public ParentArea(bool isHorizontal = true, params (double Weight, IArea Child)[ Children = childrenBuilder.ToImmutable(); } - internal ParentArea(bool isHorizontal, ImmutableList weights, ImmutableList children) + internal ParentArea(bool isRow, ImmutableList weights, ImmutableList children) { - IsHorizontal = isHorizontal; + IsRow = isRow; Weights = weights; Children = children; } @@ -50,7 +51,7 @@ internal ParentArea(bool isHorizontal, ImmutableList weights, ImmutableL public record BaseSliceArea : BaseArea { - public uint StartIndex { get; } + internal uint StartIndex { get; set; } } public record SliceArea : BaseSliceArea @@ -62,19 +63,19 @@ public record SliceArea : BaseSliceArea public uint MaxChildren { get; } - public SliceArea(uint order = 0, uint maxChildren = 1, bool isHorizontal = true) + public SliceArea(uint order = 0, uint maxChildren = 1, bool isRow = false) { Order = order; MaxChildren = maxChildren; - IsHorizontal = isHorizontal; + IsRow = isRow; } } internal record OverflowArea : BaseSliceArea { - public OverflowArea(bool isHorizontal = true) + public OverflowArea(bool isRow = false) { - IsHorizontal = isHorizontal; + IsRow = isRow; } } @@ -88,9 +89,13 @@ internal static class AreaHelpers /// public static ParentArea Prune(this ParentArea area, int windowCount) { + // Set the start indexes of the areas. + SetStartIndexes(area); + ImmutableList.Builder childrenBuilder = ImmutableList.CreateBuilder(); ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); + double ignoredWeight = 0; for (int i = 0; i < area.Children.Count; i++) { IArea child = area.Children[i]; @@ -99,30 +104,150 @@ public static ParentArea Prune(this ParentArea area, int windowCount) parentArea = parentArea.Prune(windowCount); if (parentArea.Children.Count == 0) { + ignoredWeight += area.Weights[i]; continue; } } else if (child is BaseSliceArea baseSliceArea) { - if (baseSliceArea.StartIndex >= windowCount) + if ( + baseSliceArea.StartIndex >= windowCount + || (baseSliceArea is SliceArea sliceArea && sliceArea.MaxChildren == 0) + ) { + ignoredWeight += area.Weights[i]; continue; } + } + + childrenBuilder.Add(child); + weightsBuilder.Add(area.Weights[i]); + } + + // Redistribute the weight of the ignored children to the remaining children. + if (ignoredWeight > 0) + { + double redistributedWeight = ignoredWeight / childrenBuilder.Count; + for (int i = 0; i < weightsBuilder.Count; i++) + { + weightsBuilder[i] += redistributedWeight; + } + } + + return new ParentArea(area.IsRow, weightsBuilder.ToImmutable(), childrenBuilder.ToImmutable()); + } - if (baseSliceArea is SliceArea sliceArea && sliceArea.MaxChildren == 0) + private static void SetStartIndexes(this ParentArea area) + { + if (area.Children.Count == 0) + { + return; + } + + List sliceAreas = new(); + List parentAreas = new(); + OverflowArea? overflowArea = null; + + // Iterate through the tree and add the areas to the list. + Queue areas = new(); + areas.Enqueue(area); + + while (areas.Count > 0) + { + IArea currArea = areas.Dequeue(); + + if (currArea is ParentArea parentArea) + { + for (int i = 0; i < parentArea.Children.Count; i++) { - continue; + areas.Enqueue(parentArea.Children[i]); } - childrenBuilder.Add(child); - weightsBuilder.Add(area.Weights[i]); + parentAreas.Add(parentArea); } + else if (currArea is SliceArea sliceArea) + { + sliceAreas.Add(sliceArea); + } + else if (currArea is OverflowArea currOverflowArea) + { + overflowArea = currOverflowArea; + } + } - childrenBuilder.Add(child); - weightsBuilder.Add(area.Weights[i]); + // Sort the areas by order. + sliceAreas.Sort((a, b) => a.Order.CompareTo(b.Order)); + + // Set the start indexes. + uint currIdx = 0; + foreach (SliceArea sliceArea in sliceAreas) + { + sliceArea.StartIndex = currIdx; + currIdx += sliceArea.MaxChildren; + } + + if (overflowArea is null) + { + Logger.Error($"No overflow area found, replacing last slice area with overflow area"); + overflowArea = ReplaceLastSliceAreaWithOverflowArea(area, sliceAreas[^1].Order); + } + + if (overflowArea is not null) + { + SliceArea lastSlice = sliceAreas[^1]; + overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; + } + } + + private static OverflowArea? ReplaceLastSliceAreaWithOverflowArea(ParentArea rootArea, uint lastSliceOrder) + { + // DFS + List areaStack = new() { rootArea }; + + while (areaStack.Count > 0) + { + ParentArea currParentArea = (ParentArea)areaStack[^1]; + areaStack.RemoveAt(areaStack.Count - 1); + + // Go over each of the children. If the child is the last slice, replace it. + // Otherwise, add it to the stack. + for (int i = 0; i < currParentArea.Children.Count; i++) + { + IArea child = currParentArea.Children[i]; + if (child is ParentArea childParentArea) + { + areaStack.Add(childParentArea); + } + else if (child is SliceArea sliceArea && sliceArea.Order == lastSliceOrder) + { + OverflowArea newOverflow = new(sliceArea.IsRow); + areaStack.Add(newOverflow); + RebuildWithOverflowArea(areaStack); + return newOverflow; + } + } + } + + Logger.Error("Could not replace last slice with overflow area"); + return null; + } + + private static ParentArea RebuildWithOverflowArea(List areaStack) + { + for (int idx = areaStack.Count - 2; idx >= 0; idx--) + { + ParentArea currArea = (ParentArea)areaStack[idx]; + IArea nextArea = areaStack[idx + 1]; + + ImmutableList currAreaChildren = currArea.Children; + currAreaChildren = currAreaChildren.SetItem(currAreaChildren.IndexOf(nextArea), areaStack[idx + 1]); + + ImmutableList currAreaWeights = currArea.Weights; + + areaStack[idx] = new ParentArea(currArea.IsRow, currAreaWeights, currAreaChildren); } - return new ParentArea(area.IsHorizontal, weightsBuilder.ToImmutable(), childrenBuilder.ToImmutable()); + return (ParentArea)areaStack[0]; } public static void DoParentLayout( @@ -147,7 +272,7 @@ ParentArea area double weight = area.Weights[currIdx]; IArea childArea = area.Children[currIdx]; - if (area.IsHorizontal) + if (area.IsRow) { width = Convert.ToInt32(rectangle.Width * weight); } @@ -167,7 +292,7 @@ ParentArea area items.DoSliceLayout(startIdx + currIdx, childRectangle, sliceArea); } - if (area.IsHorizontal) + if (area.IsRow) { x += width; } @@ -206,7 +331,7 @@ BaseSliceArea area } int maxIdx = startIdx + sliceItemsCount; - if (area.IsHorizontal) + if (area.IsRow) { deltaX = rectangle.Width / sliceItemsCount; width = deltaX; @@ -221,7 +346,7 @@ BaseSliceArea area { items[currIdx] = new SliceRectangleItem(currIdx, new Rectangle(x, y, width, height)); - if (area.IsHorizontal) + if (area.IsRow) { x += deltaX; } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 74594c0f6..0f1a394cf 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -60,6 +60,7 @@ public bool ContainsWindow(IWindow window) return _windows.Contains(window); } + // TODO: Cache layouts // TODO: Handle when areas are partially or completely empty. public IEnumerable DoLayout(IRectangle rectangle, IMonitor monitor) { diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index 69ae27230..c3474411f 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -21,11 +21,7 @@ LayoutEngineIdentity identity context, plugin, identity, - new ParentArea( - isHorizontal: true, - (0.5, new SliceArea(maxChildren: 1, order: 0)), - (0.5, new OverflowArea()) - ) + new ParentArea(isRow: true, (0.5, new SliceArea(maxChildren: 1, order: 0)), (0.5, new OverflowArea())) ); /// @@ -70,6 +66,6 @@ params uint[] capacities } } - return new SliceLayoutEngine(context, plugin, identity, new ParentArea(isHorizontal: true, areas)); + return new SliceLayoutEngine(context, plugin, identity, new ParentArea(isRow: true, areas)); } } From 5ac8309d857038b02565bd6a298a51b20b95b172 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 18:54:27 +1300 Subject: [PATCH 11/64] Fix indexing issue on MultiColumnLayout --- src/Whim.SliceLayout/Area.cs | 41 ++++++++++++++++------------ src/Whim.SliceLayout/SliceLayouts.cs | 1 + 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 7dac7566e..c4b551d23 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -250,16 +250,16 @@ private static ParentArea RebuildWithOverflowArea(List areaStack) return (ParentArea)areaStack[0]; } - public static void DoParentLayout( + public static int DoParentLayout( this SliceRectangleItem[] items, - int startIdx, + int windowStartIdx, IRectangle rectangle, ParentArea area ) { - if (startIdx >= items.Length) + if (windowStartIdx >= items.Length) { - return; + return windowStartIdx; } int x = rectangle.X; @@ -267,10 +267,11 @@ ParentArea area int width = rectangle.Width; int height = rectangle.Height; - for (int currIdx = 0; currIdx < area.Children.Count; currIdx++) + int windowCurrIdx = 0; + for (int childIdx = 0; childIdx < area.Children.Count; childIdx++) { - double weight = area.Weights[currIdx]; - IArea childArea = area.Children[currIdx]; + double weight = area.Weights[childIdx]; + IArea childArea = area.Children[childIdx]; if (area.IsRow) { @@ -285,11 +286,11 @@ ParentArea area if (childArea is ParentArea parentArea) { - items.DoParentLayout(startIdx + currIdx, childRectangle, parentArea); + windowCurrIdx = items.DoParentLayout(windowStartIdx + windowCurrIdx, childRectangle, parentArea); } else if (childArea is BaseSliceArea sliceArea) { - items.DoSliceLayout(startIdx + currIdx, childRectangle, sliceArea); + windowCurrIdx = items.DoSliceLayout(windowStartIdx + windowCurrIdx, childRectangle, sliceArea); } if (area.IsRow) @@ -301,18 +302,20 @@ ParentArea area y += height; } } + + return windowStartIdx + windowCurrIdx; } - public static void DoSliceLayout( + public static int DoSliceLayout( this SliceRectangleItem[] items, - int startIdx, + int windowIdx, IRectangle rectangle, BaseSliceArea area ) { - if (startIdx >= items.Length) + if (windowIdx >= items.Length) { - return; + return windowIdx; } int x = rectangle.X; @@ -323,13 +326,13 @@ BaseSliceArea area int deltaX = 0; int deltaY = 0; - int remainingItemsCount = items.Length - startIdx; + int remainingItemsCount = items.Length - windowIdx; int sliceItemsCount = remainingItemsCount; if (area is SliceArea sliceArea) { - sliceItemsCount = Convert.ToInt32(Math.Min(sliceArea.MaxChildren, remainingItemsCount)); + sliceItemsCount = Math.Min((int)sliceArea.MaxChildren, remainingItemsCount); } - int maxIdx = startIdx + sliceItemsCount; + int maxIdx = windowIdx + sliceItemsCount; if (area.IsRow) { @@ -342,9 +345,9 @@ BaseSliceArea area height = deltaY; } - for (int currIdx = startIdx; currIdx < maxIdx; currIdx++) + for (int windowCurrIdx = windowIdx; windowCurrIdx < maxIdx; windowCurrIdx++) { - items[currIdx] = new SliceRectangleItem(currIdx, new Rectangle(x, y, width, height)); + items[windowCurrIdx] = new SliceRectangleItem(windowCurrIdx, new Rectangle(x, y, width, height)); if (area.IsRow) { @@ -355,5 +358,7 @@ BaseSliceArea area y += deltaY; } } + + return maxIdx; } } diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index c3474411f..d569b333d 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -2,6 +2,7 @@ namespace Whim.SliceLayout; +// TODO: test custom layouts public static class SliceLayouts { /// From ff340b27541bcfe2cba68c3154f556273b26ede0 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 23:15:21 +1300 Subject: [PATCH 12/64] Cache layouts for a fake scale --- src/Whim.SliceLayout/Area.cs | 1 - src/Whim.SliceLayout/SliceLayoutEngine.cs | 47 ++++++++++++++++------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index c4b551d23..4a524a9b8 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -4,7 +4,6 @@ namespace Whim.SliceLayout; -// TODO: docs public interface IArea { /// diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 0f1a394cf..d92125906 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -6,6 +6,16 @@ namespace Whim.SliceLayout; internal record SliceRectangleItem(int Index, Rectangle Rectangle); +/// +/// A layout engine that divides the screen into "areas", which correspond to "slices" of a list +/// of windows. This can be used to accomplish a variety of layouts, including master-stack layouts. +/// +/// +/// The layout engine is a tree of s. Each is either a +/// , , or . +/// +/// Windows are assigned to s according to the . +/// public record SliceLayoutEngine : ILayoutEngine { private readonly IContext _context; @@ -19,6 +29,13 @@ public record SliceLayoutEngine : ILayoutEngine public LayoutEngineIdentity Identity { get; } + private const int _cachedWindowStatesScale = 10000; + + /// + /// Cheekily cache the window states with fake coordinates, to facilitate linear searching. + /// + private readonly IWindowState[] _cachedWindowStates; + private SliceLayoutEngine( IContext context, ISliceLayoutPlugin plugin, @@ -32,6 +49,18 @@ ParentArea rootArea Identity = identity; _windows = windows; _rootArea = rootArea; + + _cachedWindowStates = CreateCachedWindowStates(windows, rootArea).ToArray(); + } + + private IEnumerable CreateCachedWindowStates(ImmutableList windows, ParentArea rootArea) + { + Rectangle rectangle = new(0, 0, _cachedWindowStatesScale, _cachedWindowStatesScale); + + foreach (IWindowState windowState in DoLayout(rectangle, _context.MonitorManager.PrimaryMonitor)) + { + yield return windowState; + } } public SliceLayoutEngine( @@ -60,8 +89,6 @@ public bool ContainsWindow(IWindow window) return _windows.Contains(window); } - // TODO: Cache layouts - // TODO: Handle when areas are partially or completely empty. public IEnumerable DoLayout(IRectangle rectangle, IMonitor monitor) { Logger.Debug($"Doing layout on {rectangle} on {monitor}"); @@ -176,21 +203,15 @@ private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) { Logger.Debug($"Getting window at {point}"); - // This is the easy way - we DoLayout with fake coordinates, then linearly search for the - // window at the point, then swap or rotate. + Point scaledPoint = + new((int)(point.X * _cachedWindowStatesScale), (int)(point.Y * _cachedWindowStatesScale)); - Point scaledPoint = new((int)(point.X * 10000), (int)(point.Y * 10000)); - Rectangle rectangle = new(0, 0, 10000, 10000); - - int idx = 0; - foreach (IWindowState windowState in DoLayout(rectangle, _context.MonitorManager.PrimaryMonitor)) + for (int idx = 0; idx < _cachedWindowStates.Length; idx++) { - if (windowState.Rectangle.ContainsPoint(scaledPoint)) + if (_cachedWindowStates[idx].Rectangle.ContainsPoint(scaledPoint)) { - return (idx, windowState.Window); + return (idx, _cachedWindowStates[idx].Window); } - - idx++; } return null; From 7dab8e27ede8378fd7406cf298bfa971c5d9152b Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 23:29:37 +1300 Subject: [PATCH 13/64] FocusWindowInDirection --- src/Whim.SliceLayout/SliceLayoutEngine.cs | 46 ++++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index d92125906..71ba52e89 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -122,8 +122,50 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo public void FocusWindowInDirection(Direction direction, IWindow window) { - // TODO - throw new System.NotImplementedException(); + int index = _windows.IndexOf(window); + if (index == -1) + { + return; + } + + // Figure out the adjacent point of the window + IRectangle rect = _cachedWindowStates[index].Rectangle; + double x = rect.X; + double y = rect.Y; + + if (direction.HasFlag(Direction.Left)) + { + x -= 1d / _cachedWindowStatesScale; + } + else if (direction.HasFlag(Direction.Right)) + { + x += rect.Width + (1d / _cachedWindowStatesScale); + } + + if (direction.HasFlag(Direction.Up)) + { + y -= 1d / _cachedWindowStatesScale; + } + else if (direction.HasFlag(Direction.Down)) + { + y += rect.Height + (1d / _cachedWindowStatesScale); + } + + // Get the window at that point + foreach (IWindowState windowState in _cachedWindowStates) + { + if ( + windowState.Rectangle.ContainsPoint( + new Point((int)(x * _cachedWindowStatesScale), (int)(y * _cachedWindowStatesScale)) + ) + ) + { + windowState.Window.Focus(); + return; + } + } + + Logger.Debug($"No window found at {x}, {y}"); } public IWindow? GetFirstWindow() From 67bb7c2b4e2cba77f8eef749495bdf2ef9e129cc Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 23:38:38 +1300 Subject: [PATCH 14/64] SwapWindowInDirection --- src/Whim.SliceLayout/SliceLayoutEngine.cs | 24 ++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 71ba52e89..6d99ac260 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -31,6 +31,7 @@ public record SliceLayoutEngine : ILayoutEngine private const int _cachedWindowStatesScale = 10000; + // TODO: Make lazy instead of eager /// /// Cheekily cache the window states with fake coordinates, to facilitate linear searching. /// @@ -121,11 +122,17 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo } public void FocusWindowInDirection(Direction direction, IWindow window) + { + Logger.Debug($"Focusing window in direction {direction} from {window}"); + GetWindowInDirection(direction, window)?.Focus(); + } + + private IWindow? GetWindowInDirection(Direction direction, IWindow window) { int index = _windows.IndexOf(window); if (index == -1) { - return; + return null; } // Figure out the adjacent point of the window @@ -160,12 +167,12 @@ public void FocusWindowInDirection(Direction direction, IWindow window) ) ) { - windowState.Window.Focus(); - return; + return windowState.Window; } } Logger.Debug($"No window found at {x}, {y}"); + return null; } public IWindow? GetFirstWindow() @@ -194,8 +201,15 @@ public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) { - // TODO - throw new System.NotImplementedException(); + Logger.Debug($"Swapping {window} in direction {direction}"); + + IWindow? windowInDirection = GetWindowInDirection(direction, window); + if (windowInDirection == null) + { + return this; + } + + return MoveWindowToIndex(_windows.IndexOf(window), _windows.IndexOf(windowInDirection)); } private ILayoutEngine MoveWindowToIndex(int currentIndex, int targetIndex) From 9b7ca57769080b9a738eeb2fa7678ba0fd3d43ed Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 23:41:02 +1300 Subject: [PATCH 15/64] Move private methods to partial class --- src/Whim.SliceLayout/SliceLayoutEngine.cs | 111 +---------------- .../SliceLayoutEngineHelpers.cs | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+), 110 deletions(-) create mode 100644 src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 6d99ac260..3538029c7 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -16,7 +16,7 @@ internal record SliceRectangleItem(int Index, Rectangle Rectangle); /// /// Windows are assigned to s according to the . /// -public record SliceLayoutEngine : ILayoutEngine +public partial record SliceLayoutEngine : ILayoutEngine { private readonly IContext _context; private readonly ImmutableList _windows; @@ -127,54 +127,6 @@ public void FocusWindowInDirection(Direction direction, IWindow window) GetWindowInDirection(direction, window)?.Focus(); } - private IWindow? GetWindowInDirection(Direction direction, IWindow window) - { - int index = _windows.IndexOf(window); - if (index == -1) - { - return null; - } - - // Figure out the adjacent point of the window - IRectangle rect = _cachedWindowStates[index].Rectangle; - double x = rect.X; - double y = rect.Y; - - if (direction.HasFlag(Direction.Left)) - { - x -= 1d / _cachedWindowStatesScale; - } - else if (direction.HasFlag(Direction.Right)) - { - x += rect.Width + (1d / _cachedWindowStatesScale); - } - - if (direction.HasFlag(Direction.Up)) - { - y -= 1d / _cachedWindowStatesScale; - } - else if (direction.HasFlag(Direction.Down)) - { - y += rect.Height + (1d / _cachedWindowStatesScale); - } - - // Get the window at that point - foreach (IWindowState windowState in _cachedWindowStates) - { - if ( - windowState.Rectangle.ContainsPoint( - new Point((int)(x * _cachedWindowStatesScale), (int)(y * _cachedWindowStatesScale)) - ) - ) - { - return windowState.Window; - } - } - - Logger.Debug($"No window found at {x}, {y}"); - return null; - } - public IWindow? GetFirstWindow() { Logger.Debug($"Getting first window"); @@ -211,65 +163,4 @@ public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) return MoveWindowToIndex(_windows.IndexOf(window), _windows.IndexOf(windowInDirection)); } - - private ILayoutEngine MoveWindowToIndex(int currentIndex, int targetIndex) - { - if (_plugin.WindowInsertionType == WindowInsertionType.Swap) - { - return SwapWindowIndices(currentIndex, targetIndex); - } - - return RotateWindowIndices(currentIndex, targetIndex); - } - - private ILayoutEngine SwapWindowIndices(int currentIndex, int targetIndex) - { - Logger.Debug($"Swapping {currentIndex} and {targetIndex}"); - - if (currentIndex == targetIndex) - { - return this; - } - - IWindow currentWindow = _windows[currentIndex]; - IWindow targetWindow = _windows[targetIndex]; - - ImmutableList newWindows = _windows - .SetItem(currentIndex, targetWindow) - .SetItem(targetIndex, currentWindow); - - return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); - } - - private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) - { - Logger.Debug($"Rotating {currentIndex} and {targetIndex}"); - - if (currentIndex == targetIndex) - { - return this; - } - - IWindow currentWindow = _windows[currentIndex]; - ImmutableList newWindows = _windows.Insert(targetIndex, currentWindow).RemoveAt(currentIndex); - return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); - } - - private (int Index, IWindow Window)? GetWindowAtPoint(IPoint point) - { - Logger.Debug($"Getting window at {point}"); - - Point scaledPoint = - new((int)(point.X * _cachedWindowStatesScale), (int)(point.Y * _cachedWindowStatesScale)); - - for (int idx = 0; idx < _cachedWindowStates.Length; idx++) - { - if (_cachedWindowStates[idx].Rectangle.ContainsPoint(scaledPoint)) - { - return (idx, _cachedWindowStates[idx].Window); - } - } - - return null; - } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs new file mode 100644 index 000000000..70b851668 --- /dev/null +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -0,0 +1,115 @@ +using System.Collections.Immutable; + +namespace Whim.SliceLayout; + +public partial record SliceLayoutEngine +{ + private IWindow? GetWindowInDirection(Direction direction, IWindow window) + { + int index = _windows.IndexOf(window); + if (index == -1) + { + return null; + } + + // Figure out the adjacent point of the window + IRectangle rect = _cachedWindowStates[index].Rectangle; + double x = rect.X; + double y = rect.Y; + + if (direction.HasFlag(Direction.Left)) + { + x -= 1d / _cachedWindowStatesScale; + } + else if (direction.HasFlag(Direction.Right)) + { + x += rect.Width + (1d / _cachedWindowStatesScale); + } + + if (direction.HasFlag(Direction.Up)) + { + y -= 1d / _cachedWindowStatesScale; + } + else if (direction.HasFlag(Direction.Down)) + { + y += rect.Height + (1d / _cachedWindowStatesScale); + } + + // Get the window at that point + foreach (IWindowState windowState in _cachedWindowStates) + { + if ( + windowState.Rectangle.ContainsPoint( + new Point((int)(x * _cachedWindowStatesScale), (int)(y * _cachedWindowStatesScale)) + ) + ) + { + return windowState.Window; + } + } + + Logger.Debug($"No window found at {x}, {y}"); + return null; + } + + private ILayoutEngine MoveWindowToIndex(int currentIndex, int targetIndex) + { + if (_plugin.WindowInsertionType == WindowInsertionType.Swap) + { + return SwapWindowIndices(currentIndex, targetIndex); + } + + return RotateWindowIndices(currentIndex, targetIndex); + } + + private ILayoutEngine SwapWindowIndices(int currentIndex, int targetIndex) + { + Logger.Debug($"Swapping {currentIndex} and {targetIndex}"); + + if (currentIndex == targetIndex) + { + return this; + } + + IWindow currentWindow = _windows[currentIndex]; + IWindow targetWindow = _windows[targetIndex]; + + ImmutableList newWindows = _windows + .SetItem(currentIndex, targetWindow) + .SetItem(targetIndex, currentWindow); + + return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); + } + + private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) + { + Logger.Debug($"Rotating {currentIndex} and {targetIndex}"); + + if (currentIndex == targetIndex) + { + return this; + } + + IWindow currentWindow = _windows[currentIndex]; + ImmutableList newWindows = _windows.Insert(targetIndex, currentWindow).RemoveAt(currentIndex); + return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); + } + + private (int Index, IWindow Window)? GetWindowAtPoint(IPoint point) + { + Logger.Debug($"Getting window at {point}"); + + Point scaledPoint = + new((int)(point.X * _cachedWindowStatesScale), (int)(point.Y * _cachedWindowStatesScale)); + + for (int idx = 0; idx < _cachedWindowStates.Length; idx++) + { + if (_cachedWindowStates[idx].Rectangle.ContainsPoint(scaledPoint)) + { + return (idx, _cachedWindowStates[idx].Window); + } + } + + return null; + } +} From 8ba0b1a183da238cb7ff06bbc1988db55fe404ef Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Thu, 21 Dec 2023 23:48:57 +1300 Subject: [PATCH 16/64] Switch from eager to lazy --- src/Whim.SliceLayout/SliceLayoutEngine.cs | 17 ++-------- .../SliceLayoutEngineHelpers.cs | 33 ++++++++++++++++--- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 3538029c7..b8142877c 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -31,11 +31,12 @@ public partial record SliceLayoutEngine : ILayoutEngine private const int _cachedWindowStatesScale = 10000; - // TODO: Make lazy instead of eager /// /// Cheekily cache the window states with fake coordinates, to facilitate linear searching. + /// + /// NOTE: Do not access this directly - instead use /// - private readonly IWindowState[] _cachedWindowStates; + private IWindowState[]? _cachedWindowStates; private SliceLayoutEngine( IContext context, @@ -50,18 +51,6 @@ ParentArea rootArea Identity = identity; _windows = windows; _rootArea = rootArea; - - _cachedWindowStates = CreateCachedWindowStates(windows, rootArea).ToArray(); - } - - private IEnumerable CreateCachedWindowStates(ImmutableList windows, ParentArea rootArea) - { - Rectangle rectangle = new(0, 0, _cachedWindowStatesScale, _cachedWindowStatesScale); - - foreach (IWindowState windowState in DoLayout(rectangle, _context.MonitorManager.PrimaryMonitor)) - { - yield return windowState; - } } public SliceLayoutEngine( diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs index 70b851668..7a61d9811 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -4,6 +4,27 @@ namespace Whim.SliceLayout; public partial record SliceLayoutEngine { + private IWindowState[] GetLazyWindowStates() + { + if (_cachedWindowStates is not null) + { + return _cachedWindowStates; + } + + IWindowState[] cachedWindowStates = new IWindowState[_windows.Count]; + + Rectangle rectangle = new(0, 0, _cachedWindowStatesScale, _cachedWindowStatesScale); + int idx = 0; + foreach (IWindowState windowState in DoLayout(rectangle, _context.MonitorManager.PrimaryMonitor)) + { + cachedWindowStates[idx] = windowState; + idx++; + } + + _cachedWindowStates = cachedWindowStates; + return cachedWindowStates; + } + private IWindow? GetWindowInDirection(Direction direction, IWindow window) { int index = _windows.IndexOf(window); @@ -13,7 +34,8 @@ public partial record SliceLayoutEngine } // Figure out the adjacent point of the window - IRectangle rect = _cachedWindowStates[index].Rectangle; + IWindowState[] windowStates = GetLazyWindowStates(); + IRectangle rect = windowStates[index].Rectangle; double x = rect.X; double y = rect.Y; @@ -36,7 +58,7 @@ public partial record SliceLayoutEngine } // Get the window at that point - foreach (IWindowState windowState in _cachedWindowStates) + foreach (IWindowState windowState in windowStates) { if ( windowState.Rectangle.ContainsPoint( @@ -101,12 +123,13 @@ private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) Point scaledPoint = new((int)(point.X * _cachedWindowStatesScale), (int)(point.Y * _cachedWindowStatesScale)); + IWindowState[] windowStates = GetLazyWindowStates(); - for (int idx = 0; idx < _cachedWindowStates.Length; idx++) + for (int idx = 0; idx < windowStates.Length; idx++) { - if (_cachedWindowStates[idx].Rectangle.ContainsPoint(scaledPoint)) + if (windowStates[idx].Rectangle.ContainsPoint(scaledPoint)) { - return (idx, _cachedWindowStates[idx].Window); + return (idx, windowStates[idx].Window); } } From 9874a9c88b1d92c62d20edd77f0756e11b711632 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Fri, 22 Dec 2023 00:03:32 +1300 Subject: [PATCH 17/64] Add commands --- src/Whim.SliceLayout/SliceLayoutCommands.cs | 31 +++++++++++++++++++++ src/Whim.SliceLayout/SliceLayoutEngine.cs | 4 +-- src/Whim.SliceLayout/SliceLayoutPlugin.cs | 31 ++++++--------------- 3 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 src/Whim.SliceLayout/SliceLayoutCommands.cs diff --git a/src/Whim.SliceLayout/SliceLayoutCommands.cs b/src/Whim.SliceLayout/SliceLayoutCommands.cs new file mode 100644 index 000000000..90fec1b8b --- /dev/null +++ b/src/Whim.SliceLayout/SliceLayoutCommands.cs @@ -0,0 +1,31 @@ +namespace Whim.SliceLayout; + +/// +/// Commands for the . +/// +public class SliceLayoutCommands : PluginCommands +{ + private readonly ISliceLayoutPlugin _sliceLayoutPlugin; + + // TODO: Document in README + /// + /// Creates a new instance of the slice layout commands. + /// + /// + public SliceLayoutCommands(ISliceLayoutPlugin sliceLayoutPlugin) + : base(sliceLayoutPlugin.Name) + { + _sliceLayoutPlugin = sliceLayoutPlugin; + + Add( + identifier: "set_insertion_type.swap", + title: "Set slice insertion type to swap", + callback: () => _sliceLayoutPlugin.WindowInsertionType = WindowInsertionType.Swap + ) + .Add( + identifier: "set_insertion_type.rotate", + title: "Set slice insertion type to rotate", + callback: () => _sliceLayoutPlugin.WindowInsertionType = WindowInsertionType.Rotate + ); + } +} diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index b8142877c..44d385e75 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -124,8 +124,8 @@ public void FocusWindowInDirection(Direction direction, IWindow window) public ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint deltas, IWindow window) { - // TODO - throw new System.NotImplementedException(); + // TODO: Put issue reference here + return this; } public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) diff --git a/src/Whim.SliceLayout/SliceLayoutPlugin.cs b/src/Whim.SliceLayout/SliceLayoutPlugin.cs index 6bffb8284..423e9626f 100644 --- a/src/Whim.SliceLayout/SliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/SliceLayoutPlugin.cs @@ -6,29 +6,16 @@ public class SliceLayoutPlugin : ISliceLayoutPlugin { public string Name => "whim.leader_stack_layout"; - // TODO - public IPluginCommands PluginCommands => new PluginCommands(Name); + // TODO: Bar extension issue + public IPluginCommands PluginCommands => new SliceLayoutCommands(this); public WindowInsertionType WindowInsertionType { get; set; } - public void PreInitialize() - { - // TODO - } - - public void PostInitialize() - { - // TODO - } - - public void LoadState(JsonElement state) - { - // TODO - } - - public JsonElement? SaveState() - { - // TODO - return null; - } + public void PreInitialize() { } + + public void PostInitialize() { } + + public void LoadState(JsonElement state) { } + + public JsonElement? SaveState() => null; } From b1c148cb381e19af650efac4c17736b22ee24801 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Fri, 22 Dec 2023 16:41:26 +1300 Subject: [PATCH 18/64] Add PerformCustomAction --- src/Whim.Bar/BarLayoutEngine.cs | 4 +++ .../FloatingLayoutEngine.cs | 13 ++++++++ src/Whim.Gaps/GapsLayoutEngine.cs | 4 +++ src/Whim.TestUtils/TestLayoutEngine.cs | 3 ++ src/Whim.TreeLayout/TreeLayoutEngine.cs | 2 ++ src/Whim/Layout/BaseProxyLayoutEngine.cs | 2 ++ src/Whim/Layout/ColumnLayoutEngine.cs | 2 ++ src/Whim/Layout/ILayoutEngine.cs | 24 ++++++++++++++ src/Whim/Workspace/IWorkspace.cs | 18 +++++++++++ src/Whim/Workspace/Workspace.cs | 31 +++++++++++++++++++ 10 files changed, 103 insertions(+) diff --git a/src/Whim.Bar/BarLayoutEngine.cs b/src/Whim.Bar/BarLayoutEngine.cs index b7f4908ba..4b5714997 100644 --- a/src/Whim.Bar/BarLayoutEngine.cs +++ b/src/Whim.Bar/BarLayoutEngine.cs @@ -71,4 +71,8 @@ public override ILayoutEngine MoveWindowToPoint(IWindow window, IPoint p /// public override ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) => UpdateInner(InnerLayoutEngine.SwapWindowInDirection(direction, window)); + + /// + public override ILayoutEngine PerformCustomAction(string actionName, T args) => + UpdateInner(InnerLayoutEngine.PerformCustomAction(actionName, args)); } diff --git a/src/Whim.FloatingLayout/FloatingLayoutEngine.cs b/src/Whim.FloatingLayout/FloatingLayoutEngine.cs index ddc012167..4118c6b77 100644 --- a/src/Whim.FloatingLayout/FloatingLayoutEngine.cs +++ b/src/Whim.FloatingLayout/FloatingLayoutEngine.cs @@ -68,6 +68,15 @@ private FloatingLayoutEngine UpdateInner(ILayoutEngine newInnerLayoutEngine, IWi : new FloatingLayoutEngine(this, newInnerLayoutEngine, newFloatingWindowRects); } + /// + /// Returns a new instance of with the given inner layout engine, + /// Only use this for updates that do not involve a window. + /// + /// + /// + private FloatingLayoutEngine UpdateInner(ILayoutEngine newInnerLayoutEngine) => + InnerLayoutEngine == newInnerLayoutEngine ? this : new FloatingLayoutEngine(this, newInnerLayoutEngine); + /// public override ILayoutEngine AddWindow(IWindow window) { @@ -245,4 +254,8 @@ public override ILayoutEngine SwapWindowInDirection(Direction direction, IWindow /// public override bool ContainsWindow(IWindow window) => _floatingWindowRects.ContainsKey(window) || InnerLayoutEngine.ContainsWindow(window); + + /// + public override ILayoutEngine PerformCustomAction(string actionName, T args) => + UpdateInner(InnerLayoutEngine.PerformCustomAction(actionName, args)); } diff --git a/src/Whim.Gaps/GapsLayoutEngine.cs b/src/Whim.Gaps/GapsLayoutEngine.cs index 400fa694e..9f0ed4a90 100644 --- a/src/Whim.Gaps/GapsLayoutEngine.cs +++ b/src/Whim.Gaps/GapsLayoutEngine.cs @@ -116,4 +116,8 @@ public override ILayoutEngine MoveWindowToPoint(IWindow window, IPoint p /// public override ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) => UpdateInner(InnerLayoutEngine.SwapWindowInDirection(direction, window)); + + /// + public override ILayoutEngine PerformCustomAction(string actionName, T args) => + UpdateInner(InnerLayoutEngine.PerformCustomAction(actionName, args)); } diff --git a/src/Whim.TestUtils/TestLayoutEngine.cs b/src/Whim.TestUtils/TestLayoutEngine.cs index adc36361b..e717cbd7a 100644 --- a/src/Whim.TestUtils/TestLayoutEngine.cs +++ b/src/Whim.TestUtils/TestLayoutEngine.cs @@ -46,4 +46,7 @@ public ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint /// public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) => throw new NotImplementedException(); + + /// + public ILayoutEngine PerformCustomAction(string actionName, T args) => throw new NotImplementedException(); } diff --git a/src/Whim.TreeLayout/TreeLayoutEngine.cs b/src/Whim.TreeLayout/TreeLayoutEngine.cs index 1b05ff952..0f3e21cd5 100644 --- a/src/Whim.TreeLayout/TreeLayoutEngine.cs +++ b/src/Whim.TreeLayout/TreeLayoutEngine.cs @@ -684,4 +684,6 @@ WindowNode bNode ); return new TreeLayoutEngine(this, currentNode, newWindows); } + + public ILayoutEngine PerformCustomAction(string actionName, T args) => this; } diff --git a/src/Whim/Layout/BaseProxyLayoutEngine.cs b/src/Whim/Layout/BaseProxyLayoutEngine.cs index 283633382..048bd560a 100644 --- a/src/Whim/Layout/BaseProxyLayoutEngine.cs +++ b/src/Whim/Layout/BaseProxyLayoutEngine.cs @@ -76,6 +76,8 @@ protected BaseProxyLayoutEngine(ILayoutEngine innerLayoutEngine) /// public abstract IEnumerable DoLayout(IRectangle rectangle, IMonitor monitor); + public abstract ILayoutEngine PerformCustomAction(string actionName, T args); + /// /// Checks to see if this /// or a child layout engine is type . diff --git a/src/Whim/Layout/ColumnLayoutEngine.cs b/src/Whim/Layout/ColumnLayoutEngine.cs index 6798c3878..4d0c04577 100644 --- a/src/Whim/Layout/ColumnLayoutEngine.cs +++ b/src/Whim/Layout/ColumnLayoutEngine.cs @@ -235,4 +235,6 @@ private static int GetDelta(bool leftToRight, Direction direction) return direction == Direction.Left ? 1 : -1; } } + + public ILayoutEngine PerformCustomAction(string actionName, T args) => this; } diff --git a/src/Whim/Layout/ILayoutEngine.cs b/src/Whim/Layout/ILayoutEngine.cs index 2ec98e83e..bd1f5147c 100644 --- a/src/Whim/Layout/ILayoutEngine.cs +++ b/src/Whim/Layout/ILayoutEngine.cs @@ -118,6 +118,30 @@ public interface ILayoutEngine /// The new after the move. ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint deltas, IWindow window); + /// + /// A custom action handler for the given . + /// Each layout engine should handle the appropriately. + /// + /// + /// This method is used to handle custom actions that are not part of the + /// interface. + /// For example, the SliceLayoutEngine uses this method to promote/demote windows in the + /// window stack. + /// + /// + /// The type of . + /// + /// + /// The name of the action. This should be unique to the layout engine type. + /// + /// + /// The payload of the action, which the handler can use to perform the action. + /// + /// + /// A new layout engine if the action is handled, otherwise it returns the current layout engine. + /// + ILayoutEngine PerformCustomAction(string actionName, T args); + /// /// Checks to see if this or a child layout engine is type /// . diff --git a/src/Whim/Workspace/IWorkspace.cs b/src/Whim/Workspace/IWorkspace.cs index e232bf64c..bf1038b01 100644 --- a/src/Whim/Workspace/IWorkspace.cs +++ b/src/Whim/Workspace/IWorkspace.cs @@ -149,4 +149,22 @@ public interface IWorkspace : IDisposable /// The point to move the window to. void MoveWindowToPoint(IWindow window, IPoint point); #endregion + + /// + /// Performs a custom action in a layout engine. + /// + /// + /// Layout engines need to handle the custom action in . + /// For more, see . + /// + /// + /// The type of . + /// + /// + /// The name of the action. This should be unique to the layout engine type. + /// + /// + /// The payload of the action, which the handler can use to perform the action. + /// + void PerformCustomLayoutEngineAction(string actionName, T args); } diff --git a/src/Whim/Workspace/Workspace.cs b/src/Whim/Workspace/Workspace.cs index 298cc6998..98e366211 100644 --- a/src/Whim/Workspace/Workspace.cs +++ b/src/Whim/Workspace/Workspace.cs @@ -657,6 +657,37 @@ private bool GarbageCollect() return garbageCollected; } + public void PerformCustomLayoutEngineAction(string actionname, T args) + { + Logger.Debug($"Attempting to perform custom layout engine action {actionname} for workspace {Name}"); + + bool doLayout = false; + for (int idx = 0; idx < _layoutEngines.Length; idx++) + { + ILayoutEngine oldEngine = _layoutEngines[idx]; + ILayoutEngine newEngine = oldEngine.PerformCustomAction(actionname, args); + + if (newEngine.Equals(oldEngine)) + { + Logger.Debug($"Layout engine {oldEngine} could not perform action {actionname}"); + } + else + { + _layoutEngines[idx] = newEngine; + + if (oldEngine == ActiveLayoutEngine) + { + doLayout = true; + } + } + } + + if (doLayout) + { + DoLayout(); + } + } + protected virtual void Dispose(bool disposing) { if (!_disposedValue) From 0c8e2fc985ff90f9261a217410a4c027d9e12cd5 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Fri, 22 Dec 2023 16:41:48 +1300 Subject: [PATCH 19/64] Add PerformCustomAction for SliceLayoutEngine --- src/Whim.SliceLayout/SliceLayoutEngine.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 44d385e75..be04fbf92 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -152,4 +152,10 @@ public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) return MoveWindowToIndex(_windows.IndexOf(window), _windows.IndexOf(windowInDirection)); } + + public ILayoutEngine PerformCustomAction(string actionName, T args) + { + // TODO + return this; + } } From 3c966f0e0d7a695e364ebf97e32fccd0c99e0e3c Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Fri, 22 Dec 2023 17:27:53 +1300 Subject: [PATCH 20/64] Promote and demote commands --- src/Whim.SliceLayout/Area.cs | 32 +++++-- src/Whim.SliceLayout/ISliceLayoutPlugin.cs | 34 +++++++ src/Whim.SliceLayout/SliceLayoutCommands.cs | 12 ++- src/Whim.SliceLayout/SliceLayoutEngine.cs | 91 ++++++++++++++++++- .../SliceLayoutEngineHelpers.cs | 9 +- src/Whim.SliceLayout/SliceLayoutPlugin.cs | 37 +++++++- 6 files changed, 197 insertions(+), 18 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 4a524a9b8..13787b9e2 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -60,6 +60,9 @@ public record SliceArea : BaseSliceArea /// public uint Order { get; } + /// + /// Maximum number of children this area can have. This must be a non-negative integer. + /// public uint MaxChildren { get; } public SliceArea(uint order = 0, uint maxChildren = 1, bool isRow = false) @@ -88,9 +91,6 @@ internal static class AreaHelpers /// public static ParentArea Prune(this ParentArea area, int windowCount) { - // Set the start indexes of the areas. - SetStartIndexes(area); - ImmutableList.Builder childrenBuilder = ImmutableList.CreateBuilder(); ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); @@ -136,11 +136,18 @@ public static ParentArea Prune(this ParentArea area, int windowCount) return new ParentArea(area.IsRow, weightsBuilder.ToImmutable(), childrenBuilder.ToImmutable()); } - private static void SetStartIndexes(this ParentArea area) + /// + /// Set the start indexes of the areas. + /// + /// + /// + /// The areas which directly contain windows, in order. + /// + public static IArea[] SetStartIndexes(this ParentArea area) { if (area.Children.Count == 0) { - return; + return Array.Empty(); } List sliceAreas = new(); @@ -189,16 +196,21 @@ private static void SetStartIndexes(this ParentArea area) { Logger.Error($"No overflow area found, replacing last slice area with overflow area"); overflowArea = ReplaceLastSliceAreaWithOverflowArea(area, sliceAreas[^1].Order); - } - if (overflowArea is not null) - { SliceArea lastSlice = sliceAreas[^1]; + sliceAreas.RemoveAt(sliceAreas.Count - 1); overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; } + + IArea[] windowAreas = new IArea[sliceAreas.Count + 1]; + sliceAreas.CopyTo((SliceArea[])windowAreas); + windowAreas[^1] = overflowArea; + + return windowAreas; } - private static OverflowArea? ReplaceLastSliceAreaWithOverflowArea(ParentArea rootArea, uint lastSliceOrder) + // TODO: Test as public + private static OverflowArea ReplaceLastSliceAreaWithOverflowArea(ParentArea rootArea, uint lastSliceOrder) { // DFS List areaStack = new() { rootArea }; @@ -228,7 +240,7 @@ private static void SetStartIndexes(this ParentArea area) } Logger.Error("Could not replace last slice with overflow area"); - return null; + return null!; } private static ParentArea RebuildWithOverflowArea(List areaStack) diff --git a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs index bbcfd57b1..7f49f71c8 100644 --- a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs @@ -1,5 +1,8 @@ namespace Whim.SliceLayout; +/// +/// The type of insertion to use when adding a window to a slice. +/// public enum WindowInsertionType { /// @@ -15,5 +18,36 @@ public enum WindowInsertionType public interface ISliceLayoutPlugin : IPlugin { + /// + /// The name of the action that promotes a window in the stack to the next-higher slice. + /// + string PromoteActionName { get; } + + /// + /// The name of the action that demotes a window in the stack to the next-lower slice. + /// + string DemoteActionName { get; } + + /// + /// The type of insertion to use when adding a window to a slice. + /// WindowInsertionType WindowInsertionType { get; set; } + + /// + /// Promotes the given window in the stack. + /// + /// + /// The window to promote. If , then + /// is used. + /// + void PromoteWindowInStack(IWindow? window = null); + + /// + /// Demotes the given window in the stack. + /// + /// + /// The window to demote. If , then + /// is used. + /// + void DemoteWindowInStack(IWindow? window = null); } diff --git a/src/Whim.SliceLayout/SliceLayoutCommands.cs b/src/Whim.SliceLayout/SliceLayoutCommands.cs index 90fec1b8b..9c635b363 100644 --- a/src/Whim.SliceLayout/SliceLayoutCommands.cs +++ b/src/Whim.SliceLayout/SliceLayoutCommands.cs @@ -17,7 +17,7 @@ public SliceLayoutCommands(ISliceLayoutPlugin sliceLayoutPlugin) { _sliceLayoutPlugin = sliceLayoutPlugin; - Add( + _ = Add( identifier: "set_insertion_type.swap", title: "Set slice insertion type to swap", callback: () => _sliceLayoutPlugin.WindowInsertionType = WindowInsertionType.Swap @@ -26,6 +26,16 @@ public SliceLayoutCommands(ISliceLayoutPlugin sliceLayoutPlugin) identifier: "set_insertion_type.rotate", title: "Set slice insertion type to rotate", callback: () => _sliceLayoutPlugin.WindowInsertionType = WindowInsertionType.Rotate + ) + .Add( + identifier: "stack.promote", + title: "Promote window in stack", + callback: () => _sliceLayoutPlugin.PromoteWindowInStack() + ) + .Add( + identifier: "stack.demote", + title: "Demote window in stack", + callback: () => _sliceLayoutPlugin.DemoteWindowInStack() ); } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index be04fbf92..5773468e7 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -31,6 +31,12 @@ public partial record SliceLayoutEngine : ILayoutEngine private const int _cachedWindowStatesScale = 10000; + /// + /// The areas in the tree which contain windows, in order of the . + /// The last area is an . + /// + private readonly IArea[] _windowAreas; + /// /// Cheekily cache the window states with fake coordinates, to facilitate linear searching. /// @@ -51,6 +57,8 @@ ParentArea rootArea Identity = identity; _windows = windows; _rootArea = rootArea; + + _windowAreas = _rootArea.SetStartIndexes(); } public SliceLayoutEngine( @@ -88,7 +96,7 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo return Enumerable.Empty(); } - // Prune the empty areas from the tree + // Prune the empty areas from the tree. ParentArea prunedRootArea = _rootArea.Prune(_windows.Count); // Get the rectangles for each window @@ -153,9 +161,88 @@ public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) return MoveWindowToIndex(_windows.IndexOf(window), _windows.IndexOf(windowInDirection)); } + private ILayoutEngine PromoteWindowInStack(IWindow window) + { + Logger.Debug($"Promoting {window} in stack"); + + int windowIndex = _windows.IndexOf(window); + if (windowIndex == 0) + { + return this; + } + + // Find the area which contains the window. + int areaIndex = GetAreaStackForWindowIndex(windowIndex); + if (areaIndex <= 0) + { + return this; + } + + SliceArea targetArea = (SliceArea)_windowAreas[areaIndex - 1]; + int targetIndex = (int)(targetArea.StartIndex + targetArea.MaxChildren - 1); + + return MoveWindowToIndex(windowIndex, targetIndex); + } + + private ILayoutEngine DemoteWindowInStack(IWindow window) + { + Logger.Debug($"Demoting {window} in stack"); + + int windowIndex = _windows.IndexOf(window); + if (windowIndex == _windows.Count - 1) + { + return this; + } + + // Find the area which contains the window. + int areaIndex = GetAreaStackForWindowIndex(windowIndex); + if (areaIndex > _windowAreas.Length - 2 || areaIndex == -1) + { + return this; + } + + SliceArea targetArea = (SliceArea)_windowAreas[areaIndex + 1]; + int targetIndex = (int)targetArea.StartIndex; + + return MoveWindowToIndex(windowIndex, targetIndex); + } + + private int GetAreaStackForWindowIndex(int windowIndex) + { + int areaStartIndex = 0; + foreach (IArea area in _windowAreas) + { + if (area is SliceArea sliceArea) + { + int areaEndIndex = areaStartIndex + (int)sliceArea.MaxChildren; + if (windowIndex >= areaStartIndex && windowIndex < areaEndIndex) + { + return areaStartIndex; + } + + areaStartIndex = areaEndIndex; + } + + if (area is OverflowArea) + { + return areaStartIndex; + } + } + + return -1; + } + public ILayoutEngine PerformCustomAction(string actionName, T args) { - // TODO + if (actionName == _plugin.PromoteActionName && args is IWindow promoteWindow) + { + return PromoteWindowInStack(promoteWindow); + } + else if (actionName == _plugin.DemoteActionName && args is IWindow demoteWindow) + { + return DemoteWindowInStack(demoteWindow); + } + return this; } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs index 7a61d9811..dc9311205 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -39,22 +39,23 @@ private IWindowState[] GetLazyWindowStates() double x = rect.X; double y = rect.Y; + double delta = 1d / _cachedWindowStatesScale; if (direction.HasFlag(Direction.Left)) { - x -= 1d / _cachedWindowStatesScale; + x -= delta; } else if (direction.HasFlag(Direction.Right)) { - x += rect.Width + (1d / _cachedWindowStatesScale); + x += rect.Width + delta; } if (direction.HasFlag(Direction.Up)) { - y -= 1d / _cachedWindowStatesScale; + y -= delta; } else if (direction.HasFlag(Direction.Down)) { - y += rect.Height + (1d / _cachedWindowStatesScale); + y += rect.Height + delta; } // Get the window at that point diff --git a/src/Whim.SliceLayout/SliceLayoutPlugin.cs b/src/Whim.SliceLayout/SliceLayoutPlugin.cs index 423e9626f..ee6ff43e6 100644 --- a/src/Whim.SliceLayout/SliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/SliceLayoutPlugin.cs @@ -4,7 +4,18 @@ namespace Whim.SliceLayout; public class SliceLayoutPlugin : ISliceLayoutPlugin { - public string Name => "whim.leader_stack_layout"; + private readonly IContext _context; + + public SliceLayoutPlugin(IContext context) + { + _context = context; + } + + public string Name => "whim.slice_layout"; + + public string PromoteActionName => $"{Name}.stack.promote"; + + public string DemoteActionName => $"{Name}.stack.demote"; // TODO: Bar extension issue public IPluginCommands PluginCommands => new SliceLayoutCommands(this); @@ -18,4 +29,28 @@ public void PostInitialize() { } public void LoadState(JsonElement state) { } public JsonElement? SaveState() => null; + + private void ChangeWindowRank(IWindow? window, bool promote) + { + window ??= _context.WorkspaceManager.ActiveWorkspace.LastFocusedWindow; + + if (window is null) + { + Logger.Debug("No window to change rank for"); + return; + } + + IWorkspace? workspace = _context.WorkspaceManager.GetWorkspaceForWindow(window); + if (workspace is null) + { + Logger.Debug("Window is not in a workspace"); + return; + } + + workspace.PerformCustomLayoutEngineAction(promote ? PromoteActionName : DemoteActionName, window); + } + + public void PromoteWindowInStack(IWindow? window = null) => ChangeWindowRank(window, promote: true); + + public void DemoteWindowInStack(IWindow? window = null) => ChangeWindowRank(window, promote: false); } From de13abdbbc67a8311c93d2815c77a29c27f092fc Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Fri, 22 Dec 2023 18:21:53 +1300 Subject: [PATCH 21/64] Fix promotion/demotion --- src/Whim.SliceLayout/Area.cs | 17 +++++++------ src/Whim.SliceLayout/SliceLayoutEngine.cs | 29 ++++++++++++----------- src/Whim/Workspace/Workspace.cs | 2 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 13787b9e2..112171cd9 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -143,11 +143,11 @@ public static ParentArea Prune(this ParentArea area, int windowCount) /// /// The areas which directly contain windows, in order. /// - public static IArea[] SetStartIndexes(this ParentArea area) + public static BaseSliceArea[] SetStartIndexes(this ParentArea area) { if (area.Children.Count == 0) { - return Array.Empty(); + return Array.Empty(); } List sliceAreas = new(); @@ -196,14 +196,17 @@ public static IArea[] SetStartIndexes(this ParentArea area) { Logger.Error($"No overflow area found, replacing last slice area with overflow area"); overflowArea = ReplaceLastSliceAreaWithOverflowArea(area, sliceAreas[^1].Order); - - SliceArea lastSlice = sliceAreas[^1]; sliceAreas.RemoveAt(sliceAreas.Count - 1); - overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; } - IArea[] windowAreas = new IArea[sliceAreas.Count + 1]; - sliceAreas.CopyTo((SliceArea[])windowAreas); + SliceArea lastSlice = sliceAreas[^1]; + overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; + + BaseSliceArea[] windowAreas = new BaseSliceArea[sliceAreas.Count + 1]; + for (int i = 0; i < sliceAreas.Count; i++) + { + windowAreas[i] = sliceAreas[i]; + } windowAreas[^1] = overflowArea; return windowAreas; diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 5773468e7..452a5b993 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -35,7 +35,7 @@ public partial record SliceLayoutEngine : ILayoutEngine /// The areas in the tree which contain windows, in order of the . /// The last area is an . /// - private readonly IArea[] _windowAreas; + private readonly BaseSliceArea[] _windowAreas; /// /// Cheekily cache the window states with fake coordinates, to facilitate linear searching. @@ -44,6 +44,11 @@ public partial record SliceLayoutEngine : ILayoutEngine /// private IWindowState[]? _cachedWindowStates; + /// + /// The root area, with any empty areas pruned. + /// + private readonly ParentArea _prunedRootArea; + private SliceLayoutEngine( IContext context, ISliceLayoutPlugin plugin, @@ -59,6 +64,7 @@ ParentArea rootArea _rootArea = rootArea; _windowAreas = _rootArea.SetStartIndexes(); + _prunedRootArea = _rootArea.Prune(_windows.Count); } public SliceLayoutEngine( @@ -96,12 +102,9 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo return Enumerable.Empty(); } - // Prune the empty areas from the tree. - ParentArea prunedRootArea = _rootArea.Prune(_windows.Count); - // Get the rectangles for each window SliceRectangleItem[] items = new SliceRectangleItem[_windows.Count]; - items.DoParentLayout(0, rectangle, prunedRootArea); + items.DoParentLayout(0, rectangle, _prunedRootArea); // Get the window states IWindowState[] windowStates = new IWindowState[_windows.Count]; @@ -201,7 +204,7 @@ private ILayoutEngine DemoteWindowInStack(IWindow window) return this; } - SliceArea targetArea = (SliceArea)_windowAreas[areaIndex + 1]; + BaseSliceArea targetArea = _windowAreas[areaIndex + 1]; int targetIndex = (int)targetArea.StartIndex; return MoveWindowToIndex(windowIndex, targetIndex); @@ -209,23 +212,21 @@ private ILayoutEngine DemoteWindowInStack(IWindow window) private int GetAreaStackForWindowIndex(int windowIndex) { - int areaStartIndex = 0; - foreach (IArea area in _windowAreas) + for (int idx = 0; idx < _windowAreas.Length; idx++) { + IArea area = _windowAreas[idx]; if (area is SliceArea sliceArea) { - int areaEndIndex = areaStartIndex + (int)sliceArea.MaxChildren; - if (windowIndex >= areaStartIndex && windowIndex < areaEndIndex) + int areaEndIndex = (int)(sliceArea.StartIndex + sliceArea.MaxChildren); + if (windowIndex >= sliceArea.StartIndex && windowIndex < areaEndIndex) { - return areaStartIndex; + return idx; } - - areaStartIndex = areaEndIndex; } if (area is OverflowArea) { - return areaStartIndex; + return idx; } } diff --git a/src/Whim/Workspace/Workspace.cs b/src/Whim/Workspace/Workspace.cs index 98e366211..64b962f67 100644 --- a/src/Whim/Workspace/Workspace.cs +++ b/src/Whim/Workspace/Workspace.cs @@ -675,7 +675,7 @@ public void PerformCustomLayoutEngineAction(string actionname, T args) { _layoutEngines[idx] = newEngine; - if (oldEngine == ActiveLayoutEngine) + if (oldEngine.Identity == ActiveLayoutEngine.Identity) { doLayout = true; } From 21187f48fea437091eb18b908e0c12d1181077c2 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Fri, 22 Dec 2023 18:36:27 +1300 Subject: [PATCH 22/64] Added empty test project --- Whim.sln | 14 +++++ .../Whim.SliceLayout.Tests.csproj | 53 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/Whim.SliceLayout.Tests/Whim.SliceLayout.Tests.csproj diff --git a/Whim.sln b/Whim.sln index 686569c97..2c7ac92f4 100644 --- a/Whim.sln +++ b/Whim.sln @@ -64,6 +64,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Updater.Tests", "src\W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.SliceLayout", "src\Whim.SliceLayout\Whim.SliceLayout.csproj", "{38946E3C-F336-450F-8159-164AD14C025B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Whim.SliceLayout.Tests", "src\Whim.SliceLayout.Tests\Whim.SliceLayout.Tests.csproj", "{0C32EB78-44C7-4397-8643-2E1C1F3442E9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -366,6 +368,18 @@ Global {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.Build.0 = Release|Any CPU {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.ActiveCfg = Release|Any CPU {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.Build.0 = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.ActiveCfg = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.Build.0 = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|x64.Build.0 = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|Any CPU.Build.0 = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.ActiveCfg = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.Build.0 = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.ActiveCfg = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Whim.SliceLayout.Tests/Whim.SliceLayout.Tests.csproj b/src/Whim.SliceLayout.Tests/Whim.SliceLayout.Tests.csproj new file mode 100644 index 000000000..d22915225 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/Whim.SliceLayout.Tests.csproj @@ -0,0 +1,53 @@ + + + + All + All + None + All + All + All + All + All + All + All + All + All + Isaac Daly + true + An extensible window manager for Windows. + true + true + true + enable + enable + x64;arm64;Any CPU + net7.0-windows10.0.19041.0 + 0.2.0 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + \ No newline at end of file From 65c3d686f9ebcde2b636e1c5d1c40e8003e965f4 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Fri, 22 Dec 2023 23:09:56 +1300 Subject: [PATCH 23/64] Move AreaHelpers to its own file --- src/Whim.SliceLayout/Area.cs | 298 --------------------------- src/Whim.SliceLayout/AreaHelpers.cs | 301 ++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+), 298 deletions(-) create mode 100644 src/Whim.SliceLayout/AreaHelpers.cs diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 112171cd9..ea3f8815d 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; namespace Whim.SliceLayout; @@ -80,299 +78,3 @@ public OverflowArea(bool isRow = false) IsRow = isRow; } } - -internal static class AreaHelpers -{ - /// - /// Prune the tree of empty areas. - /// - /// - /// - /// - public static ParentArea Prune(this ParentArea area, int windowCount) - { - ImmutableList.Builder childrenBuilder = ImmutableList.CreateBuilder(); - ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); - - double ignoredWeight = 0; - for (int i = 0; i < area.Children.Count; i++) - { - IArea child = area.Children[i]; - if (child is ParentArea parentArea) - { - parentArea = parentArea.Prune(windowCount); - if (parentArea.Children.Count == 0) - { - ignoredWeight += area.Weights[i]; - continue; - } - } - else if (child is BaseSliceArea baseSliceArea) - { - if ( - baseSliceArea.StartIndex >= windowCount - || (baseSliceArea is SliceArea sliceArea && sliceArea.MaxChildren == 0) - ) - { - ignoredWeight += area.Weights[i]; - continue; - } - } - - childrenBuilder.Add(child); - weightsBuilder.Add(area.Weights[i]); - } - - // Redistribute the weight of the ignored children to the remaining children. - if (ignoredWeight > 0) - { - double redistributedWeight = ignoredWeight / childrenBuilder.Count; - for (int i = 0; i < weightsBuilder.Count; i++) - { - weightsBuilder[i] += redistributedWeight; - } - } - - return new ParentArea(area.IsRow, weightsBuilder.ToImmutable(), childrenBuilder.ToImmutable()); - } - - /// - /// Set the start indexes of the areas. - /// - /// - /// - /// The areas which directly contain windows, in order. - /// - public static BaseSliceArea[] SetStartIndexes(this ParentArea area) - { - if (area.Children.Count == 0) - { - return Array.Empty(); - } - - List sliceAreas = new(); - List parentAreas = new(); - OverflowArea? overflowArea = null; - - // Iterate through the tree and add the areas to the list. - Queue areas = new(); - areas.Enqueue(area); - - while (areas.Count > 0) - { - IArea currArea = areas.Dequeue(); - - if (currArea is ParentArea parentArea) - { - for (int i = 0; i < parentArea.Children.Count; i++) - { - areas.Enqueue(parentArea.Children[i]); - } - - parentAreas.Add(parentArea); - } - else if (currArea is SliceArea sliceArea) - { - sliceAreas.Add(sliceArea); - } - else if (currArea is OverflowArea currOverflowArea) - { - overflowArea = currOverflowArea; - } - } - - // Sort the areas by order. - sliceAreas.Sort((a, b) => a.Order.CompareTo(b.Order)); - - // Set the start indexes. - uint currIdx = 0; - foreach (SliceArea sliceArea in sliceAreas) - { - sliceArea.StartIndex = currIdx; - currIdx += sliceArea.MaxChildren; - } - - if (overflowArea is null) - { - Logger.Error($"No overflow area found, replacing last slice area with overflow area"); - overflowArea = ReplaceLastSliceAreaWithOverflowArea(area, sliceAreas[^1].Order); - sliceAreas.RemoveAt(sliceAreas.Count - 1); - } - - SliceArea lastSlice = sliceAreas[^1]; - overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; - - BaseSliceArea[] windowAreas = new BaseSliceArea[sliceAreas.Count + 1]; - for (int i = 0; i < sliceAreas.Count; i++) - { - windowAreas[i] = sliceAreas[i]; - } - windowAreas[^1] = overflowArea; - - return windowAreas; - } - - // TODO: Test as public - private static OverflowArea ReplaceLastSliceAreaWithOverflowArea(ParentArea rootArea, uint lastSliceOrder) - { - // DFS - List areaStack = new() { rootArea }; - - while (areaStack.Count > 0) - { - ParentArea currParentArea = (ParentArea)areaStack[^1]; - areaStack.RemoveAt(areaStack.Count - 1); - - // Go over each of the children. If the child is the last slice, replace it. - // Otherwise, add it to the stack. - for (int i = 0; i < currParentArea.Children.Count; i++) - { - IArea child = currParentArea.Children[i]; - if (child is ParentArea childParentArea) - { - areaStack.Add(childParentArea); - } - else if (child is SliceArea sliceArea && sliceArea.Order == lastSliceOrder) - { - OverflowArea newOverflow = new(sliceArea.IsRow); - areaStack.Add(newOverflow); - RebuildWithOverflowArea(areaStack); - return newOverflow; - } - } - } - - Logger.Error("Could not replace last slice with overflow area"); - return null!; - } - - private static ParentArea RebuildWithOverflowArea(List areaStack) - { - for (int idx = areaStack.Count - 2; idx >= 0; idx--) - { - ParentArea currArea = (ParentArea)areaStack[idx]; - IArea nextArea = areaStack[idx + 1]; - - ImmutableList currAreaChildren = currArea.Children; - currAreaChildren = currAreaChildren.SetItem(currAreaChildren.IndexOf(nextArea), areaStack[idx + 1]); - - ImmutableList currAreaWeights = currArea.Weights; - - areaStack[idx] = new ParentArea(currArea.IsRow, currAreaWeights, currAreaChildren); - } - - return (ParentArea)areaStack[0]; - } - - public static int DoParentLayout( - this SliceRectangleItem[] items, - int windowStartIdx, - IRectangle rectangle, - ParentArea area - ) - { - if (windowStartIdx >= items.Length) - { - return windowStartIdx; - } - - int x = rectangle.X; - int y = rectangle.Y; - int width = rectangle.Width; - int height = rectangle.Height; - - int windowCurrIdx = 0; - for (int childIdx = 0; childIdx < area.Children.Count; childIdx++) - { - double weight = area.Weights[childIdx]; - IArea childArea = area.Children[childIdx]; - - if (area.IsRow) - { - width = Convert.ToInt32(rectangle.Width * weight); - } - else - { - height = Convert.ToInt32(rectangle.Height * weight); - } - - Rectangle childRectangle = new(x, y, width, height); - - if (childArea is ParentArea parentArea) - { - windowCurrIdx = items.DoParentLayout(windowStartIdx + windowCurrIdx, childRectangle, parentArea); - } - else if (childArea is BaseSliceArea sliceArea) - { - windowCurrIdx = items.DoSliceLayout(windowStartIdx + windowCurrIdx, childRectangle, sliceArea); - } - - if (area.IsRow) - { - x += width; - } - else - { - y += height; - } - } - - return windowStartIdx + windowCurrIdx; - } - - public static int DoSliceLayout( - this SliceRectangleItem[] items, - int windowIdx, - IRectangle rectangle, - BaseSliceArea area - ) - { - if (windowIdx >= items.Length) - { - return windowIdx; - } - - int x = rectangle.X; - int y = rectangle.Y; - int width = rectangle.Width; - int height = rectangle.Height; - - int deltaX = 0; - int deltaY = 0; - - int remainingItemsCount = items.Length - windowIdx; - int sliceItemsCount = remainingItemsCount; - if (area is SliceArea sliceArea) - { - sliceItemsCount = Math.Min((int)sliceArea.MaxChildren, remainingItemsCount); - } - int maxIdx = windowIdx + sliceItemsCount; - - if (area.IsRow) - { - deltaX = rectangle.Width / sliceItemsCount; - width = deltaX; - } - else - { - deltaY = rectangle.Height / sliceItemsCount; - height = deltaY; - } - - for (int windowCurrIdx = windowIdx; windowCurrIdx < maxIdx; windowCurrIdx++) - { - items[windowCurrIdx] = new SliceRectangleItem(windowCurrIdx, new Rectangle(x, y, width, height)); - - if (area.IsRow) - { - x += deltaX; - } - else - { - y += deltaY; - } - } - - return maxIdx; - } -} diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs new file mode 100644 index 000000000..a65470eae --- /dev/null +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Whim.SliceLayout; + +internal static class AreaHelpers +{ + /// + /// Prune the tree of empty areas. + /// + /// + /// + /// + public static ParentArea Prune(this ParentArea area, int windowCount) + { + ImmutableList.Builder childrenBuilder = ImmutableList.CreateBuilder(); + ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); + + double ignoredWeight = 0; + for (int i = 0; i < area.Children.Count; i++) + { + IArea child = area.Children[i]; + if (child is ParentArea parentArea) + { + parentArea = parentArea.Prune(windowCount); + if (parentArea.Children.Count == 0) + { + ignoredWeight += area.Weights[i]; + continue; + } + } + else if (child is BaseSliceArea baseSliceArea) + { + if ( + baseSliceArea.StartIndex >= windowCount + || (baseSliceArea is SliceArea sliceArea && sliceArea.MaxChildren == 0) + ) + { + ignoredWeight += area.Weights[i]; + continue; + } + } + + childrenBuilder.Add(child); + weightsBuilder.Add(area.Weights[i]); + } + + // Redistribute the weight of the ignored children to the remaining children. + if (ignoredWeight > 0) + { + double redistributedWeight = ignoredWeight / childrenBuilder.Count; + for (int i = 0; i < weightsBuilder.Count; i++) + { + weightsBuilder[i] += redistributedWeight; + } + } + + return new ParentArea(area.IsRow, weightsBuilder.ToImmutable(), childrenBuilder.ToImmutable()); + } + + /// + /// Set the start indexes of the areas. + /// + /// + /// + /// The areas which directly contain windows, in order. + /// + public static BaseSliceArea[] SetStartIndexes(this ParentArea area) + { + if (area.Children.Count == 0) + { + return Array.Empty(); + } + + List sliceAreas = new(); + List parentAreas = new(); + OverflowArea? overflowArea = null; + + // Iterate through the tree and add the areas to the list. + Queue areas = new(); + areas.Enqueue(area); + + while (areas.Count > 0) + { + IArea currArea = areas.Dequeue(); + + if (currArea is ParentArea parentArea) + { + for (int i = 0; i < parentArea.Children.Count; i++) + { + areas.Enqueue(parentArea.Children[i]); + } + + parentAreas.Add(parentArea); + } + else if (currArea is SliceArea sliceArea) + { + sliceAreas.Add(sliceArea); + } + else if (currArea is OverflowArea currOverflowArea) + { + overflowArea = currOverflowArea; + } + } + + // Sort the areas by order. + sliceAreas.Sort((a, b) => a.Order.CompareTo(b.Order)); + + // Set the start indexes. + uint currIdx = 0; + foreach (SliceArea sliceArea in sliceAreas) + { + sliceArea.StartIndex = currIdx; + currIdx += sliceArea.MaxChildren; + } + + if (overflowArea is null) + { + Logger.Error($"No overflow area found, replacing last slice area with overflow area"); + overflowArea = ReplaceLastSliceAreaWithOverflowArea(area, sliceAreas[^1].Order); + sliceAreas.RemoveAt(sliceAreas.Count - 1); + } + + SliceArea lastSlice = sliceAreas[^1]; + overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; + + BaseSliceArea[] windowAreas = new BaseSliceArea[sliceAreas.Count + 1]; + for (int i = 0; i < sliceAreas.Count; i++) + { + windowAreas[i] = sliceAreas[i]; + } + windowAreas[^1] = overflowArea; + + return windowAreas; + } + + // TODO: Test as public + private static OverflowArea ReplaceLastSliceAreaWithOverflowArea(ParentArea rootArea, uint lastSliceOrder) + { + // DFS + List areaStack = new() { rootArea }; + + while (areaStack.Count > 0) + { + ParentArea currParentArea = (ParentArea)areaStack[^1]; + areaStack.RemoveAt(areaStack.Count - 1); + + // Go over each of the children. If the child is the last slice, replace it. + // Otherwise, add it to the stack. + for (int i = 0; i < currParentArea.Children.Count; i++) + { + IArea child = currParentArea.Children[i]; + if (child is ParentArea childParentArea) + { + areaStack.Add(childParentArea); + } + else if (child is SliceArea sliceArea && sliceArea.Order == lastSliceOrder) + { + OverflowArea newOverflow = new(sliceArea.IsRow); + areaStack.Add(newOverflow); + RebuildWithOverflowArea(areaStack); + return newOverflow; + } + } + } + + Logger.Error("Could not replace last slice with overflow area"); + return null!; + } + + private static ParentArea RebuildWithOverflowArea(List areaStack) + { + for (int idx = areaStack.Count - 2; idx >= 0; idx--) + { + ParentArea currArea = (ParentArea)areaStack[idx]; + IArea nextArea = areaStack[idx + 1]; + + ImmutableList currAreaChildren = currArea.Children; + currAreaChildren = currAreaChildren.SetItem(currAreaChildren.IndexOf(nextArea), areaStack[idx + 1]); + + ImmutableList currAreaWeights = currArea.Weights; + + areaStack[idx] = new ParentArea(currArea.IsRow, currAreaWeights, currAreaChildren); + } + + return (ParentArea)areaStack[0]; + } + + public static int DoParentLayout( + this SliceRectangleItem[] items, + int windowStartIdx, + IRectangle rectangle, + ParentArea area + ) + { + if (windowStartIdx >= items.Length) + { + return windowStartIdx; + } + + int x = rectangle.X; + int y = rectangle.Y; + int width = rectangle.Width; + int height = rectangle.Height; + + int windowCurrIdx = 0; + for (int childIdx = 0; childIdx < area.Children.Count; childIdx++) + { + double weight = area.Weights[childIdx]; + IArea childArea = area.Children[childIdx]; + + if (area.IsRow) + { + width = Convert.ToInt32(rectangle.Width * weight); + } + else + { + height = Convert.ToInt32(rectangle.Height * weight); + } + + Rectangle childRectangle = new(x, y, width, height); + + if (childArea is ParentArea parentArea) + { + windowCurrIdx = items.DoParentLayout(windowStartIdx + windowCurrIdx, childRectangle, parentArea); + } + else if (childArea is BaseSliceArea sliceArea) + { + windowCurrIdx = items.DoSliceLayout(windowStartIdx + windowCurrIdx, childRectangle, sliceArea); + } + + if (area.IsRow) + { + x += width; + } + else + { + y += height; + } + } + + return windowStartIdx + windowCurrIdx; + } + + public static int DoSliceLayout( + this SliceRectangleItem[] items, + int windowIdx, + IRectangle rectangle, + BaseSliceArea area + ) + { + if (windowIdx >= items.Length) + { + return windowIdx; + } + + int x = rectangle.X; + int y = rectangle.Y; + int width = rectangle.Width; + int height = rectangle.Height; + + int deltaX = 0; + int deltaY = 0; + + int remainingItemsCount = items.Length - windowIdx; + int sliceItemsCount = remainingItemsCount; + if (area is SliceArea sliceArea) + { + sliceItemsCount = Math.Min((int)sliceArea.MaxChildren, remainingItemsCount); + } + int maxIdx = windowIdx + sliceItemsCount; + + if (area.IsRow) + { + deltaX = rectangle.Width / sliceItemsCount; + width = deltaX; + } + else + { + deltaY = rectangle.Height / sliceItemsCount; + height = deltaY; + } + + for (int windowCurrIdx = windowIdx; windowCurrIdx < maxIdx; windowCurrIdx++) + { + items[windowCurrIdx] = new SliceRectangleItem(windowCurrIdx, new Rectangle(x, y, width, height)); + + if (area.IsRow) + { + x += deltaX; + } + else + { + y += deltaY; + } + } + + return maxIdx; + } +} From 42a671ed7c3a57641815ba79db34bd8beb36fc0f Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sat, 23 Dec 2023 14:35:53 +1300 Subject: [PATCH 24/64] SetStartIndexes tests --- .../AreaHelpers/SetStartIndexesTests.cs | 176 ++++++++++++++++++ .../GlobalSuppressions.cs | 17 ++ .../SampleSliceLayouts.cs | 66 +++++++ src/Whim.SliceLayout/AreaHelpers.cs | 104 ++++++----- src/Whim.SliceLayout/SliceLayoutEngine.cs | 3 +- src/Whim.SliceLayout/SliceLayouts.cs | 119 +++++++++++- 6 files changed, 423 insertions(+), 62 deletions(-) create mode 100644 src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs create mode 100644 src/Whim.SliceLayout.Tests/GlobalSuppressions.cs create mode 100644 src/Whim.SliceLayout.Tests/SampleSliceLayouts.cs diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs new file mode 100644 index 000000000..78f65dbdc --- /dev/null +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs @@ -0,0 +1,176 @@ +using FluentAssertions; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class SetStartIndexesTests +{ + public static IEnumerable SetStartIndexes_Data() + { + // Primary stack + yield return new object[] + { + SliceLayouts.CreatePrimaryStackArea(), + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 1) { StartIndex = 0 }), + (0.5, new OverflowArea() { StartIndex = 1 }) + ), + 2, + }; + + // Multi-column 2-1-0 + yield return new object[] + { + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + new ParentArea( + isRow: true, + (1.0 / 3.0, new SliceArea(order: 0, maxChildren: 2) { StartIndex = 0 }), + (1.0 / 3.0, new SliceArea(order: 1, maxChildren: 1) { StartIndex = 2 }), + (1.0 / 3.0, new OverflowArea() { StartIndex = 3 }) + ), + 3, + }; + + // Secondary primary + yield return new object[] + { + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + new ParentArea( + isRow: true, + (0.25, new SliceArea(order: 1, maxChildren: 2) { StartIndex = 1 }), + (0.5, new SliceArea(order: 0, maxChildren: 1) { StartIndex = 0 }), + (0.25, new OverflowArea() { StartIndex = 3 }) + ), + 3, + }; + + // Overflow column + yield return new object[] + { + SampleSliceLayouts.CreateOverflowColumnLayout(), + new ParentArea(isRow: false, (1.0, new OverflowArea() { StartIndex = 0 })), + 1, + }; + + // Nested + yield return new object[] + { + SampleSliceLayouts.CreateNestedLayout(), + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 2) { StartIndex = 0 }), + ( + 0.5, + new ParentArea( + isRow: false, + (0.5, new SliceArea(order: 1, maxChildren: 2) { StartIndex = 2 }), + (0.5, new OverflowArea() { StartIndex = 4 }) + ) + ) + ), + 3, + }; + + // Create OverflowArea + yield return new object[] + { + new ParentArea(isRow: false, (1.0, new SliceArea(maxChildren: 4))), + new ParentArea(isRow: false, (1.0, new OverflowArea())), + 1 + }; + + // Create OverflowArea for nested + yield return new object[] + { + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 2, maxChildren: 4)), + ( + 0.5, + new ParentArea( + isRow: false, + (0.5, new SliceArea(order: 0, maxChildren: 4)), + (0.5, new SliceArea(order: 1, maxChildren: 4)) + ) + ) + ), + new ParentArea( + isRow: true, + (0.5, new OverflowArea()), + ( + 0.5, + new ParentArea( + isRow: false, + (0.5, new SliceArea(order: 0, maxChildren: 4)), + (0.5, new SliceArea(order: 1, maxChildren: 4)) + ) + ) + ), + 3 + }; + + // Create OverflowArea for complex nested + yield return new object[] + { + new ParentArea( + isRow: true, + (0.25, new SliceArea(order: 3, maxChildren: 4)), + ( + 0.25, + new ParentArea( + isRow: false, + (0.5, new SliceArea(order: 0, maxChildren: 4)), + (0.5, new SliceArea(order: 1, maxChildren: 4)) + ) + ), + ( + 0.25, + new ParentArea( + isRow: false, + (0.5, new SliceArea(order: 4, maxChildren: 4)), + (0.5, new SliceArea(order: 5, maxChildren: 4)) + ) + ), + (0.25, new SliceArea(order: 2, maxChildren: 4)) + ), + new ParentArea( + isRow: true, + (0.25, new SliceArea(order: 3, maxChildren: 4)), + ( + 0.25, + new ParentArea( + isRow: false, + (0.5, new SliceArea(order: 0, maxChildren: 4)), + (0.5, new SliceArea(order: 1, maxChildren: 4)) + ) + ), + ( + 0.25, + new ParentArea( + isRow: false, + (0.5, new SliceArea(order: 4, maxChildren: 4)), + (0.5, new OverflowArea()) + ) + ), + (0.25, new SliceArea(order: 2, maxChildren: 4)) + ), + 6 + }; + } + + [Theory] + [MemberData(nameof(SetStartIndexes_Data))] + public void SetStartIndexes(ParentArea parentArea, ParentArea expectedParentArea, int sliceAreasCount) + { + // When the slice areas are set up + (ParentArea resultingParentArea, BaseSliceArea[] sliceAreas) = parentArea.SetStartIndexes(); + + // Then the start indexes are set correctly. + resultingParentArea.Should().BeEquivalentTo(expectedParentArea); + + // Ensure order of slice areas is correct. + Assert.Equal(sliceAreasCount, sliceAreas.Length); + sliceAreas.Should().BeInAscendingOrder(a => a.StartIndex); + } +} diff --git a/src/Whim.SliceLayout.Tests/GlobalSuppressions.cs b/src/Whim.SliceLayout.Tests/GlobalSuppressions.cs new file mode 100644 index 000000000..bb50cfb13 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/GlobalSuppressions.cs @@ -0,0 +1,17 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +// 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", "CA1014:Mark assemblies with CLSCompliantAttribute")] +[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")] +[assembly: SuppressMessage("Style", "IDE0008:Use explicit type")] +[assembly: SuppressMessage("Style", "IDE0042:Variable declaration can be deconstructed")] +[assembly: SuppressMessage("Style", "IDE0058:Expression value is never used")] +[assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods")] +[assembly: SuppressMessage("Design", "CA1024:Use properties where appropriate")] diff --git a/src/Whim.SliceLayout.Tests/SampleSliceLayouts.cs b/src/Whim.SliceLayout.Tests/SampleSliceLayouts.cs new file mode 100644 index 000000000..78d8ce74e --- /dev/null +++ b/src/Whim.SliceLayout.Tests/SampleSliceLayouts.cs @@ -0,0 +1,66 @@ +namespace Whim.SliceLayout.Tests; + +public static class SampleSliceLayouts +{ + // ------------------------------------------------------------------ + // | | | + // | | | + // | | | + // | | | + // | | Slice 1 | + // | | 2 windows | + // | | | + // | | | + // | | | + // | | | + // | Slice 0 |--------------------------------| + // | 2 windows | | + // | | | + // | | | + // | | | + // | | | + // | | Overflow | + // | | | + // | | | + // | | | + // | | | + // | | | + // | | | + // ------------------------------------------------------------------ + public static ParentArea CreateNestedLayout() => + new( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 2)), + ( + 0.5, + new ParentArea(isRow: false, (0.5, new SliceArea(order: 1, maxChildren: 2)), (0.5, new OverflowArea())) + ) + ); + + // -------------------------------- + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | Overflow | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // -------------------------------- + public static ParentArea CreateOverflowColumnLayout() => new(isRow: false, (1.0, new OverflowArea())); +} diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index a65470eae..cec0c673e 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -60,17 +60,18 @@ public static ParentArea Prune(this ParentArea area, int windowCount) } /// - /// Set the start indexes of the areas. + /// Set the start indexes of the areas. If there is no , then the last + /// is replaced with an . /// - /// + /// /// - /// The areas which directly contain windows, in order. + /// The root area to use, and the areas which directly contain windows, in order. /// - public static BaseSliceArea[] SetStartIndexes(this ParentArea area) + public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartIndexes(this ParentArea rootArea) { - if (area.Children.Count == 0) + if (rootArea.Children.Count == 0) { - return Array.Empty(); + return (rootArea, Array.Empty()); } List sliceAreas = new(); @@ -79,7 +80,7 @@ public static BaseSliceArea[] SetStartIndexes(this ParentArea area) // Iterate through the tree and add the areas to the list. Queue areas = new(); - areas.Enqueue(area); + areas.Enqueue(rootArea); while (areas.Count > 0) { @@ -117,13 +118,28 @@ public static BaseSliceArea[] SetStartIndexes(this ParentArea area) if (overflowArea is null) { + // Replace the last slice area with an overflow area. Logger.Error($"No overflow area found, replacing last slice area with overflow area"); - overflowArea = ReplaceLastSliceAreaWithOverflowArea(area, sliceAreas[^1].Order); + (ParentArea, OverflowArea)? result = ReplaceLastSliceAreaWithOverflowArea(rootArea, sliceAreas[^1].Order); + + if (result is null) + { + Logger.Error($"Failed to replace last slice area with overflow area"); + return (rootArea, Array.Empty()); + } + else + { + (rootArea, overflowArea) = result.Value; + } sliceAreas.RemoveAt(sliceAreas.Count - 1); } - SliceArea lastSlice = sliceAreas[^1]; - overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; + // If there are multiple slice areas, then the overflow area should start after the last slice area. + if (sliceAreas.Count > 0) + { + SliceArea lastSlice = sliceAreas[^1]; + overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; + } BaseSliceArea[] windowAreas = new BaseSliceArea[sliceAreas.Count + 1]; for (int i = 0; i < sliceAreas.Count; i++) @@ -132,59 +148,47 @@ public static BaseSliceArea[] SetStartIndexes(this ParentArea area) } windowAreas[^1] = overflowArea; - return windowAreas; + return (rootArea, windowAreas); } - // TODO: Test as public - private static OverflowArea ReplaceLastSliceAreaWithOverflowArea(ParentArea rootArea, uint lastSliceOrder) + private static (ParentArea Parent, OverflowArea Overflow)? ReplaceLastSliceAreaWithOverflowArea( + ParentArea rootArea, + uint lastSliceOrder + ) { - // DFS - List areaStack = new() { rootArea }; - - while (areaStack.Count > 0) + // Recursive DFS + foreach (IArea child in rootArea.Children) { - ParentArea currParentArea = (ParentArea)areaStack[^1]; - areaStack.RemoveAt(areaStack.Count - 1); - - // Go over each of the children. If the child is the last slice, replace it. - // Otherwise, add it to the stack. - for (int i = 0; i < currParentArea.Children.Count; i++) + if (child is ParentArea parentArea) { - IArea child = currParentArea.Children[i]; - if (child is ParentArea childParentArea) + (ParentArea Parent, OverflowArea Overflow)? result = ReplaceLastSliceAreaWithOverflowArea(parentArea, lastSliceOrder); + if (result is not null) { - areaStack.Add(childParentArea); - } - else if (child is SliceArea sliceArea && sliceArea.Order == lastSliceOrder) - { - OverflowArea newOverflow = new(sliceArea.IsRow); - areaStack.Add(newOverflow); - RebuildWithOverflowArea(areaStack); - return newOverflow; + return (ReplaceParentArea(rootArea, parentArea, result.Value.Parent), result.Value.Overflow); } } + else if (child is SliceArea sliceArea && sliceArea.Order == lastSliceOrder) + { + OverflowArea newOverflow = new(sliceArea.IsRow) { StartIndex = sliceArea.StartIndex }; + + ImmutableList newRootAreaChildren = rootArea.Children; + newRootAreaChildren = newRootAreaChildren.SetItem(newRootAreaChildren.IndexOf(sliceArea), newOverflow); + + return (new ParentArea(isRow: rootArea.IsRow, rootArea.Weights, newRootAreaChildren), newOverflow); + } } - Logger.Error("Could not replace last slice with overflow area"); - return null!; + return null; } - private static ParentArea RebuildWithOverflowArea(List areaStack) + private static ParentArea ReplaceParentArea( + ParentArea rootArea, + IArea oldChild, IArea newChild + ) { - for (int idx = areaStack.Count - 2; idx >= 0; idx--) - { - ParentArea currArea = (ParentArea)areaStack[idx]; - IArea nextArea = areaStack[idx + 1]; - - ImmutableList currAreaChildren = currArea.Children; - currAreaChildren = currAreaChildren.SetItem(currAreaChildren.IndexOf(nextArea), areaStack[idx + 1]); - - ImmutableList currAreaWeights = currArea.Weights; - - areaStack[idx] = new ParentArea(currArea.IsRow, currAreaWeights, currAreaChildren); - } - - return (ParentArea)areaStack[0]; + ImmutableList newRootAreaChildren = rootArea.Children; + newRootAreaChildren = newRootAreaChildren.SetItem(newRootAreaChildren.IndexOf(oldChild), newChild); + return new ParentArea(isRow: rootArea.IsRow, rootArea.Weights, newRootAreaChildren); } public static int DoParentLayout( diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 62044a032..2bf269986 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -61,9 +61,8 @@ ParentArea rootArea _plugin = plugin; Identity = identity; _windows = windows; - _rootArea = rootArea; - _windowAreas = _rootArea.SetStartIndexes(); + (_rootArea, _windowAreas) = rootArea.SetStartIndexes(); _prunedRootArea = _rootArea.Prune(_windows.Count); } diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index d569b333d..a95e778d7 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -17,19 +17,48 @@ public static ILayoutEngine CreatePrimaryStackLayout( IContext context, ISliceLayoutPlugin plugin, LayoutEngineIdentity identity - ) => - new SliceLayoutEngine( - context, - plugin, - identity, - new ParentArea(isRow: true, (0.5, new SliceArea(maxChildren: 1, order: 0)), (0.5, new OverflowArea())) - ); + ) => new SliceLayoutEngine(context, plugin, identity, CreatePrimaryStackArea()); + + internal static ParentArea CreatePrimaryStackArea() => + new(isRow: true, (0.5, new SliceArea(order: 0, maxChildren: 1)), (0.5, new OverflowArea())); /// /// Creates a multi-column layout with the given number of columns. ///
/// For example, new uint[] { 2, 1, 0 } will create a layout with 3 columns, where the /// first column has 2 rows, the second column has 1 row, and the third column has infinite rows. + /// + /// For example: + /// + /// + /// + /// ------------------------------------------------------------------------------------------------- + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | Slice 1 | Slice 2 | Overflow | + /// | 2 windows | 1 window | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// ------------------------------------------------------------------------------------------------- + /// + /// ///
/// The to use /// The to use @@ -42,7 +71,9 @@ public static ILayoutEngine CreateMultiColumnLayout( ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, params uint[] capacities - ) + ) => new SliceLayoutEngine(context, plugin, identity, CreateMultiColumnArea(capacities)); + + internal static ParentArea CreateMultiColumnArea(uint[] capacities) { double weight = 1.0 / capacities.Length; (double, IArea)[] areas = new (double, IArea)[capacities.Length]; @@ -55,6 +86,7 @@ params uint[] capacities { if (createdOverflow) { + // TODO: Handle this throw new ArgumentException("Cannot have multiple base areas"); } @@ -63,10 +95,77 @@ params uint[] capacities } else { - areas[idx] = (weight, new SliceArea(maxChildren: capacity, order: (uint)idx)); + areas[idx] = (weight, new SliceArea(order: (uint)idx, maxChildren: capacity)); } } - return new SliceLayoutEngine(context, plugin, identity, new ParentArea(isRow: true, areas)); + ParentArea parentArea = new(isRow: true, areas); + return parentArea; + } + + /// + /// Creates a multi-column layout, where the primary column is in the middle, the secondary + /// column is on the left, and the overflow column is on the right. + /// + /// The middle column takes up 50% of the screen, and the left and right columns take up 25%. + /// + /// For example: + /// + /// + /// + /// ------------------------------------------------------------------------ + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | Slice 2 | Slice 1 | Overflow | + /// | 2 windows | 1 window | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// | | | | + /// ------------------------------------------------------------------------ + /// + /// + /// + /// + /// + /// + /// + public static ILayoutEngine CreateSecondaryPrimaryStackLayout( + IContext context, + ISliceLayoutPlugin plugin, + LayoutEngineIdentity identity, + uint primaryColumnCapacity = 1, + uint secondaryColumnCapacity = 2 + ) => + new SliceLayoutEngine( + context, + plugin, + identity, + CreateSecondaryPrimaryStackArea(primaryColumnCapacity, secondaryColumnCapacity) + ); + + internal static ParentArea CreateSecondaryPrimaryStackArea(uint primaryColumnCapacity, uint secondaryColumnCapacity) + { + return new ParentArea( + isRow: true, + (0.25, new SliceArea(order: 1, maxChildren: secondaryColumnCapacity)), + (0.5, new SliceArea(order: 0, maxChildren: primaryColumnCapacity)), + (0.25, new OverflowArea()) + ); } } From 45078854ef856775654532d05acbae44b3983376 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sat, 23 Dec 2023 14:39:08 +1300 Subject: [PATCH 25/64] Formatting --- src/Whim.SliceLayout/AreaHelpers.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index cec0c673e..6bcf75b75 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -161,7 +161,10 @@ uint lastSliceOrder { if (child is ParentArea parentArea) { - (ParentArea Parent, OverflowArea Overflow)? result = ReplaceLastSliceAreaWithOverflowArea(parentArea, lastSliceOrder); + (ParentArea Parent, OverflowArea Overflow)? result = ReplaceLastSliceAreaWithOverflowArea( + parentArea, + lastSliceOrder + ); if (result is not null) { return (ReplaceParentArea(rootArea, parentArea, result.Value.Parent), result.Value.Overflow); @@ -181,10 +184,7 @@ uint lastSliceOrder return null; } - private static ParentArea ReplaceParentArea( - ParentArea rootArea, - IArea oldChild, IArea newChild - ) + private static ParentArea ReplaceParentArea(ParentArea rootArea, IArea oldChild, IArea newChild) { ImmutableList newRootAreaChildren = rootArea.Children; newRootAreaChildren = newRootAreaChildren.SetItem(newRootAreaChildren.IndexOf(oldChild), newChild); From 66a1006f75195ce434a4f1e3cc205e442e486bf7 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sat, 23 Dec 2023 14:54:38 +1300 Subject: [PATCH 26/64] Refactor a bit and fix build --- .github/workflows/commit.yml | 2 +- .github/workflows/release.yml | 2 +- Whim.sln | 69 +++++++------- src/Whim.Runner/Whim.Runner.csproj | 97 +++++++------------- src/Whim.SliceLayout/AreaHelpers.cs | 83 +++++++++-------- src/Whim.SliceLayout/Whim.SliceLayout.csproj | 81 ++++++++-------- 6 files changed, 158 insertions(+), 176 deletions(-) diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 3a09a8714..5d02364b4 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -73,7 +73,7 @@ jobs: # dotnet format analyzers Whim.sln --verify-no-changes --no-restore - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.1 + uses: microsoft/setup-msbuild@v1.3.1 - name: Build run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fda4f06c7..3444bcb06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,7 +94,7 @@ jobs: Configuration: ${{ matrix.configuration }} - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.1 + uses: microsoft/setup-msbuild@v1.3.1 - name: Build run: | diff --git a/Whim.sln b/Whim.sln index 74438746a..30b6d66bc 100644 --- a/Whim.sln +++ b/Whim.sln @@ -13,11 +13,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.FloatingLayout", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Runner", "src\Whim.Runner\Whim.Runner.csproj", "{D74F1389-F4D1-4A1C-ACE3-D91D70472DBB}" ProjectSection(ProjectDependencies) = postProject - {4F66FB22-C4B3-494F-BEE0-9F878349621E} = {4F66FB22-C4B3-494F-BEE0-9F878349621E} - {636EA561-2625-4D2A-9A8A-890E5AEF18B9} = {636EA561-2625-4D2A-9A8A-890E5AEF18B9} + {81F6ADB0-5053-46E7-81C6-1F04E243FB1D} = {81F6ADB0-5053-46E7-81C6-1F04E243FB1D} {6B13ECFB-0754-4721-819A-9F4712B75E18} = {6B13ECFB-0754-4721-819A-9F4712B75E18} - {C16D9C8A-A36E-430C-AB84-700919BC7259} = {C16D9C8A-A36E-430C-AB84-700919BC7259} + {636EA561-2625-4D2A-9A8A-890E5AEF18B9} = {636EA561-2625-4D2A-9A8A-890E5AEF18B9} {D1CD9632-7BF6-40D3-BEE5-E6FA9BCE79CB} = {D1CD9632-7BF6-40D3-BEE5-E6FA9BCE79CB} + {C16D9C8A-A36E-430C-AB84-700919BC7259} = {C16D9C8A-A36E-430C-AB84-700919BC7259} + {05AB1322-761E-4C82-BF41-9613CA563C5A} = {05AB1322-761E-4C82-BF41-9613CA563C5A} + {4F66FB22-C4B3-494F-BEE0-9F878349621E} = {4F66FB22-C4B3-494F-BEE0-9F878349621E} + {8CD01B7A-78BD-48BD-B274-B2FEE48CC8F4} = {8CD01B7A-78BD-48BD-B274-B2FEE48CC8F4} + {38946E3C-F336-450F-8159-164AD14C025B} = {38946E3C-F336-450F-8159-164AD14C025B} + {3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC} = {3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC} + {1E722ACD-6DC6-4D2E-AD30-642483643B7C} = {1E722ACD-6DC6-4D2E-AD30-642483643B7C} + {E91865D5-66D9-448B-9EEC-E0EEF6EC5EBE} = {E91865D5-66D9-448B-9EEC-E0EEF6EC5EBE} EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Bar", "src\Whim.Bar\Whim.Bar.csproj", "{D1CD9632-7BF6-40D3-BEE5-E6FA9BCE79CB}" @@ -50,6 +57,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.TestUtils", "src\Whim. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Bar.Tests", "src\Whim.Bar.Tests\Whim.Bar.Tests.csproj", "{6339FE1D-E9CF-4C71-868A-931CB51D6486}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.SliceLayout", "src\Whim.SliceLayout\Whim.SliceLayout.csproj", "{38946E3C-F336-450F-8159-164AD14C025B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.SliceLayout.Tests", "src\Whim.SliceLayout.Tests\Whim.SliceLayout.Tests.csproj", "{0C32EB78-44C7-4397-8643-2E1C1F3442E9}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.TreeLayout", "src\Whim.TreeLayout\Whim.TreeLayout.csproj", "{3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.TreeLayout.Tests", "src\Whim.TreeLayout.Tests\Whim.TreeLayout.Tests.csproj", "{804053FC-ABCF-4A54-9B24-CF623CEA990F}" @@ -62,10 +73,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Updater", "src\Whim.Up EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.Updater.Tests", "src\Whim.Updater.Tests\Whim.Updater.Tests.csproj", "{741677F5-5C83-474E-83B7-08F7A84DE11D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.SliceLayout", "src\Whim.SliceLayout\Whim.SliceLayout.csproj", "{38946E3C-F336-450F-8159-164AD14C025B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Whim.SliceLayout.Tests", "src\Whim.SliceLayout.Tests\Whim.SliceLayout.Tests.csproj", "{0C32EB78-44C7-4397-8643-2E1C1F3442E9}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -284,6 +291,30 @@ Global {6339FE1D-E9CF-4C71-868A-931CB51D6486}.Release|arm64.Build.0 = Release|arm64 {6339FE1D-E9CF-4C71-868A-931CB51D6486}.Release|x64.ActiveCfg = Release|x64 {6339FE1D-E9CF-4C71-868A-931CB51D6486}.Release|x64.Build.0 = Release|x64 + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.ActiveCfg = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.Build.0 = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.ActiveCfg = Debug|x64 + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.Build.0 = Debug|x64 + {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.Build.0 = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.ActiveCfg = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.Build.0 = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.ActiveCfg = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.Build.0 = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.ActiveCfg = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.Build.0 = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|x64.ActiveCfg = Debug|x64 + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|x64.Build.0 = Debug|x64 + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|Any CPU.Build.0 = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.ActiveCfg = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.Build.0 = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.ActiveCfg = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.Build.0 = Release|Any CPU {3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC}.Debug|arm64.ActiveCfg = Debug|arm64 @@ -356,30 +387,6 @@ Global {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|arm64.Build.0 = Release|Any CPU {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|x64.ActiveCfg = Release|Any CPU {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|x64.Build.0 = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.ActiveCfg = Debug|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.Build.0 = Debug|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.ActiveCfg = Debug|x64 - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.Build.0 = Debug|x64 - {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.Build.0 = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.ActiveCfg = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.Build.0 = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.ActiveCfg = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.Build.0 = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.ActiveCfg = Debug|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.Build.0 = Debug|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|x64.ActiveCfg = Debug|x64 - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|x64.Build.0 = Debug|x64 - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|Any CPU.Build.0 = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.ActiveCfg = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.Build.0 = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.ActiveCfg = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Whim.Runner/Whim.Runner.csproj b/src/Whim.Runner/Whim.Runner.csproj index 9b9159b68..e23c44142 100644 --- a/src/Whim.Runner/Whim.Runner.csproj +++ b/src/Whim.Runner/Whim.Runner.csproj @@ -90,8 +90,7 @@ - + @@ -102,10 +101,9 @@ - + - + @@ -128,115 +126,84 @@ - + - - + + - - + + - - + + - + - - + + - - + + - + - - + + - - + + - + - + - + - - + + - - + + - - + + - + - - + + \ No newline at end of file diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index 6bcf75b75..52bd4e1d8 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -74,39 +74,7 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI return (rootArea, Array.Empty()); } - List sliceAreas = new(); - List parentAreas = new(); - OverflowArea? overflowArea = null; - - // Iterate through the tree and add the areas to the list. - Queue areas = new(); - areas.Enqueue(rootArea); - - while (areas.Count > 0) - { - IArea currArea = areas.Dequeue(); - - if (currArea is ParentArea parentArea) - { - for (int i = 0; i < parentArea.Children.Count; i++) - { - areas.Enqueue(parentArea.Children[i]); - } - - parentAreas.Add(parentArea); - } - else if (currArea is SliceArea sliceArea) - { - sliceAreas.Add(sliceArea); - } - else if (currArea is OverflowArea currOverflowArea) - { - overflowArea = currOverflowArea; - } - } - - // Sort the areas by order. - sliceAreas.Sort((a, b) => a.Order.CompareTo(b.Order)); + (List sliceAreas, OverflowArea? overflowArea) = GetAreasSorted(rootArea); // Set the start indexes. uint currIdx = 0; @@ -127,10 +95,8 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI Logger.Error($"Failed to replace last slice area with overflow area"); return (rootArea, Array.Empty()); } - else - { - (rootArea, overflowArea) = result.Value; - } + + (rootArea, overflowArea) = result.Value; sliceAreas.RemoveAt(sliceAreas.Count - 1); } @@ -141,6 +107,7 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI overflowArea.StartIndex = lastSlice.StartIndex + lastSlice.MaxChildren; } + // Create an array of the slice areas, and add the overflow area to the end. BaseSliceArea[] windowAreas = new BaseSliceArea[sliceAreas.Count + 1]; for (int i = 0; i < sliceAreas.Count; i++) { @@ -151,6 +118,48 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI return (rootArea, windowAreas); } + /// + /// Get the areas sorted by order, and the overflow area if it exists. + /// We don't handle the case where there are multiple overflow areas, as overflow areas are + /// greedy. + /// + /// + /// + private static (List SliceAreas, OverflowArea? OverflowArea) GetAreasSorted(ParentArea rootArea) + { + // Iterate through the tree and add the areas to the list, using DFS. + List sliceAreas = new(); + OverflowArea? overflowArea = null; + List areas = new() { rootArea }; + + while (areas.Count > 0) + { + IArea currArea = areas[^1]; + areas.RemoveAt(areas.Count - 1); + + if (currArea is ParentArea parentArea) + { + for (int i = 0; i < parentArea.Children.Count; i++) + { + areas.Add(parentArea.Children[i]); + } + } + else if (currArea is SliceArea sliceArea) + { + sliceAreas.Add(sliceArea); + } + else if (currArea is OverflowArea currOverflowArea) + { + overflowArea = currOverflowArea; + } + } + + // Sort the areas by order. + sliceAreas.Sort((a, b) => a.Order.CompareTo(b.Order)); + + return (sliceAreas, overflowArea); + } + private static (ParentArea Parent, OverflowArea Overflow)? ReplaceLastSliceAreaWithOverflowArea( ParentArea rootArea, uint lastSliceOrder diff --git a/src/Whim.SliceLayout/Whim.SliceLayout.csproj b/src/Whim.SliceLayout/Whim.SliceLayout.csproj index ce332b3f0..9af630579 100644 --- a/src/Whim.SliceLayout/Whim.SliceLayout.csproj +++ b/src/Whim.SliceLayout/Whim.SliceLayout.csproj @@ -1,48 +1,47 @@ - + - - All - All - None - All - All - All - All - All - All - All - All - All - Isaac Daly - true - An extensible window manager for Windows. - true - true - true - enable - x64;arm64;Any CPU - Whim.SliceLayout - win10-x64;win10-arm64 - net7.0-windows10.0.19041.0 - 10.0.17763.0 - true - 0.2.0 - + + All + All + None + All + All + All + All + All + All + All + All + All + Isaac Daly + true + An extensible window manager for Windows. + true + true + true + enable + x64;arm64;Any CPU + Whim.SliceLayout + win10-x64;win10-arm64 + net7.0-windows10.0.19041.0 + 10.0.17763.0 + true + 0.2.0 + - - - - + + + + - - - - - + + + + - - - + + + \ No newline at end of file From 5d9bc561026e9ed0de45df6d406f864926a7fa96 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sat, 23 Dec 2023 16:49:59 +1300 Subject: [PATCH 27/64] Try turn off parallel build --- .github/workflows/commit.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 5d02364b4..9a142a543 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -80,7 +80,6 @@ jobs: msbuild Whim.sln ` -p:Configuration=$env:Configuration ` -p:Platform=$env:Platform ` - -p:BuildInParallel=true ` -maxCpuCount env: Configuration: ${{ matrix.configuration }} From 57d7a43e647302ee3c2125f76e99549d3593b022 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sat, 23 Dec 2023 17:01:58 +1300 Subject: [PATCH 28/64] Remove SliceLayout from Whim.Runner.csproj --- .github/workflows/commit.yml | 1 + src/Whim.Runner/Whim.Runner.csproj | 97 +++++++++++++++++++----------- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 9a142a543..5d02364b4 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -80,6 +80,7 @@ jobs: msbuild Whim.sln ` -p:Configuration=$env:Configuration ` -p:Platform=$env:Platform ` + -p:BuildInParallel=true ` -maxCpuCount env: Configuration: ${{ matrix.configuration }} diff --git a/src/Whim.Runner/Whim.Runner.csproj b/src/Whim.Runner/Whim.Runner.csproj index e23c44142..e75b00c37 100644 --- a/src/Whim.Runner/Whim.Runner.csproj +++ b/src/Whim.Runner/Whim.Runner.csproj @@ -90,7 +90,8 @@ - + @@ -103,7 +104,8 @@ - + @@ -126,84 +128,109 @@ - + - - + + - - + + - - + + - + - - + + - - + + - + - - + + - - - - - - - + + - + - + - - + + - - + + - - + + - + - - + + \ No newline at end of file From 4089bbba61ab6211aab380d0d6bf54930af13189 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sat, 23 Dec 2023 17:08:45 +1300 Subject: [PATCH 29/64] Add SliceLayout build back in --- src/Whim.Runner/Whim.Runner.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Whim.Runner/Whim.Runner.csproj b/src/Whim.Runner/Whim.Runner.csproj index e75b00c37..d4095ed80 100644 --- a/src/Whim.Runner/Whim.Runner.csproj +++ b/src/Whim.Runner/Whim.Runner.csproj @@ -191,6 +191,12 @@ DestinationFolder="$(TargetDir)plugins\Whim.LayoutPreview\" SkipUnchangedFiles="true" /> + + + + + Date: Sat, 23 Dec 2023 17:40:40 +1300 Subject: [PATCH 30/64] Maybe fix build? --- Whim.sln | 36 +++++------ src/Whim.Runner/Whim.Runner.csproj | 95 ++++++++++-------------------- 2 files changed, 49 insertions(+), 82 deletions(-) diff --git a/Whim.sln b/Whim.sln index 30b6d66bc..5ecb76673 100644 --- a/Whim.sln +++ b/Whim.sln @@ -293,28 +293,28 @@ Global {6339FE1D-E9CF-4C71-868A-931CB51D6486}.Release|x64.Build.0 = Release|x64 {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38946E3C-F336-450F-8159-164AD14C025B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.ActiveCfg = Debug|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.Build.0 = Debug|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.ActiveCfg = Debug|arm64 + {38946E3C-F336-450F-8159-164AD14C025B}.Debug|arm64.Build.0 = Debug|arm64 {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.ActiveCfg = Debug|x64 {38946E3C-F336-450F-8159-164AD14C025B}.Debug|x64.Build.0 = Debug|x64 {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {38946E3C-F336-450F-8159-164AD14C025B}.Release|Any CPU.Build.0 = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.ActiveCfg = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.Build.0 = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.ActiveCfg = Release|Any CPU - {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.Build.0 = Release|Any CPU + {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.ActiveCfg = Release|arm64 + {38946E3C-F336-450F-8159-164AD14C025B}.Release|arm64.Build.0 = Release|arm64 + {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.ActiveCfg = Release|x64 + {38946E3C-F336-450F-8159-164AD14C025B}.Release|x64.Build.0 = Release|x64 {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.ActiveCfg = Debug|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.Build.0 = Debug|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.ActiveCfg = Debug|arm64 + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|arm64.Build.0 = Debug|arm64 {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|x64.ActiveCfg = Debug|x64 {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Debug|x64.Build.0 = Debug|x64 {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|Any CPU.Build.0 = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.ActiveCfg = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.Build.0 = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.ActiveCfg = Release|Any CPU - {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.Build.0 = Release|Any CPU + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.ActiveCfg = Release|arm64 + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|arm64.Build.0 = Release|arm64 + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.ActiveCfg = Release|x64 + {0C32EB78-44C7-4397-8643-2E1C1F3442E9}.Release|x64.Build.0 = Release|x64 {3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {3F7A2C45-EC4E-4CF5-8D62-4BA3A52F6CAC}.Debug|arm64.ActiveCfg = Debug|arm64 @@ -377,16 +377,16 @@ Global {E91865D5-66D9-448B-9EEC-E0EEF6EC5EBE}.Release|x64.Build.0 = Release|x64 {741677F5-5C83-474E-83B7-08F7A84DE11D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {741677F5-5C83-474E-83B7-08F7A84DE11D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {741677F5-5C83-474E-83B7-08F7A84DE11D}.Debug|arm64.ActiveCfg = Debug|Any CPU - {741677F5-5C83-474E-83B7-08F7A84DE11D}.Debug|arm64.Build.0 = Debug|Any CPU + {741677F5-5C83-474E-83B7-08F7A84DE11D}.Debug|arm64.ActiveCfg = Debug|arm64 + {741677F5-5C83-474E-83B7-08F7A84DE11D}.Debug|arm64.Build.0 = Debug|arm64 {741677F5-5C83-474E-83B7-08F7A84DE11D}.Debug|x64.ActiveCfg = Debug|x64 {741677F5-5C83-474E-83B7-08F7A84DE11D}.Debug|x64.Build.0 = Debug|x64 {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|Any CPU.ActiveCfg = Release|Any CPU {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|Any CPU.Build.0 = Release|Any CPU - {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|arm64.ActiveCfg = Release|Any CPU - {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|arm64.Build.0 = Release|Any CPU - {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|x64.ActiveCfg = Release|Any CPU - {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|x64.Build.0 = Release|Any CPU + {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|arm64.ActiveCfg = Release|arm64 + {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|arm64.Build.0 = Release|arm64 + {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|x64.ActiveCfg = Release|x64 + {741677F5-5C83-474E-83B7-08F7A84DE11D}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Whim.Runner/Whim.Runner.csproj b/src/Whim.Runner/Whim.Runner.csproj index d4095ed80..e23c44142 100644 --- a/src/Whim.Runner/Whim.Runner.csproj +++ b/src/Whim.Runner/Whim.Runner.csproj @@ -90,8 +90,7 @@ - + @@ -104,8 +103,7 @@ - + @@ -128,115 +126,84 @@ - + - - + + - - + + - - + + - + - - + + - - + + - + - - + + - - + + - + - + - + - - + + - - + + - - + + - + - - + + \ No newline at end of file From a0fef0a836ac77965b9e3656cccbbfc865f6e610 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sat, 23 Dec 2023 18:43:43 +1300 Subject: [PATCH 31/64] Prune tests --- .../AreaHelpers/PruneTests.cs | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs new file mode 100644 index 000000000..46d13adc5 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs @@ -0,0 +1,213 @@ +using FluentAssertions; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class PruneTests +{ + public static IEnumerable Prune_PrimaryStack() + { + // Empty + yield return new object[] { SliceLayouts.CreatePrimaryStackArea(), 0, new ParentArea(isRow: true), }; + + // Fill primary + yield return new object[] + { + SliceLayouts.CreatePrimaryStackArea(), + 1, + new ParentArea(isRow: true, (1.0, new SliceArea(order: 0, maxChildren: 1))), + }; + + // Fill overflow + yield return new object[] + { + SliceLayouts.CreatePrimaryStackArea(), + 3, + new ParentArea(isRow: true, (0.5, new SliceArea(order: 0, maxChildren: 1)), (0.5, new OverflowArea())), + }; + } + + public static IEnumerable Prune_MultiColumn() + { + // Empty + yield return new object[] + { + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + 0, + new ParentArea(isRow: true), + }; + + // Single window + yield return new object[] + { + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + 1, + new ParentArea(isRow: true, (1.0, new SliceArea(order: 0, maxChildren: 2))), + }; + + // Fill primary + yield return new object[] + { + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + 2, + new ParentArea(isRow: true, (1.0, new SliceArea(order: 0, maxChildren: 2))), + }; + + // Fill secondary + yield return new object[] + { + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + 3, + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 2)), + (0.5, new SliceArea(order: 1, maxChildren: 1) { StartIndex = 2 }) + ), + }; + + // Fill overflow + yield return new object[] + { + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + 4, + new ParentArea( + isRow: true, + (1.0 / 3.0, new SliceArea(order: 0, maxChildren: 2)), + (1.0 / 3.0, new SliceArea(order: 1, maxChildren: 1) { StartIndex = 2 }), + (1.0 / 3.0, new OverflowArea() { StartIndex = 3 }) + ), + }; + } + + public static IEnumerable Prune_SecondaryPrimary() + { + // Empty + yield return new object[] + { + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + 0, + new ParentArea(isRow: true), + }; + + // Fill primary + yield return new object[] + { + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + 1, + new ParentArea(isRow: true, (1.0, new SliceArea(order: 0, maxChildren: 1))), + }; + + // Fill secondary + yield return new object[] + { + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + 3, + new ParentArea( + isRow: true, + (0.25 + 0.125, new SliceArea(order: 1, maxChildren: 2) { StartIndex = 1 }), + (0.5 + 0.125, new SliceArea(order: 0, maxChildren: 1)) + ), + }; + + // Fill overflow + yield return new object[] + { + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + 5, + new ParentArea( + isRow: true, + (0.25, new SliceArea(order: 1, maxChildren: 2) { StartIndex = 1 }), + (0.5, new SliceArea(order: 0, maxChildren: 1)), + (0.25, new OverflowArea() { StartIndex = 2 }) + ), + }; + } + + public static IEnumerable Prune_OverflowColumn() + { + // Empty + yield return new object[] { SampleSliceLayouts.CreateOverflowColumnLayout(), 0, new ParentArea(isRow: false), }; + + // Single window + yield return new object[] + { + SampleSliceLayouts.CreateOverflowColumnLayout(), + 1, + new ParentArea(isRow: false, (1.0, new OverflowArea())), + }; + + // Fill overflow + yield return new object[] + { + SampleSliceLayouts.CreateOverflowColumnLayout(), + 2, + new ParentArea(isRow: false, (1.0, new OverflowArea())), + }; + } + + public static IEnumerable Prune_Nested() + { + // Empty + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 0, new ParentArea(isRow: true), }; + + // Single window + yield return new object[] + { + SampleSliceLayouts.CreateNestedLayout(), + 1, + new ParentArea(isRow: true, (1.0, new SliceArea(order: 0, maxChildren: 2))), + }; + + // Fill primary + yield return new object[] + { + SampleSliceLayouts.CreateNestedLayout(), + 2, + new ParentArea(isRow: true, (1.0, new SliceArea(order: 0, maxChildren: 2))), + }; + + // Fill secondary + yield return new object[] + { + SampleSliceLayouts.CreateNestedLayout(), + 4, + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 2)), + (0.5, new SliceArea(order: 1, maxChildren: 2) { StartIndex = 2 }) + ), + }; + + // Fill overflow + yield return new object[] + { + SampleSliceLayouts.CreateNestedLayout(), + 6, + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 2)), + ( + 0.5, + new ParentArea( + isRow: false, + (0.5, new SliceArea(order: 1, maxChildren: 2) { StartIndex = 2 }), + (0.5, new OverflowArea() { StartIndex = 4 }) + ) + ) + ), + }; + } + + [Theory] + [MemberData(nameof(Prune_PrimaryStack))] + [MemberData(nameof(Prune_MultiColumn))] + [MemberData(nameof(Prune_SecondaryPrimary))] + [MemberData(nameof(Prune_OverflowColumn))] + [MemberData(nameof(Prune_Nested))] + public void Prune(ParentArea area, int windowCount, ParentArea expected) + { + area.SetStartIndexes(); + ParentArea pruned = area.Prune(windowCount); + expected.Should().BeEquivalentTo(pruned); + } +} From 16bddd4800a582355689b9d8785a60358cc28723 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sat, 23 Dec 2023 19:07:33 +1300 Subject: [PATCH 32/64] Handle empty parents --- .../AreaHelpers/SetStartIndexesTests.cs | 8 ++++++++ src/Whim.SliceLayout/AreaHelpers.cs | 14 +++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs index 78f65dbdc..c3050034b 100644 --- a/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs @@ -7,6 +7,14 @@ public class SetStartIndexesTests { public static IEnumerable SetStartIndexes_Data() { + // Empty + yield return new object[] + { + new ParentArea(isRow: true), + new ParentArea(isRow: true, (1.0, new OverflowArea(isRow: true))), + 1, + }; + // Primary stack yield return new object[] { diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index 52bd4e1d8..7f8af24da 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -71,7 +71,8 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI { if (rootArea.Children.Count == 0) { - return (rootArea, Array.Empty()); + Logger.Error($"No children found, creating single overflow area"); + return SingleOverflowArea(); } (List sliceAreas, OverflowArea? overflowArea) = GetAreasSorted(rootArea); @@ -92,8 +93,8 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI if (result is null) { - Logger.Error($"Failed to replace last slice area with overflow area"); - return (rootArea, Array.Empty()); + Logger.Error($"Failed to replace last slice area with overflow area, creating single overflow area"); + return SingleOverflowArea(); } (rootArea, overflowArea) = result.Value; @@ -118,6 +119,13 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI return (rootArea, windowAreas); } + private static (ParentArea Parent, BaseSliceArea[] OrderedSliceAreas) SingleOverflowArea() + { + OverflowArea overflowArea = new(isRow: true); + ParentArea parentArea = new(isRow: true, (1.0, overflowArea)); + return (parentArea, new BaseSliceArea[] { overflowArea }); + } + /// /// Get the areas sorted by order, and the overflow area if it exists. /// We don't handle the case where there are multiple overflow areas, as overflow areas are From 77072959acd21e27025a3730464a8ccaf3d5a7a2 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 00:04:17 +1300 Subject: [PATCH 33/64] Most DoLayout tests --- .../AreaHelpers/DoLayout.cs | 223 ++++++++++++++++++ src/Whim.SliceLayout/AreaHelpers.cs | 2 +- src/Whim.SliceLayout/SliceLayoutEngine.cs | 4 +- src/Whim/Whim.csproj | 1 + 4 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs new file mode 100644 index 000000000..b08f4f79f --- /dev/null +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs @@ -0,0 +1,223 @@ +using FluentAssertions; +using NSubstitute; +using Whim.TestUtils; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class DoLayoutTests +{ + private static readonly LayoutEngineIdentity identity = new(); + + public static IEnumerable DoLayout_PrimaryStack() + { + // Empty + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreatePrimaryStackArea(), + Array.Empty(), + }; + + // Fill primary + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreatePrimaryStackArea(), + new[] { new Rectangle(0, 0, 100, 100), }, + }; + + // Fill overflow + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreatePrimaryStackArea(), + new[] + { + new Rectangle(0, 0, 50, 100), + new Rectangle(50, 0, 50, 50), + new Rectangle(50, 50, 50, 50), + }, + }; + } + + public static IEnumerable DoLayout_MultiColumn() + { + // Empty + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + Array.Empty(), + }; + + // Single window + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + new[] { new Rectangle(0, 0, 100, 100), }, + }; + + // Fill primary + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + new[] { new Rectangle(0, 0, 100, 50), new Rectangle(0, 50, 100, 50), }, + }; + + // Fill secondary + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + new[] + { + new Rectangle(0, 0, 50, 50), + new Rectangle(0, 50, 50, 50), + new Rectangle(50, 0, 50, 100), + }, + }; + + // Fill overflow + int third = 100 / 3; + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0 }), + new[] + { + new Rectangle(0, 0, third, 50), + new Rectangle(0, 50, third, 50), + new Rectangle(third, 0, third, 100), + new Rectangle(2 * third, 0, third, third), + new Rectangle(2 * third, third, third, third), + new Rectangle(2 * third, 2 * third, third, third), + }, + }; + } + + public static IEnumerable DoLayout_SecondaryPrimary() + { + // Empty + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + Array.Empty(), + }; + + // Fill primary + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + new[] { new Rectangle(0, 0, 100, 100), }, + }; + + // Fill secondary + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + new[] + { + new Rectangle(38, 0, 62, 100), + new Rectangle(0, 0, 38, 50), + new Rectangle(0, 50, 38, 50), + }, + }; + + // Fill overflow + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + new[] + { + new Rectangle(25, 0, 50, 100), + new Rectangle(0, 0, 25, 50), + new Rectangle(0, 50, 25, 50), + new Rectangle(75, 0, 25, 33), + new Rectangle(75, 33, 25, 33), + new Rectangle(75, 66, 25, 33), + }, + }; + } + + public static IEnumerable DoLayout_OverflowColumn() + { + // Empty + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateOverflowColumnLayout(), + Array.Empty(), + }; + + // Single window + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateOverflowColumnLayout(), + new[] { new Rectangle(0, 0, 100, 100), }, + }; + + // Fill overflow + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateOverflowColumnLayout(), + new[] + { + new Rectangle(0, 0, 100, 25), + new Rectangle(0, 25, 100, 25), + new Rectangle(0, 50, 100, 25), + new Rectangle(0, 75, 100, 25), + }, + }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(DoLayout_PrimaryStack))] + [MemberAutoSubstituteData(nameof(DoLayout_MultiColumn))] + [MemberAutoSubstituteData(nameof(DoLayout_SecondaryPrimary))] + [MemberAutoSubstituteData(nameof(DoLayout_OverflowColumn))] + internal void DoLayout( + IRectangle rectangle, + ParentArea area, + IRectangle[] expectedRectangles, + IContext ctx, + ISliceLayoutPlugin plugin + ) + { + // Given + int windowCount = expectedRectangles.Length; + IWindow[] windows = Enumerable.Range(0, windowCount).Select(i => Substitute.For()).ToArray(); + + IWindowState[] expectedWindowStates = new IWindowState[windowCount]; + for (int i = 0; i < windowCount; i++) + { + expectedWindowStates[i] = new WindowState() + { + Rectangle = expectedRectangles[i], + Window = windows[i], + WindowSize = WindowSize.Normal + }; + } + + ILayoutEngine sliceLayoutEngine = new SliceLayoutEngine(ctx, plugin, identity, area); + + // When + for (int i = 0; i < windowCount; i++) + { + sliceLayoutEngine = sliceLayoutEngine.AddWindow(windows[i]); + } + IWindowState[] windowStates = sliceLayoutEngine.DoLayout(rectangle, Substitute.For()).ToArray(); + + // Then + Assert.Equal(windowCount, windowStates.Length); + expectedWindowStates.Should().BeEquivalentTo(windowStates); + } +} diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index 7f8af24da..28e695d77 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -264,7 +264,7 @@ ParentArea area return windowStartIdx + windowCurrIdx; } - public static int DoSliceLayout( + private static int DoSliceLayout( this SliceRectangleItem[] items, int windowIdx, IRectangle rectangle, diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 2bf269986..9cc4aedde 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -107,12 +107,12 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo // Get the window states IWindowState[] windowStates = new IWindowState[_windows.Count]; - for (int idx = 0; idx < _windows.Count; idx++) + for (int idx = 0; idx < items.Length; idx++) { windowStates[idx] = new WindowState() { Rectangle = items[idx].Rectangle, - Window = _windows[idx], + Window = _windows[items[idx].Index], WindowSize = WindowSize.Normal }; } diff --git a/src/Whim/Whim.csproj b/src/Whim/Whim.csproj index c028eed9d..37adb6e03 100644 --- a/src/Whim/Whim.csproj +++ b/src/Whim/Whim.csproj @@ -56,6 +56,7 @@ + From bb4531fce9dd1cf7ec8c36e594be6b301f6dcee4 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 11:28:37 +1300 Subject: [PATCH 34/64] DoLayout tests --- .../AreaHelpers/DoLayout.cs | 59 +++++++++ src/Whim.SliceLayout/AreaHelpers.cs | 121 ++++++++---------- src/Whim.SliceLayout/SliceLayoutEngine.cs | 7 +- 3 files changed, 119 insertions(+), 68 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs index b08f4f79f..658ac3870 100644 --- a/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs @@ -179,11 +179,70 @@ public static IEnumerable DoLayout_OverflowColumn() }; } + public static IEnumerable DoLayout_Nested() + { + // Empty + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateNestedLayout(), + Array.Empty(), + }; + + // Single window + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateNestedLayout(), + new[] { new Rectangle(0, 0, 100, 100), }, + }; + + // Fill primary + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateNestedLayout(), + new[] { new Rectangle(0, 0, 100, 50), new Rectangle(0, 50, 100, 50), }, + }; + + //Fill secondary + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateNestedLayout(), + new[] + { + new Rectangle(0, 0, 50, 50), + new Rectangle(0, 50, 50, 50), + new Rectangle(50, 0, 50, 50), + new Rectangle(50, 50, 50, 50), + }, + }; + + // Fill overflow + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateNestedLayout(), + new[] + { + new Rectangle(0, 0, 50, 50), + new Rectangle(0, 50, 50, 50), + new Rectangle(50, 0, 50, 25), + new Rectangle(50, 25, 50, 25), + new Rectangle(50, 50, 50, 16), + new Rectangle(50, 66, 50, 16), + new Rectangle(50, 82, 50, 16), + }, + }; + } + [Theory] [MemberAutoSubstituteData(nameof(DoLayout_PrimaryStack))] [MemberAutoSubstituteData(nameof(DoLayout_MultiColumn))] [MemberAutoSubstituteData(nameof(DoLayout_SecondaryPrimary))] [MemberAutoSubstituteData(nameof(DoLayout_OverflowColumn))] + [MemberAutoSubstituteData(nameof(DoLayout_Nested))] internal void DoLayout( IRectangle rectangle, ParentArea area, diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index 28e695d77..5920bfcc7 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -23,12 +23,15 @@ public static ParentArea Prune(this ParentArea area, int windowCount) IArea child = area.Children[i]; if (child is ParentArea parentArea) { - parentArea = parentArea.Prune(windowCount); - if (parentArea.Children.Count == 0) + // Prune the child area. If the child tree has changed, then rebuild the tree. + ParentArea prunedParentArea = parentArea.Prune(windowCount); + if (prunedParentArea.Children.Count == 0) { ignoredWeight += area.Weights[i]; continue; } + + child = prunedParentArea; } else if (child is BaseSliceArea baseSliceArea) { @@ -89,15 +92,10 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI { // Replace the last slice area with an overflow area. Logger.Error($"No overflow area found, replacing last slice area with overflow area"); - (ParentArea, OverflowArea)? result = ReplaceLastSliceAreaWithOverflowArea(rootArea, sliceAreas[^1].Order); - - if (result is null) - { - Logger.Error($"Failed to replace last slice area with overflow area, creating single overflow area"); - return SingleOverflowArea(); - } + SliceArea sliceToReplace = sliceAreas[^1]; - (rootArea, overflowArea) = result.Value; + overflowArea = new(isRow: sliceToReplace.IsRow); + rootArea = RebuildTree(rootArea, sliceToReplace, overflowArea); sliceAreas.RemoveAt(sliceAreas.Count - 1); } @@ -168,64 +166,61 @@ private static (List SliceAreas, OverflowArea? OverflowArea) GetAreas return (sliceAreas, overflowArea); } - private static (ParentArea Parent, OverflowArea Overflow)? ReplaceLastSliceAreaWithOverflowArea( - ParentArea rootArea, - uint lastSliceOrder - ) + /// + /// Use recursive DFS to find the parent area which contains the child area, then replace the + /// child area with the new child area. + /// + /// + /// + /// + /// + private static ParentArea RebuildTree(ParentArea rootArea, IArea oldChild, IArea newChild) { - // Recursive DFS foreach (IArea child in rootArea.Children) { - if (child is ParentArea parentArea) + if (child == oldChild) { - (ParentArea Parent, OverflowArea Overflow)? result = ReplaceLastSliceAreaWithOverflowArea( - parentArea, - lastSliceOrder - ); - if (result is not null) - { - return (ReplaceParentArea(rootArea, parentArea, result.Value.Parent), result.Value.Overflow); - } + return ReplaceNode(rootArea, oldChild, newChild); } - else if (child is SliceArea sliceArea && sliceArea.Order == lastSliceOrder) + else if (child is ParentArea parentArea) { - OverflowArea newOverflow = new(sliceArea.IsRow) { StartIndex = sliceArea.StartIndex }; - - ImmutableList newRootAreaChildren = rootArea.Children; - newRootAreaChildren = newRootAreaChildren.SetItem(newRootAreaChildren.IndexOf(sliceArea), newOverflow); - - return (new ParentArea(isRow: rootArea.IsRow, rootArea.Weights, newRootAreaChildren), newOverflow); + ParentArea newParentArea = RebuildTree(parentArea, oldChild, newChild); + if (newParentArea != parentArea) + { + return ReplaceNode(rootArea, parentArea, newParentArea); + } } } - return null; + return rootArea; } - private static ParentArea ReplaceParentArea(ParentArea rootArea, IArea oldChild, IArea newChild) + private static ParentArea ReplaceNode(ParentArea rootArea, IArea oldChild, IArea newChild) { ImmutableList newRootAreaChildren = rootArea.Children; newRootAreaChildren = newRootAreaChildren.SetItem(newRootAreaChildren.IndexOf(oldChild), newChild); return new ParentArea(isRow: rootArea.IsRow, rootArea.Weights, newRootAreaChildren); } - public static int DoParentLayout( - this SliceRectangleItem[] items, - int windowStartIdx, - IRectangle rectangle, - ParentArea area - ) + /// + /// Perform a general layout of the given . This will recursively layout + /// the children of the , and place the generated rectangles will be + /// placed inside . + /// + ///
+ /// + /// The are in order of the tree, not the order of the windows. + ///
+ /// + /// + /// + public static void DoParentLayout(this ParentArea area, IRectangle rectangle, SliceRectangleItem[] items) { - if (windowStartIdx >= items.Length) - { - return windowStartIdx; - } - int x = rectangle.X; int y = rectangle.Y; int width = rectangle.Width; int height = rectangle.Height; - int windowCurrIdx = 0; for (int childIdx = 0; childIdx < area.Children.Count; childIdx++) { double weight = area.Weights[childIdx]; @@ -244,11 +239,11 @@ ParentArea area if (childArea is ParentArea parentArea) { - windowCurrIdx = items.DoParentLayout(windowStartIdx + windowCurrIdx, childRectangle, parentArea); + parentArea.DoParentLayout(childRectangle, items); } else if (childArea is BaseSliceArea sliceArea) { - windowCurrIdx = items.DoSliceLayout(windowStartIdx + windowCurrIdx, childRectangle, sliceArea); + sliceArea.DoSliceLayout(childRectangle, items); } if (area.IsRow) @@ -260,22 +255,17 @@ ParentArea area y += height; } } - - return windowStartIdx + windowCurrIdx; } - private static int DoSliceLayout( - this SliceRectangleItem[] items, - int windowIdx, - IRectangle rectangle, - BaseSliceArea area - ) + /// + /// Perform a layout of the given . This will place the generated + /// rectangles inside . + /// + /// + /// + /// + private static void DoSliceLayout(this BaseSliceArea area, IRectangle rectangle, SliceRectangleItem[] items) { - if (windowIdx >= items.Length) - { - return windowIdx; - } - int x = rectangle.X; int y = rectangle.Y; int width = rectangle.Width; @@ -284,13 +274,12 @@ BaseSliceArea area int deltaX = 0; int deltaY = 0; - int remainingItemsCount = items.Length - windowIdx; - int sliceItemsCount = remainingItemsCount; + int sliceItemsCount = (int)(items.Length - area.StartIndex); if (area is SliceArea sliceArea) { - sliceItemsCount = Math.Min((int)sliceArea.MaxChildren, remainingItemsCount); + sliceItemsCount = (int)Math.Min(sliceArea.MaxChildren, sliceItemsCount); } - int maxIdx = windowIdx + sliceItemsCount; + int maxIdx = (int)(area.StartIndex + sliceItemsCount); if (area.IsRow) { @@ -303,7 +292,7 @@ BaseSliceArea area height = deltaY; } - for (int windowCurrIdx = windowIdx; windowCurrIdx < maxIdx; windowCurrIdx++) + for (int windowCurrIdx = (int)area.StartIndex; windowCurrIdx < maxIdx; windowCurrIdx++) { items[windowCurrIdx] = new SliceRectangleItem(windowCurrIdx, new Rectangle(x, y, width, height)); @@ -316,7 +305,5 @@ BaseSliceArea area y += deltaY; } } - - return maxIdx; } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 9cc4aedde..17ace5acf 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -4,6 +4,11 @@ namespace Whim.SliceLayout; +/// +/// A rectangle, and the index of the windows to use. +/// +/// +/// internal record SliceRectangleItem(int Index, Rectangle Rectangle); /// @@ -103,7 +108,7 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo // Get the rectangles for each window SliceRectangleItem[] items = new SliceRectangleItem[_windows.Count]; - items.DoParentLayout(0, rectangle, _prunedRootArea); + _prunedRootArea.DoParentLayout(rectangle, items); // Get the window states IWindowState[] windowStates = new IWindowState[_windows.Count]; From 8dc9123a81c9983e78b75e9ae4df7fc02e0a3916 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 15:07:55 +1300 Subject: [PATCH 35/64] Overflow row --- .../AreaHelpers/DoLayout.cs | 34 +++++++++++++++++++ .../SampleSliceLayouts.cs | 11 ++++++ 2 files changed, 45 insertions(+) diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs index 658ac3870..8d69cd431 100644 --- a/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs @@ -179,6 +179,39 @@ public static IEnumerable DoLayout_OverflowColumn() }; } + public static IEnumerable DoLayout_OverflowRow() + { + // Empty + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateOverflowRowLayout(), + Array.Empty(), + }; + + // Single window + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateOverflowRowLayout(), + new[] { new Rectangle(0, 0, 100, 100), }, + }; + + // Fill overflow + yield return new object[] + { + new Rectangle(0, 0, 100, 100), + SampleSliceLayouts.CreateOverflowRowLayout(), + new[] + { + new Rectangle(0, 0, 25, 100), + new Rectangle(25, 0, 25, 100), + new Rectangle(50, 0, 25, 100), + new Rectangle(75, 0, 25, 100), + }, + }; + } + public static IEnumerable DoLayout_Nested() { // Empty @@ -242,6 +275,7 @@ public static IEnumerable DoLayout_Nested() [MemberAutoSubstituteData(nameof(DoLayout_MultiColumn))] [MemberAutoSubstituteData(nameof(DoLayout_SecondaryPrimary))] [MemberAutoSubstituteData(nameof(DoLayout_OverflowColumn))] + [MemberAutoSubstituteData(nameof(DoLayout_OverflowRow))] [MemberAutoSubstituteData(nameof(DoLayout_Nested))] internal void DoLayout( IRectangle rectangle, diff --git a/src/Whim.SliceLayout.Tests/SampleSliceLayouts.cs b/src/Whim.SliceLayout.Tests/SampleSliceLayouts.cs index 78d8ce74e..c9e52dc02 100644 --- a/src/Whim.SliceLayout.Tests/SampleSliceLayouts.cs +++ b/src/Whim.SliceLayout.Tests/SampleSliceLayouts.cs @@ -63,4 +63,15 @@ public static ParentArea CreateNestedLayout() => // | | // -------------------------------- public static ParentArea CreateOverflowColumnLayout() => new(isRow: false, (1.0, new OverflowArea())); + + // ------------------------------------------------------------------ + // | | + // | | + // | | + // | Overflow | + // | | + // | | + // | | + // ------------------------------------------------------------------ + public static ParentArea CreateOverflowRowLayout() => new(isRow: true, (1.0, new OverflowArea(isRow: true))); } From be93f007e068427db7b8ea4e7f0e10f26002ff8c Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 15:24:24 +1300 Subject: [PATCH 36/64] Commands tests --- .../SliceLayoutCommandsTests.cs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/Whim.SliceLayout.Tests/SliceLayoutCommandsTests.cs diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutCommandsTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutCommandsTests.cs new file mode 100644 index 000000000..cca08556f --- /dev/null +++ b/src/Whim.SliceLayout.Tests/SliceLayoutCommandsTests.cs @@ -0,0 +1,74 @@ +using AutoFixture; +using NSubstitute; +using Whim.TestUtils; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class SliceLayoutCommandsCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + ISliceLayoutPlugin plugin = fixture.Freeze(); + plugin.Name.Returns("whim.slicelayout"); + } +} + +public class SliceLayoutCommandsTests +{ + private static ICommand CreateSut(ISliceLayoutPlugin plugin, string id) => + new PluginCommandsTestUtils(new SliceLayoutCommands(plugin)).GetCommand(id); + + [InlineAutoSubstituteData( + "whim.slicelayout.set_insertion_type.swap", + WindowInsertionType.Swap + )] + [InlineAutoSubstituteData( + "whim.slicelayout.set_insertion_type.rotate", + WindowInsertionType.Rotate + )] + [Theory] + public void SetInsertionTypeCommands( + string commandId, + WindowInsertionType windowInsertionType, + ISliceLayoutPlugin plugin + ) + { + // Given + ICommand command = CreateSut(plugin, commandId); + + // When + command.TryExecute(); + + // Then + plugin.Received(1).WindowInsertionType = windowInsertionType; + } + + [Theory] + [AutoSubstituteData] + public void PromoteWindowInStack(ISliceLayoutPlugin plugin) + { + // Given + ICommand command = CreateSut(plugin, "whim.slicelayout.stack.promote"); + + // When + command.TryExecute(); + + // Then + plugin.Received(1).PromoteWindowInStack(); + } + + [Theory] + [AutoSubstituteData] + public void DemoteWindowInStack(ISliceLayoutPlugin plugin) + { + // Given + ICommand command = CreateSut(plugin, "whim.slicelayout.stack.demote"); + + // When + command.TryExecute(); + + // Then + plugin.Received(1).DemoteWindowInStack(); + } +} From d9b8e73261b99a9ee8dd0662a3fbe2039b9df9e4 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 16:35:05 +1300 Subject: [PATCH 37/64] FocusWindowInDirection --- .../FocusWindowInDirectionTests.cs | 98 +++++++++++++++++++ .../SliceLayoutEngineHelpers.cs | 13 +-- 2 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs diff --git a/src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs b/src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs new file mode 100644 index 000000000..1ab070864 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs @@ -0,0 +1,98 @@ +using NSubstitute; +using Whim.TestUtils; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class FocusWindowInDirectionTests +{ + private static readonly LayoutEngineIdentity identity = new(); + + public static IEnumerable FocusWindowInDirection_Data() + { + // Nested, share grandparent, right + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Right, 1, 4 }; + + // Nested, share grandparent, left + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Left, 4, 1 }; + + // Same slice, down + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Down, 0, 1 }; + + // Same slice, up + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Up, 3, 2 }; + + // Last overflow window, top left + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Up | Direction.Left, 5, 1 }; + + // Slice 1, down across slices + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Down, 3, 4 }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(FocusWindowInDirection_Data))] + public void FocusWindowInDirection( + ParentArea parentArea, + int windowCount, + Direction direction, + int focusedWindowIdx, + int expectedWindowIdx, + IContext ctx, + ISliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, parentArea); + IWindow[] windows = Enumerable.Range(0, windowCount).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + sut.FocusWindowInDirection(direction, windows[focusedWindowIdx]); + + // Then + windows[expectedWindowIdx].Received(1).Focus(); + } + + public static IEnumerable FocusWindowInDirection_NoWindowInDirection_Data() + { + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Left, 1 }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Right, 2 }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Up, 0 }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Down, 1 }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Down, 5 }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(FocusWindowInDirection_NoWindowInDirection_Data))] + public void FocusWindowInDirection_NoWindowInDirection( + ParentArea parentArea, + int windowCount, + Direction direction, + int focusedWindowIdx, + IContext ctx, + ISliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, parentArea); + IWindow[] windows = Enumerable.Range(0, windowCount).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + sut.FocusWindowInDirection(direction, windows[focusedWindowIdx]); + + // Then + foreach (IWindow window in windows) + { + window.DidNotReceive().Focus(); + } + } +} diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs index dc9311205..5b23cd874 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -36,10 +36,10 @@ private IWindowState[] GetLazyWindowStates() // Figure out the adjacent point of the window IWindowState[] windowStates = GetLazyWindowStates(); IRectangle rect = windowStates[index].Rectangle; - double x = rect.X; - double y = rect.Y; + int x = rect.X; + int y = rect.Y; - double delta = 1d / _cachedWindowStatesScale; + int delta = 1; if (direction.HasFlag(Direction.Left)) { x -= delta; @@ -59,13 +59,10 @@ private IWindowState[] GetLazyWindowStates() } // Get the window at that point + Point point = new(x, y); foreach (IWindowState windowState in windowStates) { - if ( - windowState.Rectangle.ContainsPoint( - new Point((int)(x * _cachedWindowStatesScale), (int)(y * _cachedWindowStatesScale)) - ) - ) + if (windowState.Rectangle.ContainsPoint(point)) { return windowState.Window; } From 7bc6caec76f18ea43a8ea135ce863bea97425dbd Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 16:38:34 +1300 Subject: [PATCH 38/64] Handle lazy --- src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs b/src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs index 1ab070864..ad5ad9e49 100644 --- a/src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs +++ b/src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs @@ -51,10 +51,11 @@ ISliceLayoutPlugin plugin sut = sut.AddWindow(window); } + sut.FocusWindowInDirection(direction, windows[focusedWindowIdx]); sut.FocusWindowInDirection(direction, windows[focusedWindowIdx]); // Then - windows[expectedWindowIdx].Received(1).Focus(); + windows[expectedWindowIdx].Received(2).Focus(); } public static IEnumerable FocusWindowInDirection_NoWindowInDirection_Data() From 8a84c3d96ad870f5a5ca4527114677dd182cc3d6 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 20:00:19 +1300 Subject: [PATCH 39/64] SwapWindowInDirection --- .../SwapWindowInDirectionTests.cs | 130 ++++++++++++++++++ .../SliceLayoutEngineHelpers.cs | 2 +- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/Whim.SliceLayout.Tests/SwapWindowInDirectionTests.cs diff --git a/src/Whim.SliceLayout.Tests/SwapWindowInDirectionTests.cs b/src/Whim.SliceLayout.Tests/SwapWindowInDirectionTests.cs new file mode 100644 index 000000000..743315a3b --- /dev/null +++ b/src/Whim.SliceLayout.Tests/SwapWindowInDirectionTests.cs @@ -0,0 +1,130 @@ +using NSubstitute; +using Whim.TestUtils; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class SwapWindowInDirectionTests +{ + private static readonly LayoutEngineIdentity identity = new(); + + public static IEnumerable SwapWindowInDirection_Swap_Data() + { + // Nested, share grandparent, right + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Right, 1, 4 }; + + // Nested, share grandparent, left + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Left, 4, 1 }; + + // Same slice, down + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Down, 0, 1 }; + + // Same slice, up + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Up, 3, 2 }; + + // Last overflow window, top left + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Up | Direction.Left, 5, 1 }; + + // Slice 1, down across slices + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Down, 3, 4 }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(SwapWindowInDirection_Swap_Data))] + public void SwapWindowInDirection_Swap( + ParentArea parentArea, + int windowCount, + Direction direction, + int focusedWindowIdx, + int targetWindowIdx, + IContext ctx, + ISliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, parentArea); + IWindow[] windows = Enumerable.Range(0, windowCount).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + sut = sut.SwapWindowInDirection(direction, windows[focusedWindowIdx]); + IWindowState[] windowStates = sut.DoLayout( + new Rectangle(0, 0, 100, 100), + ctx.MonitorManager.PrimaryMonitor + ) + .ToArray(); + + // Then + Assert.Equal(windows[focusedWindowIdx], windowStates[targetWindowIdx].Window); + Assert.Equal(windows[targetWindowIdx], windowStates[focusedWindowIdx].Window); + } + + public static IEnumerable SwapWindowInDirection_Rotate_Data() + { + // Nested, share grandparent, right + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Right, 1, 4, 3 }; + + // Nested, share grandparent, left + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Left, 4, 1, 2 }; + + // Same slice, down + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Down, 0, 1, 0 }; + + // Same slice, up + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Up, 3, 2, 3 }; + + // Last overflow window, top left + yield return new object[] + { + SampleSliceLayouts.CreateNestedLayout(), + 6, + Direction.Up | Direction.Left, + 5, + 1, + 2 + }; + + // Slice 1, down across slices + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, Direction.Down, 3, 4, 3 }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(SwapWindowInDirection_Rotate_Data))] + public void SwapWindowInDirection_Rotate( + ParentArea parentArea, + int windowCount, + Direction direction, + int focusedWindowIdx, + int targetWindowIdx, + int targetWindowLocationIdx, + IContext ctx, + ISliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, parentArea); + IWindow[] windows = Enumerable.Range(0, windowCount).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + plugin.WindowInsertionType.Returns(WindowInsertionType.Rotate); + sut = sut.SwapWindowInDirection(direction, windows[focusedWindowIdx]); + IWindowState[] windowStates = sut.DoLayout( + new Rectangle(0, 0, 100, 100), + ctx.MonitorManager.PrimaryMonitor + ) + .ToArray(); + + // Then + Assert.Equal(windows[focusedWindowIdx], windowStates[targetWindowIdx].Window); + Assert.Equal(windows[targetWindowIdx], windowStates[targetWindowLocationIdx].Window); + } +} diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs index 5b23cd874..9e55c6f56 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -111,7 +111,7 @@ private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) } IWindow currentWindow = _windows[currentIndex]; - ImmutableList newWindows = _windows.Insert(targetIndex, currentWindow).RemoveAt(currentIndex); + ImmutableList newWindows = _windows.RemoveAt(currentIndex).Insert(targetIndex, currentWindow); return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); } From ae673635a58c32d4a885b0951d2f548db23585a9 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 20:24:12 +1300 Subject: [PATCH 40/64] Plugin coverage --- .../SliceLayoutPluginTests.cs | 194 ++++++++++++++++++ src/Whim.SliceLayout/SliceLayoutPlugin.cs | 1 - src/Whim.TestUtils/Assert.cs | 31 +++ src/Whim.TestUtils/Whim.TestUtils.csproj | 4 + .../Native/DeferWindowPosHandleTests.cs | 2 + .../TreeLayoutPluginTests.cs | 9 +- 6 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs new file mode 100644 index 000000000..1db298f23 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs @@ -0,0 +1,194 @@ +using System.Text.Json; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Whim.TestUtils; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class SliceLayoutPluginTests +{ + [Theory, AutoSubstituteData] + public void Name(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + + // When + string name = plugin.Name; + + // Then + Assert.Equal("whim.slice_layout", name); + } + + [Theory, AutoSubstituteData] + public void PluginCommands(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + + // When + IPluginCommands commands = plugin.PluginCommands; + + // Then + Assert.NotEmpty(commands.Commands); + } + + [Theory, AutoSubstituteData] + public void PreInitialize(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + + // When + plugin.PreInitialize(); + + // Then nothing + CustomAssert.NoContextCalls(ctx); + } + + [Theory, AutoSubstituteData] + public void PostInitialize(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + + // When + plugin.PostInitialize(); + + // Then nothing + CustomAssert.NoContextCalls(ctx); + } + + [Theory, AutoSubstituteData] + public void LoadState(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + + // When + plugin.LoadState(default); + + // Then nothing + CustomAssert.NoContextCalls(ctx); + } + + [Theory, AutoSubstituteData] + public void SaveState(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + + // When + JsonElement? state = plugin.SaveState(); + + // Then + Assert.Null(state); + } + + #region PromoteWindowInStack + [Theory, AutoSubstituteData] + public void PromoteWindowInStack_NoWindow(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.ReturnsNull(); + + // When + plugin.PromoteWindowInStack(); + + // Then nothing + ctx.WorkspaceManager.DidNotReceive().GetWorkspaceForWindow(Arg.Any()); + } + + [Theory, AutoSubstituteData] + public void PromoteWindowInStack_NoWorkspace(IContext ctx, IWindow window) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.Returns(window); + ctx.WorkspaceManager.GetWorkspaceForWindow(window).ReturnsNull(); + + // When + plugin.PromoteWindowInStack(window); + + // Then nothing + ctx.WorkspaceManager.ActiveWorkspace.DidNotReceive() + .PerformCustomLayoutEngineAction(Arg.Any()); + } + + [Theory, AutoSubstituteData] + public void PromoteWindowInStack(IContext ctx, IWindow window, IWorkspace workspace) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.Returns(window); + ctx.WorkspaceManager.GetWorkspaceForWindow(window).Returns(workspace); + + // When + plugin.PromoteWindowInStack(window); + + // Then + workspace + .Received(1) + .PerformCustomLayoutEngineAction( + Arg.Is( + action => action.Name == plugin.PromoteActionName && action.Window == window + ) + ); + } + #endregion + + #region DemoteWindowInStack + [Theory, AutoSubstituteData] + public void DemoteWindowInStack_NoWindow(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.ReturnsNull(); + + // When + plugin.DemoteWindowInStack(); + + // Then nothing + ctx.WorkspaceManager.DidNotReceive().GetWorkspaceForWindow(Arg.Any()); + } + + [Theory, AutoSubstituteData] + public void DemoteWindowInStack_NoWorkspace(IContext ctx, IWindow window) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.Returns(window); + ctx.WorkspaceManager.GetWorkspaceForWindow(window).ReturnsNull(); + + // When + plugin.DemoteWindowInStack(window); + + // Then nothing + ctx.WorkspaceManager.ActiveWorkspace.DidNotReceive() + .PerformCustomLayoutEngineAction(Arg.Any()); + } + + [Theory, AutoSubstituteData] + public void DemoteWindowInStack(IContext ctx, IWindow window, IWorkspace workspace) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.Returns(window); + ctx.WorkspaceManager.GetWorkspaceForWindow(window).Returns(workspace); + + // When + plugin.DemoteWindowInStack(window); + + // Then + workspace + .Received(1) + .PerformCustomLayoutEngineAction( + Arg.Is( + action => action.Name == plugin.DemoteActionName && action.Window == window + ) + ); + } + #endregion +} diff --git a/src/Whim.SliceLayout/SliceLayoutPlugin.cs b/src/Whim.SliceLayout/SliceLayoutPlugin.cs index 1d32d9d96..c04c237ba 100644 --- a/src/Whim.SliceLayout/SliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/SliceLayoutPlugin.cs @@ -17,7 +17,6 @@ public SliceLayoutPlugin(IContext context) public string DemoteActionName => $"{Name}.stack.demote"; - // TODO: Bar extension issue public IPluginCommands PluginCommands => new SliceLayoutCommands(this); public WindowInsertionType WindowInsertionType { get; set; } diff --git a/src/Whim.TestUtils/Assert.cs b/src/Whim.TestUtils/Assert.cs index c569c128a..a3f39a900 100644 --- a/src/Whim.TestUtils/Assert.cs +++ b/src/Whim.TestUtils/Assert.cs @@ -1,5 +1,7 @@ using System; using System.ComponentModel; +using NSubstitute; +using Xunit; using Xunit.Sdk; namespace Whim.TestUtils; @@ -88,4 +90,33 @@ void handler(object? sender, PropertyChangedEventArgs e) throw new DoesNotRaiseException(typeof(PropertyChangedEventArgs)); } } + + public static void NoContextCalls(IContext ctx) + { + Assert.Empty(ctx.ResourceManager.ReceivedCalls()); + Assert.Empty(ctx.WorkspaceManager.ReceivedCalls()); + Assert.Empty(ctx.WindowManager.ReceivedCalls()); + Assert.Empty(ctx.MonitorManager.ReceivedCalls()); + Assert.Empty(ctx.RouterManager.ReceivedCalls()); + Assert.Empty(ctx.FilterManager.ReceivedCalls()); + Assert.Empty(ctx.CommandManager.ReceivedCalls()); + Assert.Empty(ctx.KeybindManager.ReceivedCalls()); + Assert.Empty(ctx.PluginManager.ReceivedCalls()); + Assert.Empty(ctx.NativeManager.ReceivedCalls()); + Assert.Empty(ctx.FileManager.ReceivedCalls()); + Assert.Empty(ctx.NotificationManager.ReceivedCalls()); + } + + internal static void NoInternalContextCalls(IInternalContext internalCtx) + { + Assert.Empty(internalCtx.CoreSavedStateManager.ReceivedCalls()); + Assert.Empty(internalCtx.CoreNativeManager.ReceivedCalls()); + Assert.Empty(internalCtx.WindowMessageMonitor.ReceivedCalls()); + Assert.Empty(internalCtx.WorkspaceManager.ReceivedCalls()); + Assert.Empty(internalCtx.MonitorManager.ReceivedCalls()); + Assert.Empty(internalCtx.WindowManager.ReceivedCalls()); + Assert.Empty(internalCtx.KeybindHook.ReceivedCalls()); + Assert.Empty(internalCtx.MouseHook.ReceivedCalls()); + Assert.Empty(internalCtx.DeferWindowPosManager.ReceivedCalls()); + } } diff --git a/src/Whim.TestUtils/Whim.TestUtils.csproj b/src/Whim.TestUtils/Whim.TestUtils.csproj index 1b70f3cc1..1c0282de9 100644 --- a/src/Whim.TestUtils/Whim.TestUtils.csproj +++ b/src/Whim.TestUtils/Whim.TestUtils.csproj @@ -41,4 +41,8 @@ + + + + \ No newline at end of file diff --git a/src/Whim.Tests/Native/DeferWindowPosHandleTests.cs b/src/Whim.Tests/Native/DeferWindowPosHandleTests.cs index fd5920391..f9d45459c 100644 --- a/src/Whim.Tests/Native/DeferWindowPosHandleTests.cs +++ b/src/Whim.Tests/Native/DeferWindowPosHandleTests.cs @@ -80,6 +80,8 @@ internal void Dispose_NoWindows(IContext ctx, IInternalContext internalCtx) handle.Dispose(); // Then nothing happens + CustomAssert.NoContextCalls(ctx); + CustomAssert.NoInternalContextCalls(internalCtx); } [Theory, AutoSubstituteData] diff --git a/src/Whim.TreeLayout.Tests/TreeLayoutPluginTests.cs b/src/Whim.TreeLayout.Tests/TreeLayoutPluginTests.cs index 954555bc5..4da51f3b0 100644 --- a/src/Whim.TreeLayout.Tests/TreeLayoutPluginTests.cs +++ b/src/Whim.TreeLayout.Tests/TreeLayoutPluginTests.cs @@ -43,8 +43,7 @@ public void PreInitialize(IContext ctx) plugin.PreInitialize(); // Then nothing - Assert.Empty(ctx.WorkspaceManager.ReceivedCalls()); - Assert.Empty(ctx.WindowManager.ReceivedCalls()); + CustomAssert.NoContextCalls(ctx); } [Theory, AutoSubstituteData] @@ -57,8 +56,7 @@ public void PostInitialize(IContext ctx) plugin.PostInitialize(); // Then nothing - Assert.Empty(ctx.WorkspaceManager.ReceivedCalls()); - Assert.Empty(ctx.WindowManager.ReceivedCalls()); + CustomAssert.NoContextCalls(ctx); } #region GetAddWindowDirection @@ -275,8 +273,7 @@ public void LoadState(IContext ctx) plugin.LoadState(JsonDocument.Parse("{}").RootElement); // Then nothing - Assert.Empty(ctx.WorkspaceManager.ReceivedCalls()); - Assert.Empty(ctx.WindowManager.ReceivedCalls()); + CustomAssert.NoContextCalls(ctx); } [Theory, AutoSubstituteData] From 822898ed8b258ebb10e916f4c604f4421baf85a6 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 23:20:44 +1300 Subject: [PATCH 41/64] More tests --- .../FocusWindowInDirectionTests.cs | 0 .../SwapWindowInDirectionTests.cs | 0 .../SliceLayoutPluginTests.cs | 17 +++++++++++++++++ 3 files changed, 17 insertions(+) rename src/Whim.SliceLayout.Tests/{ => SliceLayoutEngine}/FocusWindowInDirectionTests.cs (100%) rename src/Whim.SliceLayout.Tests/{ => SliceLayoutEngine}/SwapWindowInDirectionTests.cs (100%) diff --git a/src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/FocusWindowInDirectionTests.cs similarity index 100% rename from src/Whim.SliceLayout.Tests/FocusWindowInDirectionTests.cs rename to src/Whim.SliceLayout.Tests/SliceLayoutEngine/FocusWindowInDirectionTests.cs diff --git a/src/Whim.SliceLayout.Tests/SwapWindowInDirectionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs similarity index 100% rename from src/Whim.SliceLayout.Tests/SwapWindowInDirectionTests.cs rename to src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs index 1db298f23..0526ff053 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs @@ -191,4 +191,21 @@ public void DemoteWindowInStack(IContext ctx, IWindow window, IWorkspace workspa ); } #endregion + + [Theory] + [InlineAutoSubstituteData(WindowInsertionType.Swap)] + [InlineAutoSubstituteData(WindowInsertionType.Rotate)] + public void WindowInsertionType_Set(WindowInsertionType insertionType) + { + // Given + SliceLayoutPlugin plugin = + new(Substitute.For()) + { + // When + WindowInsertionType = insertionType + }; + + // Then + Assert.Equal(insertionType, plugin.WindowInsertionType); + } } From e9002d792a392391f8aa49b630064f17cd13c406 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Sun, 24 Dec 2023 23:43:50 +1300 Subject: [PATCH 42/64] MoveWindowToPoint tests --- .../MoveWindowToPointTests.cs | 48 +++++++++++++++++++ .../SliceLayoutEngineHelpers.cs | 9 ++++ 2 files changed, 57 insertions(+) create mode 100644 src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs new file mode 100644 index 000000000..17997baef --- /dev/null +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs @@ -0,0 +1,48 @@ +using NSubstitute; +using Whim.TestUtils; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class MoveWindowToPointTests +{ + private static readonly LayoutEngineIdentity identity = new(); + + public static IEnumerable MoveWindowToPoint_Data() + { + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 1, new Point(0.7, 0.7), 4 }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 1, new Point(0.3, 0.3), 0 }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 1, new Point(0.3, 0.7), 1 }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 3, new Point(0, 0), 0 }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(MoveWindowToPoint_Data))] + public void MoveWindowToPoint( + ParentArea parentArea, + int windowCount, + int windowIdx, + IPoint point, + int expectedWindowIdx, + IContext ctx, + ISliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, parentArea); + IWindow[] windows = Enumerable.Range(0, windowCount).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + sut = sut.MoveWindowToPoint(windows[windowIdx], point); + IWindowState[] windowStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + // Then + Assert.Equal(windows[expectedWindowIdx], windowStates[windowIdx].Window); + } +} diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs index 9e55c6f56..b53a27047 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -115,6 +115,15 @@ private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); } + /// + /// Does a linear search through the window states to find the window at the given point. + /// If the point is not in any window, then is returned. + /// + /// We do a linear search here, because we need to get the window index, and the position inside + /// the tree doesn't correspond to the window index. + /// + /// + /// private (int Index, IWindow Window)? GetWindowAtPoint(IPoint point) { Logger.Debug($"Getting window at {point}"); From 490618a9a469a3d398b7d187a01ada1b1f21de75 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 00:27:01 +1300 Subject: [PATCH 43/64] PerformCustomAction tests --- .../PerformCustomActionTests.cs | 208 ++++++++++++++++++ src/Whim.SliceLayout/SliceLayoutEngine.cs | 6 +- 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs new file mode 100644 index 000000000..e6012b901 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs @@ -0,0 +1,208 @@ +using FluentAssertions; +using NSubstitute; +using Whim.TestUtils; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class PerformCustomActionTests +{ + private static readonly LayoutEngineIdentity identity = new(); + + #region PromoteWindowInStack + [Theory, AutoSubstituteData] + public void PromoteWindowInStack_CannotFindWindow(IContext ctx, SliceLayoutPlugin plugin, IWindow untrackedWindow) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + IWindowState[] beforeStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + sut = sut.PerformCustomAction( + new LayoutEngineCustomAction() + { + Name = "whim.slice_layout.stack.promote", + Window = untrackedWindow, + Payload = untrackedWindow + } + ); + + IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + // Then + beforeStates.Should().BeEquivalentTo(afterStates); + } + + public static IEnumerable PromoteWindowInStack_Data() + { + // Already highest area + yield return new object[] { 0, 0 }; + + // Promote to higher area, slice to slice + yield return new object[] { 2, 1 }; + + // Promote to higher area, overflow to slice + yield return new object[] { 5, 3 }; + + // Promote to higher area, slice to slice + yield return new object[] { 3, 1 }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(PromoteWindowInStack_Data))] + public void PromoteWindowInStack( + int focusedWindowIdx, + int expectedWindowIdx, + IContext ctx, + SliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + sut = sut.PerformCustomAction( + new LayoutEngineCustomAction() + { + Name = "whim.slice_layout.stack.promote", + Window = windows[focusedWindowIdx], + Payload = windows[focusedWindowIdx] + } + ); + + IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + // Then + Assert.Equal(windows[expectedWindowIdx], afterStates[focusedWindowIdx].Window); + Assert.Equal(windows[focusedWindowIdx], afterStates[expectedWindowIdx].Window); + } + #endregion + + #region DemoteWindowInStack + [Theory, AutoSubstituteData] + public void DemoteWindowInStack_CannotFindWindow(IContext ctx, SliceLayoutPlugin plugin, IWindow untrackedWindow) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + IWindowState[] beforeStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + sut = sut.PerformCustomAction( + new LayoutEngineCustomAction() + { + Name = "whim.slice_layout.stack.demote", + Window = untrackedWindow, + Payload = untrackedWindow + } + ); + + IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + // Then + beforeStates.Should().BeEquivalentTo(afterStates); + } + + public static IEnumerable DemoteWindowInStack_Data() + { + // Already lowest area + yield return new object[] { 5, 5 }; + + // Demote to lower area, slice to slice + yield return new object[] { 1, 2 }; + + // Demote to lower area, slice to overflow + yield return new object[] { 3, 4 }; + + // Demote to lower area, slice to slice + yield return new object[] { 1, 2 }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(DemoteWindowInStack_Data))] + public void DemoteWindowInStack(int focusedWindowIdx, int expectedWindowIdx, IContext ctx, SliceLayoutPlugin plugin) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + sut = sut.PerformCustomAction( + new LayoutEngineCustomAction() + { + Name = "whim.slice_layout.stack.demote", + Window = windows[focusedWindowIdx], + Payload = windows[focusedWindowIdx] + } + ); + + IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + // Then + Assert.Equal(windows[expectedWindowIdx], afterStates[focusedWindowIdx].Window); + Assert.Equal(windows[focusedWindowIdx], afterStates[expectedWindowIdx].Window); + } + #endregion + + [Theory, AutoSubstituteData] + public void PerformCustomAction_UnknownAction(IContext ctx, SliceLayoutPlugin plugin) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + IWindowState[] beforeStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + sut = sut.PerformCustomAction( + new LayoutEngineCustomAction() + { + Name = "whim.slice_layout.stack.unknown", + Window = windows[0], + Payload = windows[0] + } + ); + + IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + // Then + beforeStates.Should().BeEquivalentTo(afterStates); + } +} diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 17ace5acf..0f5a7bf13 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -173,8 +173,9 @@ private ILayoutEngine PromoteWindowInStack(IWindow window) Logger.Debug($"Promoting {window} in stack"); int windowIndex = _windows.IndexOf(window); - if (windowIndex == 0) + if (windowIndex == -1) { + Logger.Error($"Could not find {window} in stack"); return this; } @@ -182,6 +183,7 @@ private ILayoutEngine PromoteWindowInStack(IWindow window) int areaIndex = GetAreaStackForWindowIndex(windowIndex); if (areaIndex <= 0) { + Logger.Error($"Could not promote {window} to higher area"); return this; } @@ -198,6 +200,7 @@ private ILayoutEngine DemoteWindowInStack(IWindow window) int windowIndex = _windows.IndexOf(window); if (windowIndex == _windows.Count - 1) { + Logger.Error($"Could not find {window} in stack"); return this; } @@ -205,6 +208,7 @@ private ILayoutEngine DemoteWindowInStack(IWindow window) int areaIndex = GetAreaStackForWindowIndex(windowIndex); if (areaIndex > _windowAreas.Length - 2 || areaIndex == -1) { + Logger.Error($"Could not demote {window} to lower area"); return this; } From e3f6601dac8b0a41ab2e2bedbaf676bda34bba8e Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 11:09:12 +1300 Subject: [PATCH 44/64] More tests --- .../MoveWindowToPointTests.cs | 37 +++++ .../PerformCustomActionTests.cs | 73 +++++++-- .../SliceLayoutEngineTests.cs | 144 ++++++++++++++++++ .../SwapWindowInDirectionTests.cs | 30 ++-- src/Whim.SliceLayout/SliceLayoutEngine.cs | 2 +- 5 files changed, 259 insertions(+), 27 deletions(-) create mode 100644 src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs index 17997baef..69b076922 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs @@ -45,4 +45,41 @@ ISliceLayoutPlugin plugin // Then Assert.Equal(windows[expectedWindowIdx], windowStates[windowIdx].Window); } + + public static IEnumerable MoveWindowToPoint_InvalidPoint_Data() + { + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 1, new Point(-0.1, 0.7) }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 1, new Point(0.7, -0.1) }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 1, new Point(1.1, 0.7) }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 1, new Point(0.7, 1.1) }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(MoveWindowToPoint_InvalidPoint_Data))] + public void MoveWindowToPoint_InvalidPoint( + ParentArea parentArea, + int windowCount, + int windowIdx, + IPoint point, + IContext ctx, + ISliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, parentArea); + IWindow[] windows = Enumerable.Range(0, windowCount).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + sut = sut.MoveWindowToPoint(windows[windowIdx], point); + IWindowState[] windowStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + // Then the window should not have moved + Assert.Equal(windows[windowIdx], windowStates[windowIdx].Window); + } } diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs index e6012b901..1b15ffa8e 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs @@ -8,6 +8,7 @@ namespace Whim.SliceLayout.Tests; public class PerformCustomActionTests { private static readonly LayoutEngineIdentity identity = new(); + private static readonly IRectangle primaryMonitorBounds = new Rectangle(0, 0, 100, 100); #region PromoteWindowInStack [Theory, AutoSubstituteData] @@ -23,8 +24,7 @@ public void PromoteWindowInStack_CannotFindWindow(IContext ctx, SliceLayoutPlugi sut = sut.AddWindow(window); } - IWindowState[] beforeStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) - .ToArray(); + IWindowState[] beforeStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); sut = sut.PerformCustomAction( new LayoutEngineCustomAction() @@ -35,8 +35,7 @@ public void PromoteWindowInStack_CannotFindWindow(IContext ctx, SliceLayoutPlugi } ); - IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) - .ToArray(); + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); // Then beforeStates.Should().BeEquivalentTo(afterStates); @@ -85,13 +84,36 @@ SliceLayoutPlugin plugin } ); - IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) - .ToArray(); + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); // Then Assert.Equal(windows[expectedWindowIdx], afterStates[focusedWindowIdx].Window); Assert.Equal(windows[focusedWindowIdx], afterStates[expectedWindowIdx].Window); } + + [Theory, AutoSubstituteData] + public void PromoteWindowInStack_EmptyLayoutEngine(IContext ctx, SliceLayoutPlugin plugin, IWindow window) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + + // When + IWindowState[] beforeStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); + + sut = sut.PerformCustomAction( + new LayoutEngineCustomAction() + { + Name = "whim.slice_layout.stack.promote", + Window = window, + Payload = window + } + ); + + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); + + // Then + beforeStates.Should().BeEquivalentTo(afterStates); + } #endregion #region DemoteWindowInStack @@ -108,8 +130,7 @@ public void DemoteWindowInStack_CannotFindWindow(IContext ctx, SliceLayoutPlugin sut = sut.AddWindow(window); } - IWindowState[] beforeStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) - .ToArray(); + IWindowState[] beforeStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); sut = sut.PerformCustomAction( new LayoutEngineCustomAction() @@ -120,8 +141,7 @@ public void DemoteWindowInStack_CannotFindWindow(IContext ctx, SliceLayoutPlugin } ); - IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) - .ToArray(); + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); // Then beforeStates.Should().BeEquivalentTo(afterStates); @@ -165,13 +185,36 @@ public void DemoteWindowInStack(int focusedWindowIdx, int expectedWindowIdx, ICo } ); - IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) - .ToArray(); + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); // Then Assert.Equal(windows[expectedWindowIdx], afterStates[focusedWindowIdx].Window); Assert.Equal(windows[focusedWindowIdx], afterStates[expectedWindowIdx].Window); } + + [Theory, AutoSubstituteData] + public void DemoteWindowInStack_EmptyLayoutEngine(IContext ctx, SliceLayoutPlugin plugin, IWindow window) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + + // When + IWindowState[] beforeStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); + + sut = sut.PerformCustomAction( + new LayoutEngineCustomAction() + { + Name = "whim.slice_layout.stack.demote", + Window = window, + Payload = window + } + ); + + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); + + // Then + beforeStates.Should().BeEquivalentTo(afterStates); + } #endregion [Theory, AutoSubstituteData] @@ -187,8 +230,7 @@ public void PerformCustomAction_UnknownAction(IContext ctx, SliceLayoutPlugin pl sut = sut.AddWindow(window); } - IWindowState[] beforeStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) - .ToArray(); + IWindowState[] beforeStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); sut = sut.PerformCustomAction( new LayoutEngineCustomAction() @@ -199,8 +241,7 @@ public void PerformCustomAction_UnknownAction(IContext ctx, SliceLayoutPlugin pl } ); - IWindowState[] afterStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) - .ToArray(); + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); // Then beforeStates.Should().BeEquivalentTo(afterStates); diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs new file mode 100644 index 000000000..e922d8473 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs @@ -0,0 +1,144 @@ +using NSubstitute; +using Whim.TestUtils; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class SliceLayoutEngineTests +{ + [Theory, AutoSubstituteData] + public void Name(IContext ctx, SliceLayoutPlugin plugin) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine( + ctx, + plugin, + new LayoutEngineIdentity(), + SampleSliceLayouts.CreateNestedLayout() + ); + + // When + string name = sut.Name; + + // Then + Assert.Equal("Slice", name); + } + + [Theory] + [InlineAutoSubstituteData(0)] + [InlineAutoSubstituteData(1)] + [InlineAutoSubstituteData(5)] + public void Count(int windowCount, IContext ctx, SliceLayoutPlugin plugin) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine( + ctx, + plugin, + new LayoutEngineIdentity(), + SampleSliceLayouts.CreateNestedLayout() + ); + + // When + foreach (IWindow window in Enumerable.Range(0, windowCount).Select(_ => Substitute.For())) + { + sut = sut.AddWindow(window); + } + + // Then + Assert.Equal(windowCount, sut.Count); + } + + [Theory] + [InlineAutoSubstituteData(0, 1)] + [InlineAutoSubstituteData(1, 0)] + [InlineAutoSubstituteData(5, 3)] + public void RemoveWindow(int addCount, int removeCount, IContext ctx, SliceLayoutPlugin plugin) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine( + ctx, + plugin, + new LayoutEngineIdentity(), + SampleSliceLayouts.CreateNestedLayout() + ); + + IWindow[] windows = Enumerable.Range(0, addCount).Select(_ => Substitute.For()).ToArray(); + + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + // When + foreach (IWindow window in windows.Take(removeCount)) + { + sut = sut.RemoveWindow(window); + } + + // Then + Assert.Equal(Math.Max(0, addCount - removeCount), sut.Count); + } + + [Theory, AutoSubstituteData] + public void ContainsWindow_True(IContext ctx, SliceLayoutPlugin plugin, IWindow window) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine( + ctx, + plugin, + new LayoutEngineIdentity(), + SampleSliceLayouts.CreateNestedLayout() + ); + + sut = sut.AddWindow(window); + + // When + bool contains = sut.ContainsWindow(window); + + // Then + Assert.True(contains); + } + + [Theory, AutoSubstituteData] + public void ContainsWindow_False(IContext ctx, SliceLayoutPlugin plugin, IWindow window) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine( + ctx, + plugin, + new LayoutEngineIdentity(), + SampleSliceLayouts.CreateNestedLayout() + ); + + // When + bool contains = sut.ContainsWindow(window); + + // Then + Assert.False(contains); + } + + [Theory, AutoSubstituteData] + public void GetFirstWindow(IContext ctx, SliceLayoutPlugin plugin) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine( + ctx, + plugin, + new LayoutEngineIdentity(), + SampleSliceLayouts.CreateNestedLayout() + ); + + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + // When + IWindow? firstWindow = sut.GetFirstWindow(); + + // Then + Assert.Equal(windows[0], firstWindow); + } +} diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs index 743315a3b..34a5884de 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs @@ -7,6 +7,7 @@ namespace Whim.SliceLayout.Tests; public class SwapWindowInDirectionTests { private static readonly LayoutEngineIdentity identity = new(); + private static readonly IRectangle primaryMonitorBounds = new Rectangle(0, 0, 100, 100); public static IEnumerable SwapWindowInDirection_Swap_Data() { @@ -52,11 +53,7 @@ ISliceLayoutPlugin plugin } sut = sut.SwapWindowInDirection(direction, windows[focusedWindowIdx]); - IWindowState[] windowStates = sut.DoLayout( - new Rectangle(0, 0, 100, 100), - ctx.MonitorManager.PrimaryMonitor - ) - .ToArray(); + IWindowState[] windowStates = sut.DoLayout(primaryMonitorBounds, ctx.MonitorManager.PrimaryMonitor).ToArray(); // Then Assert.Equal(windows[focusedWindowIdx], windowStates[targetWindowIdx].Window); @@ -117,14 +114,27 @@ ISliceLayoutPlugin plugin plugin.WindowInsertionType.Returns(WindowInsertionType.Rotate); sut = sut.SwapWindowInDirection(direction, windows[focusedWindowIdx]); - IWindowState[] windowStates = sut.DoLayout( - new Rectangle(0, 0, 100, 100), - ctx.MonitorManager.PrimaryMonitor - ) - .ToArray(); + IWindowState[] windowStates = sut.DoLayout(primaryMonitorBounds, ctx.MonitorManager.PrimaryMonitor).ToArray(); // Then Assert.Equal(windows[focusedWindowIdx], windowStates[targetWindowIdx].Window); Assert.Equal(windows[targetWindowIdx], windowStates[targetWindowLocationIdx].Window); } + + [Theory, AutoSubstituteData] + public void SwapWindowInDirection_NoWindowInDirection(IContext ctx, ISliceLayoutPlugin plugin, IWindow window) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + + // When + IWindowState[] beforeStates = sut.DoLayout(primaryMonitorBounds, ctx.MonitorManager.PrimaryMonitor).ToArray(); + + sut = sut.SwapWindowInDirection(Direction.Up, window); + + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, ctx.MonitorManager.PrimaryMonitor).ToArray(); + + // Then + Assert.Equal(beforeStates, afterStates); + } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 0f5a7bf13..2f4e8bc51 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -28,7 +28,7 @@ public partial record SliceLayoutEngine : ILayoutEngine private readonly ParentArea _rootArea; private readonly ISliceLayoutPlugin _plugin; - public string Name { get; init; } = "Leader Stack"; + public string Name { get; init; } = "Slice"; public int Count => _windows.Count; From f04cc2dd93d4bdb58dd36b9684b3ebe369349c80 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 12:42:06 +1300 Subject: [PATCH 45/64] Tests for remaining functionality --- .../MoveWindowToPointTests.cs | 36 +++++++++ .../SliceLayoutEngineTests.cs | 81 ++++++++++--------- src/Whim.SliceLayout/SliceLayouts.cs | 2 +- 3 files changed, 82 insertions(+), 37 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs index 69b076922..cca32b971 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs @@ -46,6 +46,42 @@ ISliceLayoutPlugin plugin Assert.Equal(windows[expectedWindowIdx], windowStates[windowIdx].Window); } + public static IEnumerable MoveWindowToPoint_WindowDoesNotMove_Data() + { + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 4, new Point(0.7, 0.7) }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 0, new Point(0.3, 0.3) }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 1, new Point(0.3, 0.7) }; + yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 0, new Point(0, 0) }; + } + + [Theory] + [MemberAutoSubstituteData(nameof(MoveWindowToPoint_WindowDoesNotMove_Data))] + public void MoveWindowToPoint_WindowDoesNotMove( + ParentArea parentArea, + int windowIdx, + IPoint point, + IContext ctx, + ISliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, parentArea); + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + sut = sut.MoveWindowToPoint(windows[windowIdx], point); + IWindowState[] windowStates = sut.DoLayout(new Rectangle(0, 0, 100, 100), Substitute.For()) + .ToArray(); + + // Then the window should not have moved + Assert.Equal(windows[windowIdx], windowStates[windowIdx].Window); + } + public static IEnumerable MoveWindowToPoint_InvalidPoint_Data() { yield return new object[] { SampleSliceLayouts.CreateNestedLayout(), 6, 1, new Point(-0.1, 0.7) }; diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs index e922d8473..c3a2c2991 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs @@ -1,3 +1,4 @@ +using FluentAssertions; using NSubstitute; using Whim.TestUtils; using Xunit; @@ -6,16 +7,14 @@ namespace Whim.SliceLayout.Tests; public class SliceLayoutEngineTests { + private static readonly LayoutEngineIdentity identity = new(); + private static readonly IRectangle primaryMonitorBounds = new Rectangle(0, 0, 100, 100); + [Theory, AutoSubstituteData] public void Name(IContext ctx, SliceLayoutPlugin plugin) { // Given - ILayoutEngine sut = new SliceLayoutEngine( - ctx, - plugin, - new LayoutEngineIdentity(), - SampleSliceLayouts.CreateNestedLayout() - ); + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); // When string name = sut.Name; @@ -31,12 +30,7 @@ public void Name(IContext ctx, SliceLayoutPlugin plugin) public void Count(int windowCount, IContext ctx, SliceLayoutPlugin plugin) { // Given - ILayoutEngine sut = new SliceLayoutEngine( - ctx, - plugin, - new LayoutEngineIdentity(), - SampleSliceLayouts.CreateNestedLayout() - ); + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); // When foreach (IWindow window in Enumerable.Range(0, windowCount).Select(_ => Substitute.For())) @@ -55,12 +49,7 @@ public void Count(int windowCount, IContext ctx, SliceLayoutPlugin plugin) public void RemoveWindow(int addCount, int removeCount, IContext ctx, SliceLayoutPlugin plugin) { // Given - ILayoutEngine sut = new SliceLayoutEngine( - ctx, - plugin, - new LayoutEngineIdentity(), - SampleSliceLayouts.CreateNestedLayout() - ); + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); IWindow[] windows = Enumerable.Range(0, addCount).Select(_ => Substitute.For()).ToArray(); @@ -83,12 +72,7 @@ public void RemoveWindow(int addCount, int removeCount, IContext ctx, SliceLayou public void ContainsWindow_True(IContext ctx, SliceLayoutPlugin plugin, IWindow window) { // Given - ILayoutEngine sut = new SliceLayoutEngine( - ctx, - plugin, - new LayoutEngineIdentity(), - SampleSliceLayouts.CreateNestedLayout() - ); + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); sut = sut.AddWindow(window); @@ -103,12 +87,7 @@ public void ContainsWindow_True(IContext ctx, SliceLayoutPlugin plugin, IWindow public void ContainsWindow_False(IContext ctx, SliceLayoutPlugin plugin, IWindow window) { // Given - ILayoutEngine sut = new SliceLayoutEngine( - ctx, - plugin, - new LayoutEngineIdentity(), - SampleSliceLayouts.CreateNestedLayout() - ); + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); // When bool contains = sut.ContainsWindow(window); @@ -121,12 +100,7 @@ public void ContainsWindow_False(IContext ctx, SliceLayoutPlugin plugin, IWindow public void GetFirstWindow(IContext ctx, SliceLayoutPlugin plugin) { // Given - ILayoutEngine sut = new SliceLayoutEngine( - ctx, - plugin, - new LayoutEngineIdentity(), - SampleSliceLayouts.CreateNestedLayout() - ); + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); @@ -141,4 +115,39 @@ public void GetFirstWindow(IContext ctx, SliceLayoutPlugin plugin) // Then Assert.Equal(windows[0], firstWindow); } + + [Theory, AutoSubstituteData] + public void GetFirstWindow_Empty(IContext ctx, SliceLayoutPlugin plugin) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + + // When + IWindow? firstWindow = sut.GetFirstWindow(); + + // Then + Assert.Null(firstWindow); + } + + [Theory, AutoSubstituteData] + public void MoveWindowEdgesInDirection(IContext ctx, SliceLayoutPlugin plugin) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + // When + IWindowState[] beforeStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); + sut = sut.MoveWindowEdgesInDirection(Direction.Right, new Point(0.5, 0.5), windows[0]); + IWindowState[] afterStates = sut.DoLayout(primaryMonitorBounds, Substitute.For()).ToArray(); + + // Then + beforeStates.Should().BeEquivalentTo(afterStates); + } } diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index a95e778d7..c61dee3c2 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -87,7 +87,7 @@ internal static ParentArea CreateMultiColumnArea(uint[] capacities) if (createdOverflow) { // TODO: Handle this - throw new ArgumentException("Cannot have multiple base areas"); + throw new ArgumentException("Cannot have multiple overflow areas"); } areas[idx] = (weight, new OverflowArea()); From 8cb9979cc0c43c5c3659fed010d0d27204408e18 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 13:04:49 +1300 Subject: [PATCH 46/64] Switch from `uint` to `int` --- src/Whim.SliceLayout/Area.cs | 10 +++++----- src/Whim.SliceLayout/AreaHelpers.cs | 10 +++++----- src/Whim.SliceLayout/SliceLayoutEngine.cs | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index ea3f8815d..9e58e6b96 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -48,7 +48,7 @@ internal ParentArea(bool isRow, ImmutableList weights, ImmutableList /// 0-indexed order of this area in the layout engine. /// - public uint Order { get; } + public int Order { get; } /// /// Maximum number of children this area can have. This must be a non-negative integer. /// - public uint MaxChildren { get; } + public int MaxChildren { get; } public SliceArea(uint order = 0, uint maxChildren = 1, bool isRow = false) { - Order = order; - MaxChildren = maxChildren; + Order = (int)order; + MaxChildren = (int)maxChildren; IsRow = isRow; } } diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index 5920bfcc7..39f307dbe 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -81,7 +81,7 @@ public static (ParentArea RootArea, BaseSliceArea[] OrderedSliceAreas) SetStartI (List sliceAreas, OverflowArea? overflowArea) = GetAreasSorted(rootArea); // Set the start indexes. - uint currIdx = 0; + int currIdx = 0; foreach (SliceArea sliceArea in sliceAreas) { sliceArea.StartIndex = currIdx; @@ -274,12 +274,12 @@ private static void DoSliceLayout(this BaseSliceArea area, IRectangle recta int deltaX = 0; int deltaY = 0; - int sliceItemsCount = (int)(items.Length - area.StartIndex); + int sliceItemsCount = items.Length - area.StartIndex; if (area is SliceArea sliceArea) { - sliceItemsCount = (int)Math.Min(sliceArea.MaxChildren, sliceItemsCount); + sliceItemsCount = Math.Min(sliceArea.MaxChildren, sliceItemsCount); } - int maxIdx = (int)(area.StartIndex + sliceItemsCount); + int maxIdx = area.StartIndex + sliceItemsCount; if (area.IsRow) { @@ -292,7 +292,7 @@ private static void DoSliceLayout(this BaseSliceArea area, IRectangle recta height = deltaY; } - for (int windowCurrIdx = (int)area.StartIndex; windowCurrIdx < maxIdx; windowCurrIdx++) + for (int windowCurrIdx = area.StartIndex; windowCurrIdx < maxIdx; windowCurrIdx++) { items[windowCurrIdx] = new SliceRectangleItem(windowCurrIdx, new Rectangle(x, y, width, height)); diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 2f4e8bc51..98abebeef 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -188,7 +188,7 @@ private ILayoutEngine PromoteWindowInStack(IWindow window) } SliceArea targetArea = (SliceArea)_windowAreas[areaIndex - 1]; - int targetIndex = (int)(targetArea.StartIndex + targetArea.MaxChildren - 1); + int targetIndex = targetArea.StartIndex + targetArea.MaxChildren - 1; return MoveWindowToIndex(windowIndex, targetIndex); } @@ -213,7 +213,7 @@ private ILayoutEngine DemoteWindowInStack(IWindow window) } BaseSliceArea targetArea = _windowAreas[areaIndex + 1]; - int targetIndex = (int)targetArea.StartIndex; + int targetIndex = targetArea.StartIndex; return MoveWindowToIndex(windowIndex, targetIndex); } @@ -225,7 +225,7 @@ private int GetAreaStackForWindowIndex(int windowIndex) IArea area = _windowAreas[idx]; if (area is SliceArea sliceArea) { - int areaEndIndex = (int)(sliceArea.StartIndex + sliceArea.MaxChildren); + int areaEndIndex = sliceArea.StartIndex + sliceArea.MaxChildren; if (windowIndex >= sliceArea.StartIndex && windowIndex < areaEndIndex) { return idx; From be4f6d63d3fd63318179da4fbacf5fbbd95b6118 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 17:16:12 +1300 Subject: [PATCH 47/64] Semi-tested promote/demote focus --- .../SliceLayoutCommandsTests.cs | 32 ++++++- .../PerformCustomActionTests.cs | 58 +++++++++++-- .../SliceLayoutPluginTests.cs | 4 +- src/Whim.SliceLayout/ISliceLayoutPlugin.cs | 32 ++++++- src/Whim.SliceLayout/SliceLayoutCommands.cs | 14 ++- src/Whim.SliceLayout/SliceLayoutEngine.cs | 87 +++---------------- .../SliceLayoutEngineHelpers.cs | 82 +++++++++++++++++ src/Whim.SliceLayout/SliceLayoutPlugin.cs | 54 ++++++++++-- 8 files changed, 263 insertions(+), 100 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutCommandsTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutCommandsTests.cs index cca08556f..1a1dc5760 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutCommandsTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutCommandsTests.cs @@ -49,7 +49,7 @@ ISliceLayoutPlugin plugin public void PromoteWindowInStack(ISliceLayoutPlugin plugin) { // Given - ICommand command = CreateSut(plugin, "whim.slicelayout.stack.promote"); + ICommand command = CreateSut(plugin, "whim.slicelayout.window.promote"); // When command.TryExecute(); @@ -63,7 +63,7 @@ public void PromoteWindowInStack(ISliceLayoutPlugin plugin) public void DemoteWindowInStack(ISliceLayoutPlugin plugin) { // Given - ICommand command = CreateSut(plugin, "whim.slicelayout.stack.demote"); + ICommand command = CreateSut(plugin, "whim.slicelayout.window.demote"); // When command.TryExecute(); @@ -71,4 +71,32 @@ public void DemoteWindowInStack(ISliceLayoutPlugin plugin) // Then plugin.Received(1).DemoteWindowInStack(); } + + [Theory] + [AutoSubstituteData] + public void PromoteFocusInStack(ISliceLayoutPlugin plugin) + { + // Given + ICommand command = CreateSut(plugin, "whim.slicelayout.focus.promote"); + + // When + command.TryExecute(); + + // Then + plugin.Received(1).PromoteFocusInStack(); + } + + [Theory] + [AutoSubstituteData] + public void DemoteFocusInStack(ISliceLayoutPlugin plugin) + { + // Given + ICommand command = CreateSut(plugin, "whim.slicelayout.focus.demote"); + + // When + command.TryExecute(); + + // Then + plugin.Received(1).DemoteFocusInStack(); + } } diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs index 1b15ffa8e..199410cfc 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs @@ -29,7 +29,7 @@ public void PromoteWindowInStack_CannotFindWindow(IContext ctx, SliceLayoutPlugi sut = sut.PerformCustomAction( new LayoutEngineCustomAction() { - Name = "whim.slice_layout.stack.promote", + Name = "whim.slice_layout.window.promote", Window = untrackedWindow, Payload = untrackedWindow } @@ -78,7 +78,7 @@ SliceLayoutPlugin plugin sut = sut.PerformCustomAction( new LayoutEngineCustomAction() { - Name = "whim.slice_layout.stack.promote", + Name = "whim.slice_layout.window.promote", Window = windows[focusedWindowIdx], Payload = windows[focusedWindowIdx] } @@ -103,7 +103,7 @@ public void PromoteWindowInStack_EmptyLayoutEngine(IContext ctx, SliceLayoutPlug sut = sut.PerformCustomAction( new LayoutEngineCustomAction() { - Name = "whim.slice_layout.stack.promote", + Name = "whim.slice_layout.window.promote", Window = window, Payload = window } @@ -135,7 +135,7 @@ public void DemoteWindowInStack_CannotFindWindow(IContext ctx, SliceLayoutPlugin sut = sut.PerformCustomAction( new LayoutEngineCustomAction() { - Name = "whim.slice_layout.stack.demote", + Name = "whim.slice_layout.window.demote", Window = untrackedWindow, Payload = untrackedWindow } @@ -179,7 +179,7 @@ public void DemoteWindowInStack(int focusedWindowIdx, int expectedWindowIdx, ICo sut = sut.PerformCustomAction( new LayoutEngineCustomAction() { - Name = "whim.slice_layout.stack.demote", + Name = "whim.slice_layout.window.demote", Window = windows[focusedWindowIdx], Payload = windows[focusedWindowIdx] } @@ -204,7 +204,7 @@ public void DemoteWindowInStack_EmptyLayoutEngine(IContext ctx, SliceLayoutPlugi sut = sut.PerformCustomAction( new LayoutEngineCustomAction() { - Name = "whim.slice_layout.stack.demote", + Name = "whim.slice_layout.window.demote", Window = window, Payload = window } @@ -217,6 +217,50 @@ public void DemoteWindowInStack_EmptyLayoutEngine(IContext ctx, SliceLayoutPlugi } #endregion + #region PromoteFocusInStack + [Theory] + [InlineAutoSubstituteData(0, 0, true)] + [InlineAutoSubstituteData(1, 0, true)] + [InlineAutoSubstituteData(2, 1, true)] + [InlineAutoSubstituteData(4, 3, true)] + [InlineAutoSubstituteData(0, 0, false)] + [InlineAutoSubstituteData(1, 2, false)] + [InlineAutoSubstituteData(2, 3, false)] + [InlineAutoSubstituteData(4, 5, false)] + public void PerformCustomAction_PromoteFocus( + int focusedWindowIdx, + int expectedWindowIdx, + bool promote, + IContext ctx, + SliceLayoutPlugin plugin + ) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + IWindow[] windows = Enumerable.Range(0, 6).Select(_ => Substitute.For()).ToArray(); + + // When + foreach (IWindow window in windows) + { + sut = sut.AddWindow(window); + } + + ILayoutEngine resultSut= sut.PerformCustomAction( + new LayoutEngineCustomAction() + { + Name = promote ? plugin.PromoteFocusActionName : plugin.DemoteFocusActionName, + Window = windows[focusedWindowIdx], + Payload = windows[focusedWindowIdx] + } + ); + + // Then + Assert.Same(sut, resultSut); + windows[expectedWindowIdx].Received(1).Focus(); + } + + #endregion + [Theory, AutoSubstituteData] public void PerformCustomAction_UnknownAction(IContext ctx, SliceLayoutPlugin plugin) { @@ -235,7 +279,7 @@ public void PerformCustomAction_UnknownAction(IContext ctx, SliceLayoutPlugin pl sut = sut.PerformCustomAction( new LayoutEngineCustomAction() { - Name = "whim.slice_layout.stack.unknown", + Name = "whim.slice_layout.window.unknown", Window = windows[0], Payload = windows[0] } diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs index 0526ff053..9012745c0 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs @@ -133,7 +133,7 @@ public void PromoteWindowInStack(IContext ctx, IWindow window, IWorkspace worksp .Received(1) .PerformCustomLayoutEngineAction( Arg.Is( - action => action.Name == plugin.PromoteActionName && action.Window == window + action => action.Name == plugin.PromoteWindowActionName && action.Window == window ) ); } @@ -186,7 +186,7 @@ public void DemoteWindowInStack(IContext ctx, IWindow window, IWorkspace workspa .Received(1) .PerformCustomLayoutEngineAction( Arg.Is( - action => action.Name == plugin.DemoteActionName && action.Window == window + action => action.Name == plugin.DemoteWindowActionName && action.Window == window ) ); } diff --git a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs index 7f49f71c8..2f02b5479 100644 --- a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs @@ -21,12 +21,22 @@ public interface ISliceLayoutPlugin : IPlugin /// /// The name of the action that promotes a window in the stack to the next-higher slice. /// - string PromoteActionName { get; } + string PromoteWindowActionName { get; } /// /// The name of the action that demotes a window in the stack to the next-lower slice. /// - string DemoteActionName { get; } + string DemoteWindowActionName { get; } + + /// + /// The name of the action that promotes the focus in the stack to the next-higher slice. + /// + string PromoteFocusActionName { get; } + + /// + /// The name of the action that demotes the focus in the stack to the next-lower slice. + /// + string DemoteFocusActionName { get; } /// /// The type of insertion to use when adding a window to a slice. @@ -50,4 +60,22 @@ public interface ISliceLayoutPlugin : IPlugin /// is used. /// void DemoteWindowInStack(IWindow? window = null); + + /// + /// Promotes the focus to the next slice with a lower order - see . + /// + /// + /// The current window. If , then + /// is used. + /// + void PromoteFocusInStack(IWindow? window = null); + + /// + /// Demotes the focus to the next slice with a higher order - see . + /// + /// + /// The current window. If , then + /// is used. + /// + void DemoteFocusInStack(IWindow? window = null); } diff --git a/src/Whim.SliceLayout/SliceLayoutCommands.cs b/src/Whim.SliceLayout/SliceLayoutCommands.cs index 9c635b363..1c1b3e678 100644 --- a/src/Whim.SliceLayout/SliceLayoutCommands.cs +++ b/src/Whim.SliceLayout/SliceLayoutCommands.cs @@ -28,14 +28,24 @@ public SliceLayoutCommands(ISliceLayoutPlugin sliceLayoutPlugin) callback: () => _sliceLayoutPlugin.WindowInsertionType = WindowInsertionType.Rotate ) .Add( - identifier: "stack.promote", + identifier: "window.promote", title: "Promote window in stack", callback: () => _sliceLayoutPlugin.PromoteWindowInStack() ) .Add( - identifier: "stack.demote", + identifier: "window.demote", title: "Demote window in stack", callback: () => _sliceLayoutPlugin.DemoteWindowInStack() + ) + .Add( + identifier: "focus.promote", + title: "Promote focus in stack", + callback: () => _sliceLayoutPlugin.PromoteFocusInStack() + ) + .Add( + identifier: "focus.demote", + title: "Demote focus in stack", + callback: () => _sliceLayoutPlugin.DemoteFocusInStack() ); } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 98abebeef..050e72107 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -168,86 +168,19 @@ public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) return MoveWindowToIndex(_windows.IndexOf(window), _windows.IndexOf(windowInDirection)); } - private ILayoutEngine PromoteWindowInStack(IWindow window) - { - Logger.Debug($"Promoting {window} in stack"); - - int windowIndex = _windows.IndexOf(window); - if (windowIndex == -1) - { - Logger.Error($"Could not find {window} in stack"); - return this; - } - - // Find the area which contains the window. - int areaIndex = GetAreaStackForWindowIndex(windowIndex); - if (areaIndex <= 0) - { - Logger.Error($"Could not promote {window} to higher area"); - return this; - } - - SliceArea targetArea = (SliceArea)_windowAreas[areaIndex - 1]; - int targetIndex = targetArea.StartIndex + targetArea.MaxChildren - 1; - - return MoveWindowToIndex(windowIndex, targetIndex); - } - - private ILayoutEngine DemoteWindowInStack(IWindow window) - { - Logger.Debug($"Demoting {window} in stack"); - - int windowIndex = _windows.IndexOf(window); - if (windowIndex == _windows.Count - 1) - { - Logger.Error($"Could not find {window} in stack"); - return this; - } - - // Find the area which contains the window. - int areaIndex = GetAreaStackForWindowIndex(windowIndex); - if (areaIndex > _windowAreas.Length - 2 || areaIndex == -1) - { - Logger.Error($"Could not demote {window} to lower area"); - return this; - } - - BaseSliceArea targetArea = _windowAreas[areaIndex + 1]; - int targetIndex = targetArea.StartIndex; - - return MoveWindowToIndex(windowIndex, targetIndex); - } - - private int GetAreaStackForWindowIndex(int windowIndex) - { - for (int idx = 0; idx < _windowAreas.Length; idx++) - { - IArea area = _windowAreas[idx]; - if (area is SliceArea sliceArea) - { - int areaEndIndex = sliceArea.StartIndex + sliceArea.MaxChildren; - if (windowIndex >= sliceArea.StartIndex && windowIndex < areaEndIndex) - { - return idx; - } - } - - if (area is OverflowArea) - { - return idx; - } - } - - return -1; - } - public ILayoutEngine PerformCustomAction(LayoutEngineCustomAction action) => action switch { - LayoutEngineCustomAction promoteAction when promoteAction.Name == _plugin.PromoteActionName - => PromoteWindowInStack(promoteAction.Payload), - LayoutEngineCustomAction demoteAction when demoteAction.Name == _plugin.DemoteActionName - => DemoteWindowInStack(demoteAction.Payload), + LayoutEngineCustomAction promoteAction when promoteAction.Name == _plugin.PromoteWindowActionName + => PromoteWindowInStack(promoteAction.Payload, promote: true), + LayoutEngineCustomAction demoteAction when demoteAction.Name == _plugin.DemoteWindowActionName + => PromoteWindowInStack(demoteAction.Payload, promote: false), + LayoutEngineCustomAction promoteFocusAction + when promoteFocusAction.Name == _plugin.PromoteFocusActionName + => PromoteFocusInStack(promoteFocusAction.Payload, promote: true), + LayoutEngineCustomAction demoteFocusAction + when demoteFocusAction.Name == _plugin.DemoteFocusActionName + => PromoteFocusInStack(demoteFocusAction.Payload, promote: false), _ => this }; } diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs index b53a27047..9c0b6655f 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -142,4 +142,86 @@ private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) return null; } + + private ILayoutEngine PromoteWindowInStack(IWindow window, bool promote) + { + Logger.Debug($"Promoting {window} in stack"); + if (GetPromoteTargetIndex(window, promote) is not (int windowIndex, int targetIndex)) + { + return this; + } + + return MoveWindowToIndex(windowIndex, targetIndex); + } + + private ILayoutEngine PromoteFocusInStack(IWindow window, bool promote) + { + Logger.Debug($"Promoting focus in stack"); + if (GetPromoteTargetIndex(window, promote) is not (int, int targetIndex)) + { + return this; + } + + _windows[targetIndex].Focus(); + return this; + } + + private (int WindowIndex, int TargetIndex)? GetPromoteTargetIndex(IWindow window, bool promote) + { + int windowIndex = _windows.IndexOf(window); + if (windowIndex == -1) + { + Logger.Error($"Could not find {window} in stack"); + return null; + } + + // Find the area which contains the window. + int areaIndex = GetAreaStackForWindowIndex(windowIndex); + + if (promote) + { + if (areaIndex <= 0) + { + Logger.Error($"Could not promote {window} to an area with lower order"); + return null; + } + + SliceArea targetArea = (SliceArea)_windowAreas[areaIndex - 1]; + return (windowIndex, targetArea.StartIndex + targetArea.MaxChildren - 1); + } + else + { + if (areaIndex >= _windowAreas.Length - 1) + { + Logger.Error($"Could not promote {window} to an area with higher order"); + return null; + } + + BaseSliceArea targetArea = _windowAreas[areaIndex + 1]; + return (windowIndex, targetArea.StartIndex); + } + } + + private int GetAreaStackForWindowIndex(int windowIndex) + { + for (int idx = 0; idx < _windowAreas.Length; idx++) + { + IArea area = _windowAreas[idx]; + if (area is SliceArea sliceArea) + { + int areaEndIndex = sliceArea.StartIndex + sliceArea.MaxChildren; + if (windowIndex >= sliceArea.StartIndex && windowIndex < areaEndIndex) + { + return idx; + } + } + + if (area is OverflowArea) + { + return idx; + } + } + + return -1; + } } diff --git a/src/Whim.SliceLayout/SliceLayoutPlugin.cs b/src/Whim.SliceLayout/SliceLayoutPlugin.cs index c04c237ba..b7ce0fa79 100644 --- a/src/Whim.SliceLayout/SliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/SliceLayoutPlugin.cs @@ -13,9 +13,13 @@ public SliceLayoutPlugin(IContext context) public string Name => "whim.slice_layout"; - public string PromoteActionName => $"{Name}.stack.promote"; + public string PromoteWindowActionName => $"{Name}.window.promote"; - public string DemoteActionName => $"{Name}.stack.demote"; + public string DemoteWindowActionName => $"{Name}.window.demote"; + + public string PromoteFocusActionName => $"{Name}.focus.promote"; + + public string DemoteFocusActionName => $"{Name}.focus.demote"; public IPluginCommands PluginCommands => new SliceLayoutCommands(this); @@ -29,29 +33,63 @@ public void LoadState(JsonElement state) { } public JsonElement? SaveState() => null; + public void PromoteWindowInStack(IWindow? window = null) => ChangeWindowRank(window, promote: true); + + public void DemoteWindowInStack(IWindow? window = null) => ChangeWindowRank(window, promote: false); + private void ChangeWindowRank(IWindow? window, bool promote) + { + if (GetWindowWithRankDelta(window, promote) is not (IWindow definedWindow, IWorkspace workspace)) + { + return; + } + + workspace.PerformCustomLayoutEngineAction( + new LayoutEngineCustomAction() + { + Name = promote ? PromoteWindowActionName : DemoteWindowActionName, + Window = definedWindow + } + ); + } + + private (IWindow, IWorkspace)? GetWindowWithRankDelta(IWindow? window, bool promote) { window ??= _context.WorkspaceManager.ActiveWorkspace.LastFocusedWindow; if (window is null) { Logger.Debug("No window to change rank for"); - return; + return null; } IWorkspace? workspace = _context.WorkspaceManager.GetWorkspaceForWindow(window); if (workspace is null) { Logger.Debug("Window is not in a workspace"); + return null; + } + + return (window, workspace); + } + + public void PromoteFocusInStack(IWindow? window = null) => FocusWindowRank(window, promote: true); + + public void DemoteFocusInStack(IWindow? window = null) => FocusWindowRank(window, promote: false); + + private void FocusWindowRank(IWindow? window, bool promote) + { + if (GetWindowWithRankDelta(window, promote) is not (IWindow definedWindow, IWorkspace workspace)) + { return; } workspace.PerformCustomLayoutEngineAction( - new LayoutEngineCustomAction() { Name = promote ? PromoteActionName : DemoteActionName, Window = window, } + new LayoutEngineCustomAction() + { + Name = promote ? PromoteFocusActionName : DemoteFocusActionName, + Window = definedWindow + } ); } - - public void PromoteWindowInStack(IWindow? window = null) => ChangeWindowRank(window, promote: true); - - public void DemoteWindowInStack(IWindow? window = null) => ChangeWindowRank(window, promote: false); } From a229463acb9c8884a98cd9fc5d6282f7747bdb93 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 17:58:53 +1300 Subject: [PATCH 48/64] Fix promote/demote tests --- .../PerformCustomActionTests.cs | 16 ++++++++-------- src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs | 6 ++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs index 199410cfc..d23b1b15c 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs @@ -219,13 +219,13 @@ public void DemoteWindowInStack_EmptyLayoutEngine(IContext ctx, SliceLayoutPlugi #region PromoteFocusInStack [Theory] - [InlineAutoSubstituteData(0, 0, true)] - [InlineAutoSubstituteData(1, 0, true)] - [InlineAutoSubstituteData(2, 1, true)] - [InlineAutoSubstituteData(4, 3, true)] - [InlineAutoSubstituteData(0, 0, false)] - [InlineAutoSubstituteData(1, 2, false)] - [InlineAutoSubstituteData(2, 3, false)] + //[InlineAutoSubstituteData(0, 0, true)] + //[InlineAutoSubstituteData(1, 0, true)] + //[InlineAutoSubstituteData(2, 1, true)] + //[InlineAutoSubstituteData(4, 3, true)] + //[InlineAutoSubstituteData(0, 2, false)] + //[InlineAutoSubstituteData(1, 2, false)] + //[InlineAutoSubstituteData(2, 4, false)] [InlineAutoSubstituteData(4, 5, false)] public void PerformCustomAction_PromoteFocus( int focusedWindowIdx, @@ -245,7 +245,7 @@ SliceLayoutPlugin plugin sut = sut.AddWindow(window); } - ILayoutEngine resultSut= sut.PerformCustomAction( + ILayoutEngine resultSut = sut.PerformCustomAction( new LayoutEngineCustomAction() { Name = promote ? plugin.PromoteFocusActionName : plugin.DemoteFocusActionName, diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs index 9c0b6655f..4f3676a96 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -182,8 +182,7 @@ private ILayoutEngine PromoteFocusInStack(IWindow window, bool promote) { if (areaIndex <= 0) { - Logger.Error($"Could not promote {window} to an area with lower order"); - return null; + return (windowIndex, 0); } SliceArea targetArea = (SliceArea)_windowAreas[areaIndex - 1]; @@ -193,8 +192,7 @@ private ILayoutEngine PromoteFocusInStack(IWindow window, bool promote) { if (areaIndex >= _windowAreas.Length - 1) { - Logger.Error($"Could not promote {window} to an area with higher order"); - return null; + return (windowIndex, _windows.Count - 1); } BaseSliceArea targetArea = _windowAreas[areaIndex + 1]; From 9f40ca9f8ad81a80be20e8e6755af97184b84aca Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 18:01:50 +1300 Subject: [PATCH 49/64] Found some commented tests --- .../SliceLayoutEngine/PerformCustomActionTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs index d23b1b15c..77e5327cb 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/PerformCustomActionTests.cs @@ -219,13 +219,13 @@ public void DemoteWindowInStack_EmptyLayoutEngine(IContext ctx, SliceLayoutPlugi #region PromoteFocusInStack [Theory] - //[InlineAutoSubstituteData(0, 0, true)] - //[InlineAutoSubstituteData(1, 0, true)] - //[InlineAutoSubstituteData(2, 1, true)] - //[InlineAutoSubstituteData(4, 3, true)] - //[InlineAutoSubstituteData(0, 2, false)] - //[InlineAutoSubstituteData(1, 2, false)] - //[InlineAutoSubstituteData(2, 4, false)] + [InlineAutoSubstituteData(0, 0, true)] + [InlineAutoSubstituteData(1, 0, true)] + [InlineAutoSubstituteData(2, 1, true)] + [InlineAutoSubstituteData(4, 3, true)] + [InlineAutoSubstituteData(0, 2, false)] + [InlineAutoSubstituteData(1, 2, false)] + [InlineAutoSubstituteData(2, 4, false)] [InlineAutoSubstituteData(4, 5, false)] public void PerformCustomAction_PromoteFocus( int focusedWindowIdx, From d91e47be66b23dc38dc03fb7323ec1945164f32f Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 18:07:31 +1300 Subject: [PATCH 50/64] TODOs --- README.md | 13 +++++++++++++ src/Whim.SliceLayout/SliceLayoutCommands.cs | 1 - src/Whim.SliceLayout/SliceLayouts.cs | 11 +++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2a49418dc..86c872b58 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,19 @@ See [`GapsCommands.cs`](src/Whim.Gaps/GapsCommands.cs). | `whim.gaps.inner.increase` | Increase inner gap | Win + Ctrl + Shift + K | | `whim.gaps.inner.decrease` | Decrease inner gap | Win + Ctrl + Shift + J | +##### Slice Layout Plugin Commands + +See [`SliceLayoutCommands.cs`](src/Whim.SliceLayout/SliceLayoutCommands.cs). + +| Identifier | Title | Keybind | +| --------------------------- | ---------------------------------- | ------- | ------------------ | +| `set_insertion_type.swap` | Set slice insertion type to swap | Keybind | No default keybind | +| `set_insertion_type.rotate` | Set slice insertion type to rotate | Keybind | No default keybind | +| `window.promote` | Promote window in stack | Keybind | No default keybind | +| `window.demote` | Demote window in stack | Keybind | No default keybind | +| `focus.promote` | Promote focus in stack | Keybind | No default keybind | +| `focus.demote` | Demote focus in stack | Keybind | No default keybind | + ##### Tree Layout Plugin Commands See [`TreeLayoutCommands.cs`](src/Whim.TreeLayout/TreeLayoutCommands.cs). diff --git a/src/Whim.SliceLayout/SliceLayoutCommands.cs b/src/Whim.SliceLayout/SliceLayoutCommands.cs index 1c1b3e678..9bc1c92d1 100644 --- a/src/Whim.SliceLayout/SliceLayoutCommands.cs +++ b/src/Whim.SliceLayout/SliceLayoutCommands.cs @@ -7,7 +7,6 @@ public class SliceLayoutCommands : PluginCommands { private readonly ISliceLayoutPlugin _sliceLayoutPlugin; - // TODO: Document in README /// /// Creates a new instance of the slice layout commands. /// diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index c61dee3c2..635f40010 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -2,7 +2,6 @@ namespace Whim.SliceLayout; -// TODO: test custom layouts public static class SliceLayouts { /// @@ -78,20 +77,20 @@ internal static ParentArea CreateMultiColumnArea(uint[] capacities) double weight = 1.0 / capacities.Length; (double, IArea)[] areas = new (double, IArea)[capacities.Length]; - bool createdOverflow = false; + int overflowIdx = -1; for (int idx = 0; idx < capacities.Length; idx++) { uint capacity = capacities[idx]; if (capacity == 0) { - if (createdOverflow) + if (overflowIdx != -1) { - // TODO: Handle this - throw new ArgumentException("Cannot have multiple overflow areas"); + // Replace the last overflow area with a slice area + areas[overflowIdx] = (weight, new SliceArea(order: (uint)overflowIdx, maxChildren: 1)); } areas[idx] = (weight, new OverflowArea()); - createdOverflow = true; + overflowIdx = idx; } else { From 18a83327530a3db102725aba350fa4904b9d351a Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 18:08:15 +1300 Subject: [PATCH 51/64] Add to release.yml --- .github/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/release.yml b/.github/release.yml index 777018a17..9756c8662 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -41,6 +41,10 @@ changelog: labels: - "layout preview" + - title: Slice Layout 🍰 + labels: + - "slice layout" + - title: Tree Layout 🌳 labels: - "tree layout" From 071dd502d09151da41d0af2d785534a7f26411fb Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 18:16:02 +1300 Subject: [PATCH 52/64] Overflow replacement in CreateMultiColumnLayout --- .../SliceLayoutsTests.cs | 25 +++++++++++++++++++ src/Whim.SliceLayout/SliceLayouts.cs | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/Whim.SliceLayout.Tests/SliceLayoutsTests.cs diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutsTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutsTests.cs new file mode 100644 index 000000000..e08f07099 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/SliceLayoutsTests.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Xunit; + +namespace Whim.SliceLayout.Tests; + +public class SliceLayoutsTests +{ + [Fact] + public void CreateMultiColumnArea_MultipleOverflows() + { + // Given + ParentArea sut = SliceLayouts.CreateMultiColumnArea(new uint[] { 2, 1, 0, 0 }); + + // Then + new ParentArea( + isRow: true, + (0.25, new SliceArea(order: 0, maxChildren: 2)), + (0.25, new SliceArea(order: 1, maxChildren: 1)), + (0.25, new SliceArea(order: 2, maxChildren: 0)), + (0.25, new OverflowArea()) + ) + .Should() + .BeEquivalentTo(sut); + } +} diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index 635f40010..d7aa92a49 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -86,7 +86,7 @@ internal static ParentArea CreateMultiColumnArea(uint[] capacities) if (overflowIdx != -1) { // Replace the last overflow area with a slice area - areas[overflowIdx] = (weight, new SliceArea(order: (uint)overflowIdx, maxChildren: 1)); + areas[overflowIdx] = (weight, new SliceArea(order: (uint)overflowIdx, maxChildren: 0)); } areas[idx] = (weight, new OverflowArea()); From aed3b22177f046f1b99e9cf965fbfd4578ceac99 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 19:19:36 +1300 Subject: [PATCH 53/64] Expose OverflowArea --- src/Whim.SliceLayout/Area.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index 9e58e6b96..cdcabc382 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -71,7 +71,7 @@ public SliceArea(uint order = 0, uint maxChildren = 1, bool isRow = false) } } -internal record OverflowArea : BaseSliceArea +public record OverflowArea : BaseSliceArea { public OverflowArea(bool isRow = false) { From 50fab82f736e96932550ecff84b361b6f675c976 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 19:22:29 +1300 Subject: [PATCH 54/64] Name layouts --- .../AreaHelpers/DoLayout.cs | 8 ++++---- .../AreaHelpers/PruneTests.cs | 13 ++++--------- .../AreaHelpers/SetStartIndexesTests.cs | 2 +- src/Whim.SliceLayout/SliceLayouts.cs | 15 +++++++++------ 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs index 8d69cd431..906e47470 100644 --- a/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/DoLayout.cs @@ -104,7 +104,7 @@ public static IEnumerable DoLayout_SecondaryPrimary() yield return new object[] { new Rectangle(0, 0, 100, 100), - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + SliceLayouts.CreateSecondaryPrimaryArea(1, 2), Array.Empty(), }; @@ -112,7 +112,7 @@ public static IEnumerable DoLayout_SecondaryPrimary() yield return new object[] { new Rectangle(0, 0, 100, 100), - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + SliceLayouts.CreateSecondaryPrimaryArea(1, 2), new[] { new Rectangle(0, 0, 100, 100), }, }; @@ -120,7 +120,7 @@ public static IEnumerable DoLayout_SecondaryPrimary() yield return new object[] { new Rectangle(0, 0, 100, 100), - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + SliceLayouts.CreateSecondaryPrimaryArea(1, 2), new[] { new Rectangle(38, 0, 62, 100), @@ -133,7 +133,7 @@ public static IEnumerable DoLayout_SecondaryPrimary() yield return new object[] { new Rectangle(0, 0, 100, 100), - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + SliceLayouts.CreateSecondaryPrimaryArea(1, 2), new[] { new Rectangle(25, 0, 50, 100), diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs index 46d13adc5..348f37e1f 100644 --- a/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs @@ -82,17 +82,12 @@ public static IEnumerable Prune_MultiColumn() public static IEnumerable Prune_SecondaryPrimary() { // Empty - yield return new object[] - { - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), - 0, - new ParentArea(isRow: true), - }; + yield return new object[] { SliceLayouts.CreateSecondaryPrimaryArea(1, 2), 0, new ParentArea(isRow: true), }; // Fill primary yield return new object[] { - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + SliceLayouts.CreateSecondaryPrimaryArea(1, 2), 1, new ParentArea(isRow: true, (1.0, new SliceArea(order: 0, maxChildren: 1))), }; @@ -100,7 +95,7 @@ public static IEnumerable Prune_SecondaryPrimary() // Fill secondary yield return new object[] { - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + SliceLayouts.CreateSecondaryPrimaryArea(1, 2), 3, new ParentArea( isRow: true, @@ -112,7 +107,7 @@ public static IEnumerable Prune_SecondaryPrimary() // Fill overflow yield return new object[] { - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + SliceLayouts.CreateSecondaryPrimaryArea(1, 2), 5, new ParentArea( isRow: true, diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs index c3050034b..79623aa36 100644 --- a/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/SetStartIndexesTests.cs @@ -43,7 +43,7 @@ public static IEnumerable SetStartIndexes_Data() // Secondary primary yield return new object[] { - SliceLayouts.CreateSecondaryPrimaryStackArea(1, 2), + SliceLayouts.CreateSecondaryPrimaryArea(1, 2), new ParentArea( isRow: true, (0.25, new SliceArea(order: 1, maxChildren: 2) { StartIndex = 1 }), diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index d7aa92a49..fcd018292 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -16,7 +16,7 @@ public static ILayoutEngine CreatePrimaryStackLayout( IContext context, ISliceLayoutPlugin plugin, LayoutEngineIdentity identity - ) => new SliceLayoutEngine(context, plugin, identity, CreatePrimaryStackArea()); + ) => new SliceLayoutEngine(context, plugin, identity, CreatePrimaryStackArea()) { Name = "Primary stack" }; internal static ParentArea CreatePrimaryStackArea() => new(isRow: true, (0.5, new SliceArea(order: 0, maxChildren: 1)), (0.5, new OverflowArea())); @@ -70,7 +70,7 @@ public static ILayoutEngine CreateMultiColumnLayout( ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, params uint[] capacities - ) => new SliceLayoutEngine(context, plugin, identity, CreateMultiColumnArea(capacities)); + ) => new SliceLayoutEngine(context, plugin, identity, CreateMultiColumnArea(capacities)) { Name = "Multi-column " }; internal static ParentArea CreateMultiColumnArea(uint[] capacities) { @@ -144,7 +144,7 @@ internal static ParentArea CreateMultiColumnArea(uint[] capacities) /// /// /// - public static ILayoutEngine CreateSecondaryPrimaryStackLayout( + public static ILayoutEngine CreateSecondaryPrimaryLayout( IContext context, ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, @@ -155,10 +155,13 @@ public static ILayoutEngine CreateSecondaryPrimaryStackLayout( context, plugin, identity, - CreateSecondaryPrimaryStackArea(primaryColumnCapacity, secondaryColumnCapacity) - ); + CreateSecondaryPrimaryArea(primaryColumnCapacity, secondaryColumnCapacity) + ) + { + Name = "Secondary primary" + }; - internal static ParentArea CreateSecondaryPrimaryStackArea(uint primaryColumnCapacity, uint secondaryColumnCapacity) + internal static ParentArea CreateSecondaryPrimaryArea(uint primaryColumnCapacity, uint secondaryColumnCapacity) { return new ParentArea( isRow: true, From e4ae1f1ce0832b7a3cce63757a6cebe5a3aac929 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 21:23:35 +1300 Subject: [PATCH 55/64] Respect the names --- .../SliceLayoutEngineTests.cs | 16 ++++++++++ src/Whim.SliceLayout/SliceLayoutEngine.cs | 30 +++++++++++-------- .../SliceLayoutEngineHelpers.cs | 4 +-- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs index c3a2c2991..f0459bca3 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SliceLayoutEngineTests.cs @@ -23,6 +23,22 @@ public void Name(IContext ctx, SliceLayoutPlugin plugin) Assert.Equal("Slice", name); } + [Theory, AutoSubstituteData] + public void Name_Changed(IContext ctx, SliceLayoutPlugin plugin, IWindow window) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()) + { + Name = "Paradise Shelduck" + }; + + // When + sut = sut.AddWindow(window); + + // Then + Assert.Equal("Paradise Shelduck", sut.Name); + } + [Theory] [InlineAutoSubstituteData(0)] [InlineAutoSubstituteData(1)] diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 050e72107..63a194d86 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -54,41 +54,45 @@ public partial record SliceLayoutEngine : ILayoutEngine /// private readonly ParentArea _prunedRootArea; - private SliceLayoutEngine( + private SliceLayoutEngine(SliceLayoutEngine engine, ImmutableList windows) + { + _context = engine._context; + _plugin = engine._plugin; + Identity = engine.Identity; + Name = engine.Name; + + _windows = windows; + + (_rootArea, _windowAreas) = engine._rootArea.SetStartIndexes(); + _prunedRootArea = _rootArea.Prune(_windows.Count); + } + + public SliceLayoutEngine( IContext context, ISliceLayoutPlugin plugin, LayoutEngineIdentity identity, - ImmutableList windows, ParentArea rootArea ) { _context = context; _plugin = plugin; Identity = identity; - _windows = windows; + _windows = ImmutableList.Empty; (_rootArea, _windowAreas) = rootArea.SetStartIndexes(); _prunedRootArea = _rootArea.Prune(_windows.Count); } - public SliceLayoutEngine( - IContext context, - ISliceLayoutPlugin plugin, - LayoutEngineIdentity identity, - ParentArea rootArea - ) - : this(context, plugin, identity, ImmutableList.Empty, rootArea) { } - public ILayoutEngine AddWindow(IWindow window) { Logger.Debug($"Adding {window}"); - return new SliceLayoutEngine(_context, _plugin, Identity, _windows.Add(window), _rootArea); + return new SliceLayoutEngine(this, _windows.Add(window)); } public ILayoutEngine RemoveWindow(IWindow window) { Logger.Debug($"Removing {window}"); - return new SliceLayoutEngine(_context, _plugin, Identity, _windows.Remove(window), _rootArea); + return new SliceLayoutEngine(this, _windows.Remove(window)); } public bool ContainsWindow(IWindow window) diff --git a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs index 4f3676a96..9cf8accda 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngineHelpers.cs @@ -98,7 +98,7 @@ private ILayoutEngine SwapWindowIndices(int currentIndex, int targetIndex) .SetItem(currentIndex, targetWindow) .SetItem(targetIndex, currentWindow); - return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); + return new SliceLayoutEngine(this, newWindows); } private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) @@ -112,7 +112,7 @@ private ILayoutEngine RotateWindowIndices(int currentIndex, int targetIndex) IWindow currentWindow = _windows[currentIndex]; ImmutableList newWindows = _windows.RemoveAt(currentIndex).Insert(targetIndex, currentWindow); - return new SliceLayoutEngine(_context, _plugin, Identity, newWindows, _rootArea); + return new SliceLayoutEngine(this, newWindows); } /// From 4561b6469577be2ed479efc3c367d06bbc14c8ed Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 21:35:33 +1300 Subject: [PATCH 56/64] Fix `ArgumentOutOfRangeException` --- .../MoveWindowToPointTests.cs | 13 +++++++++++++ .../SwapWindowInDirectionTests.cs | 13 +++++++++++++ src/Whim.SliceLayout/SliceLayoutEngine.cs | 18 ++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs index cca32b971..6808e2900 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/MoveWindowToPointTests.cs @@ -118,4 +118,17 @@ ISliceLayoutPlugin plugin // Then the window should not have moved Assert.Equal(windows[windowIdx], windowStates[windowIdx].Window); } + + [Theory, AutoSubstituteData] + public void MoveWindowToPoint_WindowNotFound(IContext ctx, SliceLayoutPlugin plugin, IWindow window) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + + // When + ILayoutEngine resultSut = sut.MoveWindowToPoint(window, new Point(0.5, 0.5)); + + // Then + Assert.Same(sut, resultSut); + } } diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs index 34a5884de..d05525a00 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutEngine/SwapWindowInDirectionTests.cs @@ -137,4 +137,17 @@ public void SwapWindowInDirection_NoWindowInDirection(IContext ctx, ISliceLayout // Then Assert.Equal(beforeStates, afterStates); } + + [Theory, AutoSubstituteData] + public void SwapWindowInDirection_WindowNotFound(IContext ctx, ISliceLayoutPlugin plugin, IWindow window) + { + // Given + ILayoutEngine sut = new SliceLayoutEngine(ctx, plugin, identity, SampleSliceLayouts.CreateNestedLayout()); + + // When + ILayoutEngine resultSut = sut.SwapWindowInDirection(Direction.Up, window); + + // Then + Assert.Equal(sut, resultSut); + } } diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index 63a194d86..c9bfa3d98 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -151,25 +151,39 @@ public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) { Logger.Debug($"Moving {window} to {point}"); + int windowIdx = _windows.IndexOf(window); + if (windowIdx == -1) + { + Logger.Error($"Window not found"); + return this; + } + if (GetWindowAtPoint(point) is not (int, IWindow) windowAtPoint) { return this; } - return MoveWindowToIndex(_windows.IndexOf(window), windowAtPoint.Index); + return MoveWindowToIndex(windowIdx, windowAtPoint.Index); } public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) { Logger.Debug($"Swapping {window} in direction {direction}"); + int windowIdx = _windows.IndexOf(window); + if (windowIdx == -1) + { + Logger.Error($"Window not found"); + return this; + } + IWindow? windowInDirection = GetWindowInDirection(direction, window); if (windowInDirection == null) { return this; } - return MoveWindowToIndex(_windows.IndexOf(window), _windows.IndexOf(windowInDirection)); + return MoveWindowToIndex(windowIdx, _windows.IndexOf(windowInDirection)); } public ILayoutEngine PerformCustomAction(LayoutEngineCustomAction action) => From a9c34518ec8aab6cab8ba20b7d20fb46020a6945 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 22:55:59 +1300 Subject: [PATCH 57/64] Take the first overflow area --- README.md | 2 ++ .../AreaHelpers/PruneTests.cs | 20 +++++++++++++++ src/Whim.SliceLayout/AreaHelpers.cs | 25 +++++++++++++++---- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 86c872b58..511ec2417 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ context.WorkspaceManager.Add("Alt"); // Set up layout engines. context.WorkspaceManager.CreateLayoutEngines = () => new CreateLeafLayoutEngine[] { + (id) => SliceLayouts.CreateMultiColumnLayout(context, sliceLayoutPlugin, id, 1, 2, 0), + (id) => SliceLayouts.CreatePrimaryStackLayout(context, sliceLayoutPlugin, id), (id) => new TreeLayoutEngine(context, treeLayoutPlugin, id), (id) => new ColumnLayoutEngine(id) }; diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs index 348f37e1f..8822ad78d 100644 --- a/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs @@ -193,6 +193,26 @@ public static IEnumerable Prune_Nested() }; } + // Take the first overflow area + public static IEnumerable Prune_MultipleOverflows() + { + yield return new object[] + { + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 1)), + (0.25, new OverflowArea()), + (0.25, new OverflowArea()) + ), + 2, + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 1)), + (0.5, new OverflowArea() { StartIndex = 1 }) + ), + }; + } + [Theory] [MemberData(nameof(Prune_PrimaryStack))] [MemberData(nameof(Prune_MultiColumn))] diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index 39f307dbe..4f549e0de 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -18,6 +18,7 @@ public static ParentArea Prune(this ParentArea area, int windowCount) ImmutableList.Builder weightsBuilder = ImmutableList.CreateBuilder(); double ignoredWeight = 0; + bool foundOverflowArea = false; for (int i = 0; i < area.Children.Count; i++) { IArea child = area.Children[i]; @@ -33,12 +34,26 @@ public static ParentArea Prune(this ParentArea area, int windowCount) child = prunedParentArea; } - else if (child is BaseSliceArea baseSliceArea) + else if (child is OverflowArea overflowArea) { - if ( - baseSliceArea.StartIndex >= windowCount - || (baseSliceArea is SliceArea sliceArea && sliceArea.MaxChildren == 0) - ) + if (foundOverflowArea) + { + Logger.Error($"Found multiple overflow areas, ignoring"); + ignoredWeight += area.Weights[i]; + continue; + } + + if (overflowArea.StartIndex >= windowCount) + { + ignoredWeight += area.Weights[i]; + continue; + } + + foundOverflowArea = true; + } + else if (child is SliceArea sliceArea) + { + if (sliceArea.StartIndex >= windowCount || sliceArea.MaxChildren == 0) { ignoredWeight += area.Weights[i]; continue; From 69321ffc203d2d11094c892b97d3ea907c93e18c Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 23:07:09 +1300 Subject: [PATCH 58/64] Replace TODO with issue reference --- src/Whim.SliceLayout/SliceLayoutEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index c9bfa3d98..bbefb88a1 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -143,7 +143,7 @@ public void FocusWindowInDirection(Direction direction, IWindow window) public ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint deltas, IWindow window) { - // TODO: Put issue reference here + // See #738 return this; } From 63a9edcc187600fedea721687fa1d21961ad3d97 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 23:08:52 +1300 Subject: [PATCH 59/64] Remove unused method --- src/Whim.FloatingLayout/FloatingLayoutEngine.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Whim.FloatingLayout/FloatingLayoutEngine.cs b/src/Whim.FloatingLayout/FloatingLayoutEngine.cs index 284b150db..243a57dc7 100644 --- a/src/Whim.FloatingLayout/FloatingLayoutEngine.cs +++ b/src/Whim.FloatingLayout/FloatingLayoutEngine.cs @@ -69,15 +69,6 @@ private FloatingLayoutEngine UpdateInner(ILayoutEngine newInnerLayoutEngine, IWi : new FloatingLayoutEngine(this, newInnerLayoutEngine, newFloatingWindowRects); } - /// - /// Returns a new instance of with the given inner layout engine, - /// Only use this for updates that do not involve a window. - /// - /// - /// - private FloatingLayoutEngine UpdateInner(ILayoutEngine newInnerLayoutEngine) => - InnerLayoutEngine == newInnerLayoutEngine ? this : new FloatingLayoutEngine(this, newInnerLayoutEngine); - /// public override ILayoutEngine AddWindow(IWindow window) { From 9797408f85154dc1124d63bf4c4a9ebb6daedec0 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Mon, 25 Dec 2023 23:47:43 +1300 Subject: [PATCH 60/64] Documentation --- README.md | 82 +++++++++++++++++-- src/Directory.Build.props | 2 +- .../Directory.Build.props | 5 ++ src/Whim.SliceLayout/Area.cs | 51 +++++++++++- src/Whim.SliceLayout/ISliceLayoutPlugin.cs | 5 ++ src/Whim.SliceLayout/SliceLayoutEngine.cs | 23 ++++++ src/Whim.SliceLayout/SliceLayoutPlugin.cs | 20 +++++ src/Whim.SliceLayout/SliceLayouts.cs | 11 ++- src/Whim.TreeLayout/ITreeLayoutPlugin.cs | 4 +- src/Whim.TreeLayout/TreeLayoutEngine.cs | 1 + 10 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 src/Whim.SliceLayout.Tests/Directory.Build.props diff --git a/README.md b/README.md index 511ec2417..7b121d08e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,72 @@ context.PluginManager.AddPlugin(barPlugin); Each plugin needs to be added to the `context` object. +### Layout Engines + +#### `SliceLayoutEngine` + +`SliceLayoutEngine` is a layout engine that internally stores an ordered list of `IWindow`s. The monitor is divided into a number of `IArea`s. Each `IArea` corresponds to a "slice" of the `IWindow` list. + +```csharp + context.WorkspaceManager.CreateLayoutEngines = () => new CreateLeafLayoutEngine[] + { + (id) => new SliceLayoutEngine( + context, + sliceLayoutPlugin, + id, + new ParentArea( + isRow: true, + (0.5, new OverflowArea()), + (0.5, new SliceArea(order: 0, maxChildren: 2)) + ) + ) { Name = "Overflow on left" }, + + (id) => new SliceLayoutEngine( + context, + sliceLayoutPlugin, + id, + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 1)), + (0.25, new OverflowArea()), (0.25, new OverflowArea()) + ) + ) { Name = "Multiple overflows"}, + + (id) => SliceLayouts.CreateMultiColumnLayout(context, sliceLayoutPlugin, id, 1, 2, 0), + (id) => SliceLayouts.CreatePrimaryStackLayout(context, sliceLayoutPlugin, id) + }; +``` + +There are three types of `IArea`s: + +- `ParentArea`: An area that can have any `IArea` implementation as a child +- `SliceArea`: An ordered area that can have any `IWindow` as a child. There can be multiple `SliceArea`s in a `SliceLayoutEngine`, and they are ordered by the `Order` property/parameter. +- `OverflowArea`: An area that can have any infinite number of `IWindow`s as a child. There can be only one `OverflowArea` in a `SliceLayoutEngine` - any additional `OverflowArea`s will be ignored. `OverflowArea`s implicitly are the last area in the layout engine, in comparison to all `SliceArea`s. + +The `SliceLayouts` contains methods to create a few common layouts: + +- primary/stack (master/stack) +- multi-column layout +- three-column layout, with the middle column being the primary + +`SliceLayoutEngine` requires the `SliceLayoutPlugin` to be added to the `context` object: + +```csharp +SliceLayoutPlugin sliceLayoutPlugin = new(context); +context.PluginManager.AddPlugin(sliceLayoutPlugin); +``` + +#### `TreeLayoutEngine` + +`TreeLayoutEngine` is a layout that allows users to create arbitrary grid layouts. Unlike `SliceLayoutEngine`, windows can can be added in any location. + +`TreeLayoutEngine` requires the `TreeLayoutPlugin` to be added to the `context` object: + +```csharp +TreeLayoutPlugin treeLayoutPlugin = new(context); +context.PluginManager.AddPlugin(treeLayoutPlugin); +``` + ### Commands Whim stores commands ([`ICommand`](src/Whim/Commands/ICommand.cs)), which are objects with a unique identifier, title, and executable action. Commands expose easy access to functionality from Whim's core, and loaded plugins. @@ -222,14 +288,14 @@ See [`GapsCommands.cs`](src/Whim.Gaps/GapsCommands.cs). See [`SliceLayoutCommands.cs`](src/Whim.SliceLayout/SliceLayoutCommands.cs). -| Identifier | Title | Keybind | -| --------------------------- | ---------------------------------- | ------- | ------------------ | -| `set_insertion_type.swap` | Set slice insertion type to swap | Keybind | No default keybind | -| `set_insertion_type.rotate` | Set slice insertion type to rotate | Keybind | No default keybind | -| `window.promote` | Promote window in stack | Keybind | No default keybind | -| `window.demote` | Demote window in stack | Keybind | No default keybind | -| `focus.promote` | Promote focus in stack | Keybind | No default keybind | -| `focus.demote` | Demote focus in stack | Keybind | No default keybind | +| Identifier | Title | Keybind | +| --------------------------------------------- | ---------------------------------- | ------------------ | +| `whim.slice_layout.set_insertion_type.swap` | Set slice insertion type to swap | No default keybind | +| `whim.slice_layout.set_insertion_type.rotate` | Set slice insertion type to rotate | No default keybind | +| `whim.slice_layout.window.promote` | Promote window in stack | No default keybind | +| `whim.slice_layout.window.demote` | Demote window in stack | No default keybind | +| `whim.slice_layout.focus.promote` | Promote focus in stack | No default keybind | +| `whim.slice_layout.focus.demote` | Demote focus in stack | No default keybind | ##### Tree Layout Plugin Commands diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b8e516cc3..273df1851 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,5 +1,5 @@ - false + true \ No newline at end of file diff --git a/src/Whim.SliceLayout.Tests/Directory.Build.props b/src/Whim.SliceLayout.Tests/Directory.Build.props new file mode 100644 index 000000000..b8e516cc3 --- /dev/null +++ b/src/Whim.SliceLayout.Tests/Directory.Build.props @@ -0,0 +1,5 @@ + + + false + + \ No newline at end of file diff --git a/src/Whim.SliceLayout/Area.cs b/src/Whim.SliceLayout/Area.cs index cdcabc382..0e2e0d172 100644 --- a/src/Whim.SliceLayout/Area.cs +++ b/src/Whim.SliceLayout/Area.cs @@ -2,26 +2,47 @@ namespace Whim.SliceLayout; +/// +/// Represents an area in the layout engine. +/// public interface IArea { /// - /// When , the are arranged horizontally. - /// Otherwise, they are arranged vertically. + /// When , its children are arranged horizontally. Otherwise, they are + /// arranged vertically. /// bool IsRow { get; } } +/// public abstract record BaseArea : IArea { + /// public bool IsRow { get; protected set; } } +/// +/// Represents an area that can have any as a child. +/// public record ParentArea : BaseArea { + /// + /// Weights of the children. The sum of the weights should be 1.0. + /// public ImmutableList Weights { get; } + /// + /// Children of this area. + /// public ImmutableList Children { get; } + /// + /// Creates a new with the given children. + /// + /// + /// + /// A tuple of the weight and the child. The sum of the weights should be 1.0. + /// public ParentArea(bool isRow, params (double Weight, IArea Child)[] children) { IsRow = isRow; @@ -46,11 +67,19 @@ internal ParentArea(bool isRow, ImmutableList weights, ImmutableList +/// Represents an implicit slice of the list of s. The windows are contained +/// by the . +/// public record BaseSliceArea : BaseArea { internal int StartIndex { get; set; } } +/// +/// An area that can have s as children. There can be multiple +/// s in a layout engine, ordered by their s. +/// public record SliceArea : BaseSliceArea { /// @@ -63,6 +92,12 @@ public record SliceArea : BaseSliceArea /// public int MaxChildren { get; } + /// + /// Creates a new with the given order and maximum number of children. + /// + /// + /// + /// public SliceArea(uint order = 0, uint maxChildren = 1, bool isRow = false) { Order = (int)order; @@ -71,8 +106,20 @@ public SliceArea(uint order = 0, uint maxChildren = 1, bool isRow = false) } } +/// +/// An area that can have an infinite number of s as a child. +/// There can be only one in a layout engine - any additional +/// s will be ignored. +/// +/// s implicitly are the last area in the layout engine, in comparison +/// to all s. +/// public record OverflowArea : BaseSliceArea { + /// + /// Creates a new . + /// + /// public OverflowArea(bool isRow = false) { IsRow = isRow; diff --git a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs index 2f02b5479..65ec767fa 100644 --- a/src/Whim.SliceLayout/ISliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/ISliceLayoutPlugin.cs @@ -16,6 +16,11 @@ public enum WindowInsertionType Rotate } +/// +/// provides commands and functionality for the . +/// does not load the - that is done when creating +/// a workspace via . +/// public interface ISliceLayoutPlugin : IPlugin { /// diff --git a/src/Whim.SliceLayout/SliceLayoutEngine.cs b/src/Whim.SliceLayout/SliceLayoutEngine.cs index bbefb88a1..4292b1f38 100644 --- a/src/Whim.SliceLayout/SliceLayoutEngine.cs +++ b/src/Whim.SliceLayout/SliceLayoutEngine.cs @@ -28,10 +28,13 @@ public partial record SliceLayoutEngine : ILayoutEngine private readonly ParentArea _rootArea; private readonly ISliceLayoutPlugin _plugin; + /// public string Name { get; init; } = "Slice"; + /// public int Count => _windows.Count; + /// public LayoutEngineIdentity Identity { get; } private const int _cachedWindowStatesScale = 10000; @@ -67,6 +70,16 @@ private SliceLayoutEngine(SliceLayoutEngine engine, ImmutableList windo _prunedRootArea = _rootArea.Prune(_windows.Count); } + /// + /// Create a new . + /// + /// + /// + /// + /// + /// The structure of the tree of areas. This must be a , and should + /// contain at least one . + /// public SliceLayoutEngine( IContext context, ISliceLayoutPlugin plugin, @@ -83,24 +96,28 @@ ParentArea rootArea _prunedRootArea = _rootArea.Prune(_windows.Count); } + /// public ILayoutEngine AddWindow(IWindow window) { Logger.Debug($"Adding {window}"); return new SliceLayoutEngine(this, _windows.Add(window)); } + /// public ILayoutEngine RemoveWindow(IWindow window) { Logger.Debug($"Removing {window}"); return new SliceLayoutEngine(this, _windows.Remove(window)); } + /// public bool ContainsWindow(IWindow window) { Logger.Debug($"Checking if {window} is contained"); return _windows.Contains(window); } + /// public IEnumerable DoLayout(IRectangle rectangle, IMonitor monitor) { Logger.Debug($"Doing layout on {rectangle} on {monitor}"); @@ -129,24 +146,28 @@ public IEnumerable DoLayout(IRectangle rectangle, IMonitor mo return windowStates; } + /// public void FocusWindowInDirection(Direction direction, IWindow window) { Logger.Debug($"Focusing window in direction {direction} from {window}"); GetWindowInDirection(direction, window)?.Focus(); } + /// public IWindow? GetFirstWindow() { Logger.Debug($"Getting first window"); return _windows.Count > 0 ? _windows[0] : null; } + /// public ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint deltas, IWindow window) { // See #738 return this; } + /// public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) { Logger.Debug($"Moving {window} to {point}"); @@ -166,6 +187,7 @@ public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint point) return MoveWindowToIndex(windowIdx, windowAtPoint.Index); } + /// public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) { Logger.Debug($"Swapping {window} in direction {direction}"); @@ -186,6 +208,7 @@ public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window) return MoveWindowToIndex(windowIdx, _windows.IndexOf(windowInDirection)); } + /// public ILayoutEngine PerformCustomAction(LayoutEngineCustomAction action) => action switch { diff --git a/src/Whim.SliceLayout/SliceLayoutPlugin.cs b/src/Whim.SliceLayout/SliceLayoutPlugin.cs index b7ce0fa79..c1467b551 100644 --- a/src/Whim.SliceLayout/SliceLayoutPlugin.cs +++ b/src/Whim.SliceLayout/SliceLayoutPlugin.cs @@ -2,39 +2,57 @@ namespace Whim.SliceLayout; +/// public class SliceLayoutPlugin : ISliceLayoutPlugin { private readonly IContext _context; + /// + /// Create a new . + /// + /// public SliceLayoutPlugin(IContext context) { _context = context; } + /// public string Name => "whim.slice_layout"; + /// public string PromoteWindowActionName => $"{Name}.window.promote"; + /// public string DemoteWindowActionName => $"{Name}.window.demote"; + /// public string PromoteFocusActionName => $"{Name}.focus.promote"; + /// public string DemoteFocusActionName => $"{Name}.focus.demote"; + /// public IPluginCommands PluginCommands => new SliceLayoutCommands(this); + /// public WindowInsertionType WindowInsertionType { get; set; } + /// public void PreInitialize() { } + /// public void PostInitialize() { } + /// public void LoadState(JsonElement state) { } + /// public JsonElement? SaveState() => null; + /// public void PromoteWindowInStack(IWindow? window = null) => ChangeWindowRank(window, promote: true); + /// public void DemoteWindowInStack(IWindow? window = null) => ChangeWindowRank(window, promote: false); private void ChangeWindowRank(IWindow? window, bool promote) @@ -73,8 +91,10 @@ private void ChangeWindowRank(IWindow? window, bool promote) return (window, workspace); } + /// public void PromoteFocusInStack(IWindow? window = null) => FocusWindowRank(window, promote: true); + /// public void DemoteFocusInStack(IWindow? window = null) => FocusWindowRank(window, promote: false); private void FocusWindowRank(IWindow? window, bool promote) diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index fcd018292..df5ef170c 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -2,6 +2,9 @@ namespace Whim.SliceLayout; +/// +/// Methods to create common slice layouts. +/// public static class SliceLayouts { /// @@ -103,7 +106,7 @@ internal static ParentArea CreateMultiColumnArea(uint[] capacities) } /// - /// Creates a multi-column layout, where the primary column is in the middle, the secondary + /// Creates a three-column layout, where the primary column is in the middle, the secondary /// column is on the left, and the overflow column is on the right. /// /// The middle column takes up 50% of the screen, and the left and right columns take up 25%. @@ -143,6 +146,12 @@ internal static ParentArea CreateMultiColumnArea(uint[] capacities) /// /// /// + /// + /// The number of rows in the primary column. This must be a non-negative integer. + /// + /// + /// The number of rows in the secondary column. This must be a non-negative integer. + /// /// public static ILayoutEngine CreateSecondaryPrimaryLayout( IContext context, diff --git a/src/Whim.TreeLayout/ITreeLayoutPlugin.cs b/src/Whim.TreeLayout/ITreeLayoutPlugin.cs index 7c91ca23e..3263ffa16 100644 --- a/src/Whim.TreeLayout/ITreeLayoutPlugin.cs +++ b/src/Whim.TreeLayout/ITreeLayoutPlugin.cs @@ -3,8 +3,8 @@ namespace Whim.TreeLayout; /// -/// TreeLayoutPlugin provides commands and functionality for the . -/// TreeLayoutPlugin does not load the - that is done when creating +/// provides commands and functionality for the . +/// does not load the - that is done when creating /// a workspace via . /// public interface ITreeLayoutPlugin : IPlugin diff --git a/src/Whim.TreeLayout/TreeLayoutEngine.cs b/src/Whim.TreeLayout/TreeLayoutEngine.cs index 3ae9d818e..51e580460 100644 --- a/src/Whim.TreeLayout/TreeLayoutEngine.cs +++ b/src/Whim.TreeLayout/TreeLayoutEngine.cs @@ -685,5 +685,6 @@ WindowNode bNode return new TreeLayoutEngine(this, currentNode, newWindows); } + /// public ILayoutEngine PerformCustomAction(LayoutEngineCustomAction action) => this; } From 1f58436ff2868d74d53421ef3ae32b9ac073c16e Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Tue, 26 Dec 2023 10:26:04 +1300 Subject: [PATCH 61/64] Coverage --- .../SliceLayoutPluginTests.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs b/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs index 9012745c0..39e3f13a8 100644 --- a/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs +++ b/src/Whim.SliceLayout.Tests/SliceLayoutPluginTests.cs @@ -208,4 +208,110 @@ public void WindowInsertionType_Set(WindowInsertionType insertionType) // Then Assert.Equal(insertionType, plugin.WindowInsertionType); } + + #region PromoteFocusInStack + [Theory, AutoSubstituteData] + public void PromoteFocusInStack_NoWindow(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.ReturnsNull(); + + // When + plugin.PromoteFocusInStack(); + + // Then nothing + ctx.WorkspaceManager.DidNotReceive().GetWorkspaceForWindow(Arg.Any()); + } + + [Theory, AutoSubstituteData] + public void PromoteFocusInStack_NoWorkspace(IContext ctx, IWindow window) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.Returns(window); + ctx.WorkspaceManager.GetWorkspaceForWindow(window).ReturnsNull(); + + // When + plugin.PromoteFocusInStack(window); + + // Then nothing + ctx.WorkspaceManager.ActiveWorkspace.DidNotReceive() + .PerformCustomLayoutEngineAction(Arg.Any()); + } + + [Theory, AutoSubstituteData] + public void PromoteFocusInStack(IContext ctx, IWindow window, IWorkspace workspace) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.Returns(window); + ctx.WorkspaceManager.GetWorkspaceForWindow(window).Returns(workspace); + + // When + plugin.PromoteFocusInStack(window); + + // Then + workspace + .Received(1) + .PerformCustomLayoutEngineAction( + Arg.Is( + action => action.Name == plugin.PromoteFocusActionName && action.Window == window + ) + ); + } + #endregion + + #region DemoteFocusInStack + [Theory, AutoSubstituteData] + public void DemoteFocusInStack_NoWindow(IContext ctx) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.ReturnsNull(); + + // When + plugin.DemoteFocusInStack(); + + // Then nothing + ctx.WorkspaceManager.DidNotReceive().GetWorkspaceForWindow(Arg.Any()); + } + + [Theory, AutoSubstituteData] + public void DemoteFocusInStack_NoWorkspace(IContext ctx, IWindow window) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.Returns(window); + ctx.WorkspaceManager.GetWorkspaceForWindow(window).ReturnsNull(); + + // When + plugin.DemoteFocusInStack(window); + + // Then nothing + ctx.WorkspaceManager.ActiveWorkspace.DidNotReceive() + .PerformCustomLayoutEngineAction(Arg.Any()); + } + + [Theory, AutoSubstituteData] + public void DemoteFocusInStack(IContext ctx, IWindow window, IWorkspace workspace) + { + // Given + SliceLayoutPlugin plugin = new(ctx); + ctx.WorkspaceManager.ActiveWorkspace.LastFocusedWindow.Returns(window); + ctx.WorkspaceManager.GetWorkspaceForWindow(window).Returns(workspace); + + // When + plugin.DemoteFocusInStack(window); + + // Then + workspace + .Received(1) + .PerformCustomLayoutEngineAction( + Arg.Is( + action => action.Name == plugin.DemoteFocusActionName && action.Window == window + ) + ); + } + #endregion } From c5e28adc02dffb58cec9dc8c1eadeec4896ca1e3 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Tue, 26 Dec 2023 10:42:16 +1300 Subject: [PATCH 62/64] Docs and update csx --- README.md | 67 +++++++++++++++++-------------- src/Whim/Template/whim.config.csx | 9 +++++ 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 7b121d08e..fbe8fad90 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ context.WorkspaceManager.CreateLayoutEngines = () => new CreateLeafLayoutEngine[ { (id) => SliceLayouts.CreateMultiColumnLayout(context, sliceLayoutPlugin, id, 1, 2, 0), (id) => SliceLayouts.CreatePrimaryStackLayout(context, sliceLayoutPlugin, id), + (id) => SliceLayouts.CreateSecondaryPrimaryLayout(context, sliceLayoutPlugin, id), (id) => new TreeLayoutEngine(context, treeLayoutPlugin, id), (id) => new ColumnLayoutEngine(id) }; @@ -73,41 +74,13 @@ Each plugin needs to be added to the `context` object. `SliceLayoutEngine` is a layout engine that internally stores an ordered list of `IWindow`s. The monitor is divided into a number of `IArea`s. Each `IArea` corresponds to a "slice" of the `IWindow` list. -```csharp - context.WorkspaceManager.CreateLayoutEngines = () => new CreateLeafLayoutEngine[] - { - (id) => new SliceLayoutEngine( - context, - sliceLayoutPlugin, - id, - new ParentArea( - isRow: true, - (0.5, new OverflowArea()), - (0.5, new SliceArea(order: 0, maxChildren: 2)) - ) - ) { Name = "Overflow on left" }, - - (id) => new SliceLayoutEngine( - context, - sliceLayoutPlugin, - id, - new ParentArea( - isRow: true, - (0.5, new SliceArea(order: 0, maxChildren: 1)), - (0.25, new OverflowArea()), (0.25, new OverflowArea()) - ) - ) { Name = "Multiple overflows"}, - - (id) => SliceLayouts.CreateMultiColumnLayout(context, sliceLayoutPlugin, id, 1, 2, 0), - (id) => SliceLayouts.CreatePrimaryStackLayout(context, sliceLayoutPlugin, id) - }; -``` - There are three types of `IArea`s: - `ParentArea`: An area that can have any `IArea` implementation as a child - `SliceArea`: An ordered area that can have any `IWindow` as a child. There can be multiple `SliceArea`s in a `SliceLayoutEngine`, and they are ordered by the `Order` property/parameter. -- `OverflowArea`: An area that can have any infinite number of `IWindow`s as a child. There can be only one `OverflowArea` in a `SliceLayoutEngine` - any additional `OverflowArea`s will be ignored. `OverflowArea`s implicitly are the last area in the layout engine, in comparison to all `SliceArea`s. +- `OverflowArea`: An area that can have any infinite number of `IWindow`s as a child. There can be only one `OverflowArea` in a `SliceLayoutEngine` - any additional `OverflowArea`s will be ignored. + +`OverflowArea`s are implicitly the last ordered area in the layout engine, in comparison to all `SliceArea`s. The `SliceLayouts` contains methods to create a few common layouts: @@ -115,6 +88,38 @@ The `SliceLayouts` contains methods to create a few common layouts: - multi-column layout - three-column layout, with the middle column being the primary +Arbitrary layouts can be created by nesting areas. + +```csharp +context.WorkspaceManager.CreateLayoutEngines = () => new CreateLeafLayoutEngine[] +{ + (id) => new SliceLayoutEngine( + context, + sliceLayoutPlugin, + id, + new ParentArea( + isRow: true, + (0.5, new OverflowArea()), + (0.5, new SliceArea(order: 0, maxChildren: 2)) + ) + ) { Name = "Overflow on left" }, + + (id) => new SliceLayoutEngine( + context, + sliceLayoutPlugin, + id, + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 1)), + (0.25, new OverflowArea()), (0.25, new OverflowArea()) + ) + ) { Name = "Multiple overflows"}, + + (id) => SliceLayouts.CreateMultiColumnLayout(context, sliceLayoutPlugin, id, 1, 2, 0), + (id) => SliceLayouts.CreatePrimaryStackLayout(context, sliceLayoutPlugin, id) +}; +``` + `SliceLayoutEngine` requires the `SliceLayoutPlugin` to be added to the `context` object: ```csharp diff --git a/src/Whim/Template/whim.config.csx b/src/Whim/Template/whim.config.csx index 8a207148a..eccc4b94a 100644 --- a/src/Whim/Template/whim.config.csx +++ b/src/Whim/Template/whim.config.csx @@ -6,6 +6,7 @@ #r "WHIM_PATH\plugins\Whim.FocusIndicator\Whim.FocusIndicator.dll" #r "WHIM_PATH\plugins\Whim.Gaps\Whim.Gaps.dll" #r "WHIM_PATH\plugins\Whim.LayoutPreview\Whim.LayoutPreview.dll" +#r "WHIM_PATH\plugins\Whim.SliceLayout\Whim.SliceLayout.dll" #r "WHIM_PATH\plugins\Whim.TreeLayout\Whim.TreeLayout.dll" #r "WHIM_PATH\plugins\Whim.TreeLayout.Bar\Whim.TreeLayout.Bar.dll" #r "WHIM_PATH\plugins\Whim.TreeLayout.CommandPalette\Whim.TreeLayout.CommandPalette.dll" @@ -20,6 +21,7 @@ using Whim.FloatingLayout; using Whim.FocusIndicator; using Whim.Gaps; using Whim.LayoutPreview; +using Whim.SliceLayout; using Whim.TreeLayout; using Whim.TreeLayout.Bar; using Whim.TreeLayout.CommandPalette; @@ -68,6 +70,10 @@ void DoConfig(IContext context) CommandPalettePlugin commandPalettePlugin = new(context, commandPaletteConfig); context.PluginManager.AddPlugin(commandPalettePlugin); + // Slice layout. + SliceLayoutPlugin sliceLayoutPlugin = new(context); + context.PluginManager.AddPlugin(sliceLayoutPlugin); + // Tree layout. TreeLayoutPlugin treeLayoutPlugin = new(context); context.PluginManager.AddPlugin(treeLayoutPlugin); @@ -99,6 +105,9 @@ void DoConfig(IContext context) // Set up layout engines. context.WorkspaceManager.CreateLayoutEngines = () => new CreateLeafLayoutEngine[] { + (id) => SliceLayouts.CreateMultiColumnLayout(context, sliceLayoutPlugin, id, 1, 2, 0), + (id) => SliceLayouts.CreatePrimaryStackLayout(context, sliceLayoutPlugin, id), + (id) => SliceLayouts.CreateSecondaryPrimaryLayout(context, sliceLayoutPlugin, id), (id) => new TreeLayoutEngine(context, treeLayoutPlugin, id), (id) => new ColumnLayoutEngine(id) }; From aafaf1eef3e26b1171f8bcc64b66cd868c06195c Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Tue, 26 Dec 2023 11:09:29 +1300 Subject: [PATCH 63/64] Handle multiple overflow areas when there is only a single window --- src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs | 12 ++++++++++++ src/Whim.SliceLayout/AreaHelpers.cs | 3 +-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs b/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs index 8822ad78d..99783a88d 100644 --- a/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs +++ b/src/Whim.SliceLayout.Tests/AreaHelpers/PruneTests.cs @@ -196,6 +196,18 @@ public static IEnumerable Prune_Nested() // Take the first overflow area public static IEnumerable Prune_MultipleOverflows() { + yield return new object[] + { + new ParentArea( + isRow: true, + (0.5, new SliceArea(order: 0, maxChildren: 1)), + (0.25, new OverflowArea()), + (0.25, new OverflowArea()) + ), + 1, + new ParentArea(isRow: true, (0.5, new SliceArea(order: 0, maxChildren: 1))), + }; + yield return new object[] { new ParentArea( diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index 4f549e0de..4eaa62454 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -43,13 +43,12 @@ public static ParentArea Prune(this ParentArea area, int windowCount) continue; } + foundOverflowArea = true; if (overflowArea.StartIndex >= windowCount) { ignoredWeight += area.Weights[i]; continue; } - - foundOverflowArea = true; } else if (child is SliceArea sliceArea) { From e9bf22d705afc4cdc112934a17a5c28352bd9824 Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Tue, 26 Dec 2023 11:28:15 +1300 Subject: [PATCH 64/64] Docs --- README.md | 2 +- src/Whim.SliceLayout/AreaHelpers.cs | 5 ++--- src/Whim.SliceLayout/SliceLayouts.cs | 30 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fbe8fad90..a92be7ce8 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ There are three types of `IArea`s: - `ParentArea`: An area that can have any `IArea` implementation as a child - `SliceArea`: An ordered area that can have any `IWindow` as a child. There can be multiple `SliceArea`s in a `SliceLayoutEngine`, and they are ordered by the `Order` property/parameter. -- `OverflowArea`: An area that can have any infinite number of `IWindow`s as a child. There can be only one `OverflowArea` in a `SliceLayoutEngine` - any additional `OverflowArea`s will be ignored. +- `OverflowArea`: An area that can have any infinite number of `IWindow`s as a child. There can be only one `OverflowArea` in a `SliceLayoutEngine` - any additional `OverflowArea`s will be ignored. If no `OverflowArea` is specified, the `SliceLayoutEngine` will replace the last `SliceArea` with an `OverflowArea`. `OverflowArea`s are implicitly the last ordered area in the layout engine, in comparison to all `SliceArea`s. diff --git a/src/Whim.SliceLayout/AreaHelpers.cs b/src/Whim.SliceLayout/AreaHelpers.cs index 4eaa62454..536b3655a 100644 --- a/src/Whim.SliceLayout/AreaHelpers.cs +++ b/src/Whim.SliceLayout/AreaHelpers.cs @@ -139,9 +139,8 @@ private static (ParentArea Parent, BaseSliceArea[] OrderedSliceAreas) SingleOver } /// - /// Get the areas sorted by order, and the overflow area if it exists. - /// We don't handle the case where there are multiple overflow areas, as overflow areas are - /// greedy. + /// Get the areas sorted by order, and the overflow area if it exists. We take the last overflow + /// area if there are multiple. /// /// /// diff --git a/src/Whim.SliceLayout/SliceLayouts.cs b/src/Whim.SliceLayout/SliceLayouts.cs index df5ef170c..cc1cc15c3 100644 --- a/src/Whim.SliceLayout/SliceLayouts.cs +++ b/src/Whim.SliceLayout/SliceLayouts.cs @@ -10,6 +10,36 @@ public static class SliceLayouts /// /// Creates a primary stack layout, where the first window takes up half the screen, and the /// remaining windows are stacked vertically on the other half. + /// + /// + /// + /// ---------------------------------------------------------------- + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | Primary | Overflow | + /// | 1 window | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// | | | + /// ---------------------------------------------------------------- + /// + /// /// /// ///