@@ -112,6 +112,16 @@ impl From<String> for PanelType {
112112#[ derive( Clone , Copy , Debug , Default , PartialEq , Eq , Hash , serde:: Serialize , serde:: Deserialize ) ]
113113pub struct PanelGroupId ( pub u64 ) ;
114114
115+ /// Which edge of a panel group to split on when docking a dragged panel.
116+ #[ cfg_attr( feature = "wasm" , derive( tsify:: Tsify ) ) ]
117+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
118+ pub enum DockingSplitDirection {
119+ Left ,
120+ Right ,
121+ Top ,
122+ Bottom ,
123+ }
124+
115125/// State of a single panel group (leaf subdivision) in the workspace layout tree.
116126#[ cfg_attr( feature = "wasm" , derive( tsify:: Tsify ) , tsify( large_number_types_as_bigints) ) ]
117127#[ derive( Clone , Debug , Default , PartialEq , serde:: Serialize , serde:: Deserialize ) ]
@@ -207,6 +217,30 @@ impl WorkspacePanelLayout {
207217 self . root . prune ( ) ;
208218 }
209219
220+ /// Split a panel group by inserting a new panel group adjacent to it.
221+ /// The direction determines where the new group goes relative to the target.
222+ /// Left/Right creates a horizontal (row) split, Top/Bottom creates a vertical (column) split.
223+ /// Returns the ID of the newly created panel group.
224+ pub fn split_panel_group ( & mut self , target_group_id : PanelGroupId , direction : DockingSplitDirection , tabs : Vec < PanelType > ) -> PanelGroupId {
225+ let new_id = self . next_id ( ) ;
226+ let new_group = SplitChild {
227+ subdivision : PanelLayoutSubdivision :: PanelGroup {
228+ id : new_id,
229+ state : PanelGroupState { tabs, active_tab_index : 0 } ,
230+ } ,
231+ size : 50. ,
232+ } ;
233+
234+ let insert_before = matches ! ( direction, DockingSplitDirection :: Left | DockingSplitDirection :: Top ) ;
235+ let needs_horizontal = matches ! ( direction, DockingSplitDirection :: Left | DockingSplitDirection :: Right ) ;
236+
237+ if !self . root . insert_split_adjacent ( target_group_id, new_group, insert_before, needs_horizontal, 0 ) {
238+ log:: error!( "Failed to insert split adjacent to panel group {target_group_id:?}: no matching-direction ancestor found" ) ;
239+ }
240+
241+ new_id
242+ }
243+
210244 /// Recalculate the default sizes for all splits in the tree based on document panel proximity.
211245 pub fn recalculate_default_sizes ( & mut self ) {
212246 self . root . recalculate_default_sizes ( ) ;
@@ -409,23 +443,78 @@ impl PanelLayoutSubdivision {
409443 }
410444 }
411445
412- /// Remove empty panel groups and collapse single-child splits.
446+ /// Remove empty panel groups and collapse unnecessary nesting.
447+ /// Does NOT collapse single-child splits into their child, as that would change subdivision depths
448+ /// and break the direction-by-depth alternation system.
413449 pub fn prune ( & mut self ) {
414- if let PanelLayoutSubdivision :: Split { children } = self {
415- // Recursively prune children first
416- children. iter_mut ( ) . for_each ( |child| child. subdivision . prune ( ) ) ;
450+ let PanelLayoutSubdivision :: Split { children } = self else { return } ;
417451
418- // Remove empty panel groups
419- children. retain ( |child| ! matches ! ( & child. subdivision, PanelLayoutSubdivision :: PanelGroup { state , .. } if state . tabs . is_empty ( ) ) ) ;
452+ // Recursively prune children
453+ children. iter_mut ( ) . for_each ( |child| child. subdivision . prune ( ) ) ;
420454
421- // Remove empty splits (splits that lost all their children after pruning)
422- children. retain ( |child| !matches ! ( & child. subdivision, PanelLayoutSubdivision :: Split { children } if children . is_empty( ) ) ) ;
455+ // Remove empty panel groups
456+ children. retain ( |child| !matches ! ( & child. subdivision, PanelLayoutSubdivision :: PanelGroup { state , .. } if state . tabs . is_empty( ) ) ) ;
423457
424- // If a split has exactly one child, replace this subdivision with that child's subdivision
425- if children. len ( ) == 1 {
426- * self = children. remove ( 0 ) . subdivision ;
458+ // Remove empty splits (splits that lost all their children after pruning)
459+ children. retain ( |child| !matches ! ( & child. subdivision, PanelLayoutSubdivision :: Split { children } if children. is_empty( ) ) ) ;
460+ }
461+
462+ /// Check if this subtree contains a panel group with the given ID.
463+ pub fn contains_group ( & self , target_id : PanelGroupId ) -> bool {
464+ match self {
465+ PanelLayoutSubdivision :: PanelGroup { id, .. } => * id == target_id,
466+ PanelLayoutSubdivision :: Split { children } => children. iter ( ) . any ( |child| child. subdivision . contains_group ( target_id) ) ,
467+ }
468+ }
469+
470+ /// Inserts a new split child adjacent to a target panel group and returns whether the insertion was successful.
471+ /// Recurses to the deepest split closest to the target that matches the requested split direction.
472+ /// If the target is a direct child of a mismatched-direction split, this wraps it in a new sub-split.
473+ pub fn insert_split_adjacent ( & mut self , target_id : PanelGroupId , new_child : SplitChild , insert_before : bool , needs_horizontal : bool , depth : usize ) -> bool {
474+ let PanelLayoutSubdivision :: Split { children } = self else { return false } ;
475+
476+ let is_horizontal = depth. is_multiple_of ( 2 ) ;
477+ let direction_matches = is_horizontal == needs_horizontal;
478+
479+ // Find which child subtree contains the target
480+ let Some ( containing_index) = children. iter ( ) . position ( |child| child. subdivision . contains_group ( target_id) ) else {
481+ return false ;
482+ } ;
483+
484+ // If the target is a direct child: we can certainly insert the new split, either as a sibling (if direction matches) or wrapping the target in a new split (if direction is mismatched)
485+ let target_is_direct_child = matches ! ( & children[ containing_index] . subdivision, PanelLayoutSubdivision :: PanelGroup { id, .. } if * id == target_id) ;
486+ if target_is_direct_child {
487+ // Direction matches and target is right here: insert as a sibling
488+ if direction_matches {
489+ let insert_index = if insert_before { containing_index } else { containing_index + 1 } ;
490+ children. insert ( insert_index, new_child) ;
427491 }
492+ // Direction mismatch: wrap the target in a new sub-split (at depth+1, which has the opposite direction of this and thus is the requested direction)
493+ else {
494+ let old_child_subdivision = std:: mem:: replace ( & mut children[ containing_index] . subdivision , PanelLayoutSubdivision :: Split { children : vec ! [ ] } ) ;
495+ let old_child = SplitChild {
496+ subdivision : old_child_subdivision,
497+ size : 50. ,
498+ } ;
499+
500+ if let PanelLayoutSubdivision :: Split { children : sub_children } = & mut children[ containing_index] . subdivision {
501+ if insert_before {
502+ sub_children. push ( new_child) ;
503+ sub_children. push ( old_child) ;
504+ } else {
505+ sub_children. push ( old_child) ;
506+ sub_children. push ( new_child) ;
507+ }
508+ }
509+ }
510+
511+ return true ;
428512 }
513+
514+ // The target is deeper, so recurse into the containing child's subtree and return its insertion outcome
515+ children[ containing_index]
516+ . subdivision
517+ . insert_split_adjacent ( target_id, new_child. clone ( ) , insert_before, needs_horizontal, depth + 1 )
429518 }
430519
431520 /// Check if this subtree contains the document panel.
0 commit comments