@@ -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,26 @@ 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, or `None` if insertion failed.
224+ pub fn split_panel_group ( & mut self , target_group_id : PanelGroupId , direction : DockingSplitDirection , tabs : Vec < PanelType > , active_tab_index : usize ) -> Option < 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 } ,
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+ self . root . insert_split_adjacent ( target_group_id, new_group, insert_before, needs_horizontal, 0 ) . then_some ( new_id)
238+ }
239+
210240 /// Recalculate the default sizes for all splits in the tree based on document panel proximity.
211241 pub fn recalculate_default_sizes ( & mut self ) {
212242 self . root . recalculate_default_sizes ( ) ;
@@ -409,23 +439,78 @@ impl PanelLayoutSubdivision {
409439 }
410440 }
411441
412- /// Remove empty panel groups and collapse single-child splits.
442+ /// Remove empty panel groups and collapse unnecessary nesting.
443+ /// Does NOT collapse single-child splits into their child, as that would change subdivision depths
444+ /// and break the direction-by-depth alternation system.
413445 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 ( ) ) ;
446+ let PanelLayoutSubdivision :: Split { children } = self else { return } ;
417447
418- // Remove empty panel groups
419- children. retain ( |child| ! matches ! ( & child. subdivision, PanelLayoutSubdivision :: PanelGroup { state , .. } if state . tabs . is_empty ( ) ) ) ;
448+ // Recursively prune children
449+ children. iter_mut ( ) . for_each ( |child| child. subdivision . prune ( ) ) ;
420450
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( ) ) ) ;
451+ // Remove empty panel groups
452+ children. retain ( |child| !matches ! ( & child. subdivision, PanelLayoutSubdivision :: PanelGroup { state , .. } if state . tabs . is_empty( ) ) ) ;
423453
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 ;
454+ // Remove empty splits (splits that lost all their children after pruning)
455+ children. retain ( |child| !matches ! ( & child. subdivision, PanelLayoutSubdivision :: Split { children } if children. is_empty( ) ) ) ;
456+ }
457+
458+ /// Check if this subtree contains a panel group with the given ID.
459+ pub fn contains_group ( & self , target_id : PanelGroupId ) -> bool {
460+ match self {
461+ PanelLayoutSubdivision :: PanelGroup { id, .. } => * id == target_id,
462+ PanelLayoutSubdivision :: Split { children } => children. iter ( ) . any ( |child| child. subdivision . contains_group ( target_id) ) ,
463+ }
464+ }
465+
466+ /// Inserts a new split child adjacent to a target panel group and returns whether the insertion was successful.
467+ /// Recurses to the deepest split closest to the target that matches the requested split direction.
468+ /// If the target is a direct child of a mismatched-direction split, this wraps it in a new sub-split.
469+ pub fn insert_split_adjacent ( & mut self , target_id : PanelGroupId , new_child : SplitChild , insert_before : bool , needs_horizontal : bool , depth : usize ) -> bool {
470+ let PanelLayoutSubdivision :: Split { children } = self else { return false } ;
471+
472+ let is_horizontal = depth. is_multiple_of ( 2 ) ;
473+ let direction_matches = is_horizontal == needs_horizontal;
474+
475+ // Find which child subtree contains the target
476+ let Some ( containing_index) = children. iter ( ) . position ( |child| child. subdivision . contains_group ( target_id) ) else {
477+ return false ;
478+ } ;
479+
480+ // 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)
481+ let target_is_direct_child = matches ! ( & children[ containing_index] . subdivision, PanelLayoutSubdivision :: PanelGroup { id, .. } if * id == target_id) ;
482+ if target_is_direct_child {
483+ // Direction matches and target is right here: insert as a sibling
484+ if direction_matches {
485+ let insert_index = if insert_before { containing_index } else { containing_index + 1 } ;
486+ children. insert ( insert_index, new_child) ;
427487 }
488+ // 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)
489+ else {
490+ let old_child_subdivision = std:: mem:: replace ( & mut children[ containing_index] . subdivision , PanelLayoutSubdivision :: Split { children : vec ! [ ] } ) ;
491+ let old_child = SplitChild {
492+ subdivision : old_child_subdivision,
493+ size : 50. ,
494+ } ;
495+
496+ if let PanelLayoutSubdivision :: Split { children : sub_children } = & mut children[ containing_index] . subdivision {
497+ if insert_before {
498+ sub_children. push ( new_child) ;
499+ sub_children. push ( old_child) ;
500+ } else {
501+ sub_children. push ( old_child) ;
502+ sub_children. push ( new_child) ;
503+ }
504+ }
505+ }
506+
507+ return true ;
428508 }
509+
510+ // The target is deeper, so recurse into the containing child's subtree and return its insertion outcome
511+ children[ containing_index]
512+ . subdivision
513+ . insert_split_adjacent ( target_id, new_child. clone ( ) , insert_before, needs_horizontal, depth + 1 )
429514 }
430515
431516 /// Check if this subtree contains the document panel.
0 commit comments