forked from dalyIsaac/Whim
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTreeLayoutEngine.cs
687 lines (589 loc) · 19.5 KB
/
TreeLayoutEngine.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using WindowPathDict = System.Collections.Immutable.ImmutableDictionary<
Whim.IWindow,
System.Collections.Immutable.ImmutableArray<int>
>;
namespace Whim.TreeLayout;
internal record NonRootWindowData(
ISplitNode RootSplitNode,
WindowNode WindowNode,
IReadOnlyList<ISplitNode> WindowAncestors,
ImmutableArray<int> WindowPath,
IRectangle<double> WindowRectangle
);
/// <summary>
/// A tree layout engine allows users to create arbitrary window grid layouts.
/// </summary>
public record TreeLayoutEngine : ILayoutEngine
{
private readonly IContext _context;
private readonly ITreeLayoutPlugin _plugin;
private readonly INode? _root;
/// <summary>
/// Map of windows to their paths in the tree.
/// </summary>
private readonly WindowPathDict _windows;
/// <inheritdoc/>
public string Name { get; init; } = "Tree";
/// <inheritdoc/>
public int Count => _windows.Count;
/// <inheritdoc/>
public LayoutEngineIdentity Identity { get; }
private TreeLayoutEngine(TreeLayoutEngine engine, INode root, WindowPathDict windows)
: this(engine._context, engine._plugin, engine.Identity)
{
Name = engine.Name;
_root = root;
_windows = windows;
}
/// <summary>
/// Creates a new instance of the tree layout engine.
/// </summary>
/// <param name="context"></param>
/// <param name="plugin"></param>
/// <param name="identity"></param>
public TreeLayoutEngine(IContext context, ITreeLayoutPlugin plugin, LayoutEngineIdentity identity)
{
_context = context;
_plugin = plugin;
Identity = identity;
_root = null;
_windows = WindowPathDict.Empty;
}
/// <summary>
/// Creates a new <see cref="TreeLayoutEngine"/>, replacing the old node from
/// <paramref name="oldNodePath"/> with <paramref name="newNode"/>.
/// </summary>
/// <param name="oldNodeAncestors">The ancestors of the old node.</param>
/// <param name="oldNodePath">The path to the old node.</param>
/// <param name="windows">The map of windows to their paths.</param>
/// <param name="newNode">The new node to replace the old node.</param>
/// <returns></returns>
private TreeLayoutEngine CreateNewEngine(
IReadOnlyList<ISplitNode> oldNodeAncestors,
ImmutableArray<int> oldNodePath,
WindowPathDict windows,
INode newNode
)
{
// Rebuild the tree from the bottom up.
INode current = newNode;
for (int idx = oldNodePath.Length - 1; idx >= 0; idx--)
{
ISplitNode parent = oldNodeAncestors[idx];
ISplitNode newParent = parent.Replace(oldNodePath[idx], current);
current = newParent;
}
return new TreeLayoutEngine(
this,
current,
TreeHelpers.CreateUpdatedPaths(windows, oldNodePath, (ISplitNode)current)
);
}
/// <summary>
/// Creates a new dictionary. It contains only <code>{ newWindow: [0]; }</code>.
/// </summary>
/// <param name="window"></param>
/// <returns></returns>
private static WindowPathDict CreateRootNodeDict(IWindow window) =>
ImmutableDictionary.Create<IWindow, ImmutableArray<int>>().Add(window, ImmutableArray.Create(0));
/// <summary>
/// Creates a new dictionary with the top-level split node.
/// </summary>
/// <param name="splitNode"></param>
/// <returns></returns>
private static WindowPathDict CreateTopSplitNodeDict(ISplitNode splitNode)
{
WindowPathDict.Builder dictBuilder = ImmutableDictionary.CreateBuilder<IWindow, ImmutableArray<int>>();
int idx = 0;
foreach (INode child in splitNode.Children)
{
dictBuilder.Add(((WindowNode)child).Window, ImmutableArray.Create(idx));
idx++;
}
return dictBuilder.ToImmutable();
}
/// <inheritdoc/>
public ILayoutEngine AddWindow(IWindow window)
{
Logger.Debug($"Adding window {window} to layout engine {Name}");
if (_windows.ContainsKey(window))
{
Logger.Error($"Window {window} already exists in layout engine {Name}");
return this;
}
WindowNode newWindowNode = new(window);
switch (_root)
{
case WindowNode rootNode:
Logger.Debug($"Root is window node, replacing with split node");
ISplitNode newRoot = new SplitNode(rootNode, newWindowNode, _plugin.GetAddWindowDirection(this));
return new TreeLayoutEngine(this, newRoot, CreateTopSplitNodeDict(newRoot));
case ISplitNode rootNode:
return AddToSplitNode(newWindowNode, rootNode);
default:
Logger.Debug($"Root is null, creating new window node");
return new TreeLayoutEngine(this, newWindowNode, CreateRootNodeDict(window));
}
}
private ILayoutEngine AddToSplitNode(WindowNode newWindowNode, ISplitNode rootNode)
{
Logger.Debug($"Root is split node, adding new window node");
WindowNode? focusedNode;
IReadOnlyList<ISplitNode> ancestors;
ImmutableArray<int> path;
// Try get the focused window, and use its parent. Otherwise, use the right-most window.
if (
_context.WorkspaceManager.ActiveWorkspace.LastFocusedWindow is IWindow focusedWindow
&& _windows.TryGetValue(focusedWindow, out ImmutableArray<int> focusedWindowPath)
&& rootNode.GetNodeAtPath(focusedWindowPath) is NodeState nodeState
&& nodeState.Node is WindowNode focusedWindowNode
)
{
focusedNode = focusedWindowNode;
ancestors = nodeState.Ancestors;
path = focusedWindowPath;
}
else
{
(focusedNode, ancestors, path) = rootNode.GetRightMostWindow();
}
// We're assuming that there is a parent node - otherwise we wouldn't have reached this point.
ISplitNode parentNode = ancestors[^1];
ISplitNode newParent;
Direction addNodeDirection = _plugin.GetAddWindowDirection(this);
if (parentNode.IsHorizontal == addNodeDirection.IsHorizontal())
{
newParent = parentNode.Add(focusedNode, newWindowNode, addNodeDirection.InsertAfter());
}
else
{
SplitNode newChild = new(focusedNode, newWindowNode, addNodeDirection);
newParent = parentNode.Replace(path[^1], newChild);
}
return CreateNewEngine(ancestors, path.RemoveAt(path.Length - 1), _windows, newParent);
}
/// <inheritdoc />
public ILayoutEngine MoveWindowToPoint(IWindow window, IPoint<double> point)
{
Logger.Debug($"Adding window {window} to layout engine {Name}");
TreeLayoutEngine intermediateTree = (TreeLayoutEngine)RemoveWindow(window);
return intermediateTree.AddWindowAtPoint(window, point);
}
private ILayoutEngine AddWindowAtPoint(IWindow window, IPoint<double> point)
{
WindowNode newWindowNode = new(window);
if (_root is WindowNode focusedWindowNode)
{
Logger.Debug($"Root is window node, replacing with split node");
Direction newNodeDirection = Rectangle.UnitSquare<double>().GetDirectionToPoint(point);
ISplitNode newRoot = new SplitNode(focusedWindowNode, newWindowNode, newNodeDirection);
return new TreeLayoutEngine(this, newRoot, CreateTopSplitNodeDict(newRoot));
}
if (_root is ISplitNode splitNode)
{
return MoveWindowToPointSplitNode(point, newWindowNode, splitNode);
}
Logger.Debug($"Root is null, creating new window node");
return new TreeLayoutEngine(this, newWindowNode, CreateRootNodeDict(window));
}
private ILayoutEngine MoveWindowToPointSplitNode(
IPoint<double> point,
WindowNode newWindowNode,
ISplitNode rootNode
)
{
WindowNodeStateAtPoint? result = rootNode.GetNodeContainingPoint(point);
if (result is null)
{
Logger.Error($"Failed to find node containing point {point}");
return this;
}
(WindowNode focusedNode, ImmutableArray<ISplitNode> ancestors, ImmutableArray<int> path, Direction direction) =
result;
ISplitNode parentNode = ancestors[^1];
if (parentNode.IsHorizontal == direction.IsHorizontal())
{
// Add the node to the parent.
ISplitNode newParent = parentNode.Add(focusedNode, newWindowNode, direction.InsertAfter());
return CreateNewEngine(
oldNodeAncestors: ancestors,
oldNodePath: path.RemoveAt(path.Length - 1),
_windows,
newNode: newParent
);
}
// Replace the current node with a split node.
ISplitNode windowNodeReplacement = new SplitNode(focusedNode, newWindowNode, direction);
ISplitNode newParentNode = parentNode.Replace(path[^1], windowNodeReplacement);
return CreateNewEngine(
oldNodeAncestors: ancestors,
oldNodePath: path.RemoveAt(path.Length - 1),
_windows,
newNode: newParentNode
);
}
/// <inheritdoc />
public bool ContainsWindow(IWindow window)
{
Logger.Debug($"Checking if layout engine {Name} contains window {window}");
return _windows.ContainsKey(window);
}
/// <inheritdoc />
public IEnumerable<IWindowState> DoLayout(IRectangle<int> rectangle, IMonitor monitor)
{
Logger.Debug($"Doing layout for engine {Name}");
if (_root == null)
{
yield break;
}
foreach (WindowNodeRectangleState item in _root.GetWindowRectangles(rectangle))
{
yield return new WindowState()
{
Window = item.WindowNode.Window,
Rectangle = item.Rectangle,
WindowSize = item.WindowSize
};
}
}
/// <inheritdoc />
public void FocusWindowInDirection(Direction direction, IWindow window)
{
Logger.Debug($"Focusing window {window} in direction {direction}");
if (_root is null)
{
Logger.Error($"Root is null, cannot focus window {window}");
return;
}
if (!_windows.TryGetValue(window, out ImmutableArray<int> path))
{
Logger.Error($"Window {window} not found in layout engine {Name}");
return;
}
WindowNodeStateAtPoint? adjacentNodeResult = TreeHelpers.GetAdjacentWindowNode(
_root,
path,
direction,
_context.MonitorManager.ActiveMonitor
);
adjacentNodeResult?.WindowNode.Focus();
}
/// <inheritdoc />
public IWindow? GetFirstWindow()
{
Logger.Debug($"Getting first window in layout engine {Name}");
return _root switch
{
WindowNode windowNode => windowNode.Window,
ISplitNode ISplitNode => ISplitNode.GetLeftMostWindow().WindowNode.Window,
_ => null
};
}
/// <summary>
/// Gets the data for a non-root window.
/// </summary>
/// <param name="window"></param>
/// <returns></returns>
private NonRootWindowData? GetNonRootWindowData(IWindow window)
{
if (_root is not ISplitNode rootSplitNode)
{
Logger.Error($"Root is not split node, cannot get window data");
return null;
}
if (
!_windows.TryGetValue(window, out ImmutableArray<int> windowPath)
|| rootSplitNode.GetNodeAtPath(windowPath) is not NodeState windowResult
)
{
Logger.Error($"Window {window} not found in layout engine");
return null;
}
return new NonRootWindowData(
rootSplitNode,
(WindowNode)windowResult.Node,
windowResult.Ancestors,
windowPath,
windowResult.Rectangle
);
}
/// <inheritdoc />
public ILayoutEngine MoveWindowEdgesInDirection(Direction edges, IPoint<double> deltas, IWindow focusedWindow)
{
Logger.Debug($"Moving window edges {edges} in direction {deltas} for window {focusedWindow}");
if (GetNonRootWindowData(focusedWindow) is not NonRootWindowData windowData)
{
return this;
}
(
ISplitNode rootSplitNode,
INode _,
IReadOnlyList<ISplitNode> windowAncestors,
ImmutableArray<int> windowPath,
IRectangle<double> windowRectangle
) = windowData;
IMonitor monitor = _context.MonitorManager.ActiveMonitor;
WindowNodeStateAtPoint? xAdjacentResult = null;
WindowNodeStateAtPoint? yAdjacentResult = null;
// We need to adjust the deltas, because MoveSingleWindowEdgeInDirection works by moving the
// edge in the direction when the delta is positive.
// For example, if we want to move the left edge to the left, we need to have a positive
// X value for MoveSingleWindowEdgeInDirection, but a negative X value for the call to
// MoveWindowEdgesInDirection.
Point<double> directionAdjustedDeltas = new(deltas);
if (edges.HasFlag(Direction.Left))
{
xAdjacentResult = TreeHelpers.GetAdjacentWindowNode(
windowData.RootSplitNode,
Direction.Left,
monitor,
windowRectangle
);
directionAdjustedDeltas.X = -directionAdjustedDeltas.X;
}
else if (edges.HasFlag(Direction.Right))
{
xAdjacentResult = TreeHelpers.GetAdjacentWindowNode(
rootSplitNode,
Direction.Right,
monitor,
windowRectangle
);
}
if (edges.HasFlag(Direction.Up))
{
yAdjacentResult = TreeHelpers.GetAdjacentWindowNode(rootSplitNode, Direction.Up, monitor, windowRectangle);
directionAdjustedDeltas.Y = -directionAdjustedDeltas.Y;
}
else if (edges.HasFlag(Direction.Down))
{
yAdjacentResult = TreeHelpers.GetAdjacentWindowNode(
rootSplitNode,
Direction.Down,
monitor,
windowRectangle
);
}
if (xAdjacentResult == null && yAdjacentResult == null)
{
Logger.Error($"Could not find adjacent node for focused window in layout engine {Name}");
return this;
}
// For each adjacent node, move the window edge in the given direction.
TreeLayoutEngine newEngine = this;
if (xAdjacentResult != null)
{
newEngine = MoveSingleWindowEdgeInDirection(
rootSplitNode,
directionAdjustedDeltas.X,
true,
windowAncestors,
windowPath,
xAdjacentResult.Ancestors,
xAdjacentResult.Path
);
rootSplitNode = (ISplitNode)newEngine._root!;
}
if (yAdjacentResult != null)
{
newEngine = MoveSingleWindowEdgeInDirection(
rootSplitNode,
directionAdjustedDeltas.Y,
false,
windowAncestors,
windowPath,
yAdjacentResult.Ancestors,
yAdjacentResult.Path
);
}
return newEngine;
}
private TreeLayoutEngine MoveSingleWindowEdgeInDirection(
ISplitNode root,
double delta,
bool isXAxis,
IReadOnlyList<ISplitNode> focusedNodeAncestors,
ImmutableArray<int> focusedNodePath,
IReadOnlyList<ISplitNode> adjacentNodeAncestors,
ImmutableArray<int> adjacentNodePath
)
{
// Get the index of the last common ancestor.
int parentDepth = TreeHelpers.GetLastCommonAncestorIndex(focusedNodeAncestors, adjacentNodeAncestors);
if (parentDepth == -1)
{
Logger.Error($"Failed to find common parent for focused window and adjacent node");
return this;
}
// Adjust the weight of the focused node.
// First, we need to find the rectangle of the parent node.
ImmutableArray<int> parentNodePath = focusedNodePath.Take(parentDepth).ToImmutableArray();
NodeState? parentResult = root.GetNodeAtPath(parentNodePath);
if (parentResult is null || parentResult.Node is not ISplitNode parentSplitNode)
{
Logger.Error($"Failed to find parent node for focused window");
return this;
}
// Figure out what the relative delta of pixelsDeltas is given the unit square.
double relativeDelta = delta / (isXAxis ? parentResult.Rectangle.Width : parentResult.Rectangle.Height);
// Now we can adjust the weights.
ISplitNode focusedNodeParent = parentSplitNode.AdjustChildWeight(focusedNodePath[parentDepth], relativeDelta);
if (focusedNodeParent == parentSplitNode)
{
Logger.Error($"Failed to adjust child weight for focused window");
return this;
}
ISplitNode newParent = focusedNodeParent.AdjustChildWeight(adjacentNodePath[parentDepth], -relativeDelta);
if (newParent == focusedNodeParent)
{
Logger.Error($"Failed to adjust child weight for adjacent node");
return this;
}
return CreateNewEngine(parentResult.Ancestors, parentNodePath, _windows, newParent);
}
/// <inheritdoc />
public ILayoutEngine RemoveWindow(IWindow window)
{
Logger.Debug($"Removing window {window} from layout engine {Name}");
if (_root is null)
{
Logger.Error($"Root is null, cannot remove window {window} from layout engine {Name}");
return this;
}
if (_root is WindowNode windowNode)
{
if (windowNode.Window.Equals(window))
{
return new TreeLayoutEngine(_context, _plugin, Identity);
}
else
{
Logger.Error($"Root is window node, but window {window} is not the root");
return this;
}
}
if (
!_windows.TryGetValue(window, out ImmutableArray<int> windowPath)
|| _root.GetNodeAtPath(windowPath) is not NodeState windowResult
)
{
Logger.Error($"Window {window} not found in layout engine {Name}");
return this;
}
ISplitNode parentNode = windowResult.Ancestors[^1];
WindowPathDict windows = _windows.Remove(window);
// If the parent node has more than two children, just remove the window.
if (parentNode.Children.Count != 2)
{
ISplitNode newParentNode = parentNode.Remove(windowPath[^1]);
return CreateNewEngine(windowResult.Ancestors, windowPath[..^1], windows, newParentNode);
}
// If the parent node has just two children, remove the parent node and replace it with the other child.
INode otherChild =
parentNode.Children[0] == windowResult.Node ? parentNode.Children[1] : parentNode.Children[0];
if (parentNode == _root && otherChild is WindowNode otherWindowChild)
{
return new TreeLayoutEngine(this, otherChild, CreateRootNodeDict(otherWindowChild.Window));
}
return CreateNewEngine(windowResult.Ancestors, windowPath[..^1], windows, otherChild);
}
/// <inheritdoc />
public ILayoutEngine SwapWindowInDirection(Direction direction, IWindow window)
{
Logger.Debug($"Swapping window {window} in direction {direction} in layout engine {Name}");
if (GetNonRootWindowData(window) is not NonRootWindowData windowData)
{
Logger.Error($"Window {window} not found in layout engine {Name}");
return this;
}
WindowNodeStateAtPoint? adjacentResult = TreeHelpers.GetAdjacentWindowNode(
windowData.RootSplitNode,
direction,
_context.MonitorManager.ActiveMonitor,
windowData.WindowRectangle
);
if (adjacentResult is null)
{
Logger.Error($"Could not find adjacent node for focused window in layout engine {Name}");
return this;
}
// Get the parents of each node.
ISplitNode windowParent = windowData.WindowAncestors[^1];
ISplitNode adjacentParent = adjacentResult.Ancestors[^1];
// If the parents are the same, we can just swap the windows.
if (windowParent == adjacentParent)
{
ISplitNode newParent = windowParent.Swap(windowData.WindowPath[^1], adjacentResult.Path[^1]);
return CreateNewEngine(windowData.WindowAncestors, windowData.WindowPath[..^1], _windows, newParent);
}
// If the parents are different, we need to swap the window nodes.
return SwapAdjacentNodes(
windowData.WindowPath,
windowData.WindowAncestors,
windowData.WindowNode,
adjacentResult.Path,
adjacentResult.Ancestors,
adjacentResult.WindowNode
);
}
private TreeLayoutEngine SwapAdjacentNodes(
ImmutableArray<int> aPath,
IReadOnlyList<ISplitNode> aNodeAncestors,
WindowNode aNode,
ImmutableArray<int> bPath,
IReadOnlyList<ISplitNode> bNodeAncestors,
WindowNode bNode
)
{
// A's path should be longer than B's path.
if (aPath.Length < bPath.Length)
{
(aPath, aNode, aNodeAncestors, bPath, bNode, _) = (
bPath,
bNode,
bNodeAncestors,
aPath,
aNode,
aNodeAncestors
);
}
// Rebuild the tree from the bottom up for the new position for B.
INode currentNode = bNode;
ISplitNode? commonAncestor = null;
int commonAncestorIdx = 0;
for (int idx = aPath.Length - 1; idx >= 0; idx--)
{
ISplitNode parent = aNodeAncestors[idx];
ISplitNode newParent = parent.Replace(aPath[idx], currentNode);
if (idx < bPath.Length && bNodeAncestors[idx] == parent)
{
commonAncestor = newParent;
commonAncestorIdx = idx;
break;
}
currentNode = newParent;
}
if (commonAncestor is null)
{
Logger.Error($"Failed to find common ancestor for nodes {aNode} and {bNode}");
return this;
}
// Rebuild the tree from the bottom up for the new position for A.
currentNode = aNode;
for (int idx = bPath.Length - 1; idx >= 0; idx--)
{
ISplitNode parent = idx == commonAncestorIdx ? commonAncestor : bNodeAncestors[idx];
ISplitNode newParent = parent.Replace(bPath[idx], currentNode);
currentNode = newParent;
}
WindowPathDict newWindows = TreeHelpers.CreateUpdatedPaths(
_windows,
aPath[..commonAncestorIdx],
(ISplitNode)currentNode
);
return new TreeLayoutEngine(this, currentNode, newWindows);
}
}