@@ -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+ */
3240export 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