Skip to content

Commit 778773d

Browse files
author
Oliver Rose
committed
chore: comment split layout
1 parent f2f8e0f commit 778773d

File tree

1 file changed

+54
-0
lines changed

1 file changed

+54
-0
lines changed

src/lib/split-layout.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,29 @@ interface SplitRect {
2929
height: number;
3030
}
3131

32+
/**
33+
* Split layout is implemented as a binary tree where each node is either a
34+
* {@linkcode SplitBranch} or a leaf representing a pane ID.
35+
*
36+
* See also:
37+
* - https://www.warp.dev/blog/using-tree-data-structures-to-implement-terminal-split-panes-more-fun-than-it-sounds
38+
* - https://github.com/ghostty-org/ghostty/blob/main/macos/Sources/Features/Splits/SplitTree.swift
39+
*/
3240
export class SplitLayout {
3341
public static readonly EMPTY_ROOT_ID = "split-root-empty";
3442

3543
#focused: string | null = null;
3644

45+
/**
46+
* Whether the split layout is currently active.
47+
*/
3748
public get active() {
3849
return page.route.id === "/(main)/channels/split";
3950
}
4051

52+
/**
53+
* The root of the layout tree.
54+
*/
4155
public get root() {
4256
return layout.state.root;
4357
}
@@ -58,6 +72,10 @@ export class SplitLayout {
5872
this.#focused = value;
5973
}
6074

75+
/**
76+
* Splits an existing leaf node into a branch containing the original node
77+
* and a new node.
78+
*/
6179
public insert(target: string, newNode: string, branch: SplitBranch) {
6280
if (!this.root) {
6381
this.root = target;
@@ -90,14 +108,20 @@ export class SplitLayout {
90108
this.focused = id;
91109
}
92110

111+
/**
112+
* Removes the target pane and collapses the tree.
113+
*/
93114
public remove(target: string) {
94115
if (!this.root) return;
95116

117+
// target is the entire tree
96118
if (this.root === target) {
97119
this.root = null;
98120
return;
99121
}
100122

123+
// target is an immediate child of the root, so the root gets replaced
124+
// entirely by its surviving child.
101125
if (typeof this.root !== "string") {
102126
if (this.root.before === target) {
103127
this.root = this.root.after;
@@ -119,6 +143,24 @@ export class SplitLayout {
119143
this.#update(target, () => replacement);
120144
}
121145

146+
/**
147+
* Spatially navigates the layout using geometrical projection by
148+
* calculating synthetic bounding boxes for every pane and performing a
149+
* directional 2D search.
150+
*
151+
* Given the following layout and tree representation:
152+
*
153+
* ```txt
154+
* +-------+-------+ (H)
155+
* | | B | / \
156+
* | A |-------| A (V)
157+
* | | C | / \
158+
* +-------+-------+ B C
159+
* ```
160+
*
161+
* Going "right" from `A`, `getLayoutRects` determines that `B` and `C` are
162+
* candidates, but `B` is closer, so it gets picked.
163+
*/
122164
public navigate(startId: string, direction: SplitDirection) {
123165
if (!this.root || this.root === startId) return null;
124166

@@ -156,14 +198,18 @@ export class SplitLayout {
156198

157199
if (!candidates.length) return null;
158200

201+
// Score candidates to find the best visual neighbor
159202
const [best] = candidates.sort((a, b) => {
160203
const distA = this.#getDistance(current, a, direction);
161204
const distB = this.#getDistance(current, b, direction);
162205

206+
// Closest distance
163207
if (Math.abs(distA - distB) > threshold) {
164208
return distA - distB;
165209
}
166210

211+
// If distances are equal, pick the pane with the most overlapping
212+
// edge
167213
return (
168214
this.#getAlignmentScore(current, b, direction) -
169215
this.#getAlignmentScore(current, a, direction)
@@ -273,6 +319,11 @@ export class SplitLayout {
273319
};
274320
}
275321

322+
/**
323+
* Recursively maps the tree's percentage-based ratios into a synthetic 2D
324+
* layout. This normalizes all pane coordinates into an absolute space,
325+
* relative to a 1x1 area, to perform geometrical calculations.
326+
*/
276327
#getLayoutRects(
277328
node: SplitNode,
278329
{ x, y, width, height }: Omit<SplitRect, "id"> = { x: 0, y: 0, width: 1, height: 1 },
@@ -323,6 +374,9 @@ export class SplitLayout {
323374
}
324375
}
325376

377+
/**
378+
* Calculates how well-aligned two panes are on the orthogonal axis.
379+
*/
326380
#getAlignmentScore(from: SplitRect, to: SplitRect, direction: SplitDirection) {
327381
const isVerticalMove = direction === "up" || direction === "down";
328382

0 commit comments

Comments
 (0)