Skip to main content

fret_core/dock/
mutate.rs

1use super::*;
2
3impl DockGraph {
4    /// Simplify and canonicalize the docking forest for a window.
5    ///
6    /// Canonical form (v1):
7    ///
8    /// - `Tabs` nodes are non-empty (empty tabs are pruned; roots may be removed).
9    /// - `Split` nodes have `children.len() == fractions.len()`.
10    /// - `Split` fractions are finite, non-negative, and normalized (sum ~= 1.0).
11    /// - Single-child splits are pruned.
12    /// - Nested same-axis splits are flattened (bounded-depth property).
13    /// - `Floating` nodes keep their container identity, but their `child` is simplified.
14    pub(crate) fn simplify_window_forest(&mut self, window: AppWindowId) {
15        if let Some(root) = self.window_root(window) {
16            match self.simplify_subtree(root) {
17                Some(next_root) => self.set_window_root(window, next_root),
18                None => {
19                    let _ = self.remove_window_root(window);
20                }
21            }
22        }
23
24        let Some(mut floatings) = self.window_floatings.remove(&window) else {
25            return;
26        };
27
28        floatings.retain_mut(|w| match self.simplify_subtree(w.floating) {
29            Some(next_root) => {
30                w.floating = next_root;
31                true
32            }
33            None => false,
34        });
35
36        if !floatings.is_empty() {
37            self.window_floatings.insert(window, floatings);
38        }
39    }
40
41    fn simplify_subtree(&mut self, node: DockNodeId) -> Option<DockNodeId> {
42        let n = self.nodes.get(node)?.clone();
43        match n {
44            DockNode::Tabs { tabs, mut active } => {
45                if tabs.is_empty() {
46                    return None;
47                }
48                if active >= tabs.len() {
49                    active = tabs.len().saturating_sub(1);
50                }
51                if let Some(DockNode::Tabs {
52                    tabs: list,
53                    active: cur,
54                }) = self.nodes.get_mut(node)
55                {
56                    *list = tabs;
57                    *cur = active;
58                }
59                Some(node)
60            }
61            DockNode::Floating { child } => {
62                let child = self.simplify_subtree(child)?;
63                if let Some(DockNode::Floating { child: cur }) = self.nodes.get_mut(node) {
64                    *cur = child;
65                }
66                Some(node)
67            }
68            DockNode::Split {
69                axis,
70                children,
71                fractions,
72            } => {
73                let mut next_children: Vec<DockNodeId> = Vec::new();
74                let mut next_fractions: Vec<f32> = Vec::new();
75
76                // Repair mismatched lengths conservatively (treat missing fractions as 1.0 shares).
77                for (i, child) in children.into_iter().enumerate() {
78                    let Some(child) = self.simplify_subtree(child) else {
79                        continue;
80                    };
81                    let f = fractions.get(i).copied().unwrap_or(1.0);
82                    next_children.push(child);
83                    next_fractions.push(f);
84                }
85
86                if next_children.is_empty() {
87                    return None;
88                }
89                if next_children.len() == 1 {
90                    return Some(next_children[0]);
91                }
92
93                self.flatten_same_axis_splits(axis, &mut next_children, &mut next_fractions);
94
95                if next_children.is_empty() {
96                    return None;
97                }
98                if next_children.len() == 1 {
99                    return Some(next_children[0]);
100                }
101
102                normalize_shares(&mut next_fractions);
103                debug_assert_eq!(next_children.len(), next_fractions.len());
104
105                if let Some(DockNode::Split {
106                    children: cur_children,
107                    fractions: cur_fractions,
108                    ..
109                }) = self.nodes.get_mut(node)
110                {
111                    *cur_children = next_children;
112                    *cur_fractions = next_fractions;
113                }
114
115                Some(node)
116            }
117        }
118    }
119
120    fn flatten_same_axis_splits(
121        &mut self,
122        axis: Axis,
123        children: &mut Vec<DockNodeId>,
124        fractions: &mut Vec<f32>,
125    ) {
126        let mut changed = true;
127        while changed {
128            changed = false;
129
130            let mut out_children: Vec<DockNodeId> = Vec::with_capacity(children.len());
131            let mut out_fractions: Vec<f32> = Vec::with_capacity(fractions.len());
132
133            for (child, parent_share) in children.iter().copied().zip(fractions.iter().copied()) {
134                let Some(DockNode::Split {
135                    axis: child_axis,
136                    children: grand_children,
137                    fractions: grand_fractions,
138                }) = self.nodes.get(child)
139                else {
140                    out_children.push(child);
141                    out_fractions.push(parent_share);
142                    continue;
143                };
144
145                if *child_axis != axis {
146                    out_children.push(child);
147                    out_fractions.push(parent_share);
148                    continue;
149                }
150
151                // Flatten nested same-axis split by distributing the parent share across the grand-children.
152                changed = true;
153
154                let mut grand_shares = grand_fractions.clone();
155                normalize_shares(&mut grand_shares);
156                debug_assert_eq!(grand_children.len(), grand_shares.len());
157
158                for (&gc, &gs) in grand_children.iter().zip(grand_shares.iter()) {
159                    out_children.push(gc);
160                    out_fractions.push(parent_share * gs);
161                }
162            }
163
164            *children = out_children;
165            *fractions = out_fractions;
166        }
167    }
168
169    pub fn move_panel(
170        &mut self,
171        window: AppWindowId,
172        panel: PanelKey,
173        target_tabs: DockNodeId,
174        zone: DropZone,
175    ) -> bool {
176        self.move_panel_with_insert_index(window, panel, target_tabs, zone, None)
177    }
178
179    pub fn move_panel_with_insert_index(
180        &mut self,
181        window: AppWindowId,
182        panel: PanelKey,
183        target_tabs: DockNodeId,
184        zone: DropZone,
185        insert_index: Option<usize>,
186    ) -> bool {
187        self.move_panel_between_windows(window, panel, window, target_tabs, zone, insert_index)
188    }
189
190    pub fn move_panel_between_windows(
191        &mut self,
192        source_window: AppWindowId,
193        panel: PanelKey,
194        target_window: AppWindowId,
195        target_tabs: DockNodeId,
196        zone: DropZone,
197        insert_index: Option<usize>,
198    ) -> bool {
199        let Some((source_tabs, source_index)) = self.find_panel_in_window(source_window, &panel)
200        else {
201            return false;
202        };
203
204        if zone == DropZone::Center
205            && source_window == target_window
206            && source_tabs == target_tabs
207            && insert_index.is_none()
208        {
209            return true;
210        }
211
212        if !self.remove_panel_from_tabs(source_tabs, source_index) {
213            return false;
214        }
215
216        if zone == DropZone::Center {
217            let mut index = insert_index;
218            if source_window == target_window
219                && source_tabs == target_tabs
220                && let Some(i) = index.as_mut()
221                && *i > source_index
222            {
223                *i = i.saturating_sub(1);
224            }
225
226            let ok = self.insert_panel_into_tabs_at(target_tabs, panel, index);
227            self.simplify_window_forest(source_window);
228            if target_window != source_window {
229                self.simplify_window_forest(target_window);
230            }
231            return ok;
232        }
233
234        let axis = match zone {
235            DropZone::Left | DropZone::Right => Axis::Horizontal,
236            DropZone::Top | DropZone::Bottom => Axis::Vertical,
237            DropZone::Center => unreachable!(),
238        };
239
240        let new_tabs = self.insert_node(DockNode::Tabs {
241            tabs: vec![panel],
242            active: 0,
243        });
244
245        if self.insert_edge_child_prefer_same_axis_split(
246            target_window,
247            target_tabs,
248            axis,
249            zone,
250            new_tabs,
251        ) {
252            self.simplify_window_forest(source_window);
253            if target_window != source_window {
254                self.simplify_window_forest(target_window);
255            }
256            return true;
257        }
258
259        let (first, second) = match zone {
260            DropZone::Left | DropZone::Top => (new_tabs, target_tabs),
261            DropZone::Right | DropZone::Bottom => (target_tabs, new_tabs),
262            DropZone::Center => unreachable!(),
263        };
264
265        let split = self.insert_node(DockNode::Split {
266            axis,
267            children: vec![first, second],
268            fractions: vec![0.5, 0.5],
269        });
270
271        self.replace_node_in_window_tree(target_window, target_tabs, split);
272        self.simplify_window_forest(source_window);
273        if target_window != source_window {
274            self.simplify_window_forest(target_window);
275        }
276        true
277    }
278
279    pub fn move_tabs_between_windows(
280        &mut self,
281        source_window: AppWindowId,
282        source_tabs: DockNodeId,
283        target_window: AppWindowId,
284        target_tabs: DockNodeId,
285        zone: DropZone,
286        insert_index: Option<usize>,
287    ) -> bool {
288        if zone == DropZone::Center && source_window == target_window && source_tabs == target_tabs
289        {
290            return true;
291        }
292
293        if self
294            .root_for_node_in_window_forest(source_window, source_tabs)
295            .is_none()
296        {
297            return false;
298        }
299        if self
300            .root_for_node_in_window_forest(target_window, target_tabs)
301            .is_none()
302        {
303            return false;
304        }
305
306        let (panels, active) = match self.nodes.get(source_tabs) {
307            Some(DockNode::Tabs { tabs, active }) if !tabs.is_empty() => (tabs.clone(), *active),
308            _ => return false,
309        };
310        let active = active.min(panels.len().saturating_sub(1));
311
312        if zone == DropZone::Center
313            && !matches!(self.nodes.get(target_tabs), Some(DockNode::Tabs { .. }))
314        {
315            return false;
316        }
317
318        if let Some(DockNode::Tabs { tabs, active }) = self.nodes.get_mut(source_tabs) {
319            tabs.clear();
320            *active = 0;
321        }
322        if self.window_root(source_window) == Some(source_tabs) {
323            let _ = self.remove_window_root(source_window);
324        }
325        self.collapse_empty_tabs_upwards(source_window, source_tabs);
326        self.remove_empty_floating_windows(source_window);
327
328        if zone == DropZone::Center {
329            let ok = self.insert_panels_into_tabs_at(target_tabs, &panels, insert_index, active);
330            self.simplify_window_forest(target_window);
331            return ok;
332        }
333
334        let axis = match zone {
335            DropZone::Left | DropZone::Right => Axis::Horizontal,
336            DropZone::Top | DropZone::Bottom => Axis::Vertical,
337            DropZone::Center => unreachable!(),
338        };
339
340        let new_tabs = self.insert_node(DockNode::Tabs {
341            tabs: panels,
342            active,
343        });
344
345        if self.insert_edge_child_prefer_same_axis_split(
346            target_window,
347            target_tabs,
348            axis,
349            zone,
350            new_tabs,
351        ) {
352            self.simplify_window_forest(target_window);
353            return true;
354        }
355
356        let (first, second) = match zone {
357            DropZone::Left | DropZone::Top => (new_tabs, target_tabs),
358            DropZone::Right | DropZone::Bottom => (target_tabs, new_tabs),
359            DropZone::Center => unreachable!(),
360        };
361
362        let split = self.insert_node(DockNode::Split {
363            axis,
364            children: vec![first, second],
365            fractions: vec![0.5, 0.5],
366        });
367
368        self.replace_node_in_window_tree(target_window, target_tabs, split);
369        self.simplify_window_forest(target_window);
370        true
371    }
372
373    pub fn close_panel(&mut self, window: AppWindowId, panel: PanelKey) -> bool {
374        let Some((tabs, index)) = self.find_panel_in_window(window, &panel) else {
375            return false;
376        };
377        if !self.remove_panel_from_tabs(tabs, index) {
378            return false;
379        }
380        self.simplify_window_forest(window);
381        true
382    }
383
384    pub fn float_panel_to_window(
385        &mut self,
386        source_window: AppWindowId,
387        panel: PanelKey,
388        new_window: AppWindowId,
389    ) -> bool {
390        let Some((source_tabs, source_index)) = self.find_panel_in_window(source_window, &panel)
391        else {
392            return false;
393        };
394        if !self.remove_panel_from_tabs(source_tabs, source_index) {
395            return false;
396        }
397
398        let tabs = self.insert_node(DockNode::Tabs {
399            tabs: vec![panel],
400            active: 0,
401        });
402        self.set_window_root(new_window, tabs);
403        self.simplify_window_forest(source_window);
404        self.simplify_window_forest(new_window);
405        true
406    }
407
408    pub fn float_tabs_to_window(
409        &mut self,
410        source_window: AppWindowId,
411        source_tabs: DockNodeId,
412        new_window: AppWindowId,
413    ) -> bool {
414        if self
415            .root_for_node_in_window_forest(source_window, source_tabs)
416            .is_none()
417        {
418            return false;
419        }
420
421        let (panels, active) = match self.nodes.get(source_tabs) {
422            Some(DockNode::Tabs { tabs, active }) if !tabs.is_empty() => (tabs.clone(), *active),
423            _ => return false,
424        };
425        let active = active.min(panels.len().saturating_sub(1));
426
427        if let Some(DockNode::Tabs { tabs, active }) = self.nodes.get_mut(source_tabs) {
428            tabs.clear();
429            *active = 0;
430        }
431        if self.window_root(source_window) == Some(source_tabs) {
432            let _ = self.remove_window_root(source_window);
433        }
434
435        let tabs = self.insert_node(DockNode::Tabs {
436            tabs: panels,
437            active,
438        });
439        self.set_window_root(new_window, tabs);
440
441        self.simplify_window_forest(source_window);
442        self.simplify_window_forest(new_window);
443        true
444    }
445
446    pub fn float_panel_in_window(
447        &mut self,
448        source_window: AppWindowId,
449        panel: PanelKey,
450        target_window: AppWindowId,
451        rect: Rect,
452    ) -> bool {
453        let Some((source_tabs, source_index)) = self.find_panel_in_window(source_window, &panel)
454        else {
455            return false;
456        };
457        if !self.remove_panel_from_tabs(source_tabs, source_index) {
458            return false;
459        }
460
461        let tabs = self.insert_node(DockNode::Tabs {
462            tabs: vec![panel],
463            active: 0,
464        });
465        let floating = self.insert_node(DockNode::Floating { child: tabs });
466        self.floating_windows_mut(target_window)
467            .push(DockFloatingWindow { floating, rect });
468
469        self.simplify_window_forest(source_window);
470        self.simplify_window_forest(target_window);
471        true
472    }
473
474    pub fn float_tabs_in_window(
475        &mut self,
476        source_window: AppWindowId,
477        source_tabs: DockNodeId,
478        target_window: AppWindowId,
479        rect: Rect,
480    ) -> bool {
481        if self
482            .root_for_node_in_window_forest(source_window, source_tabs)
483            .is_none()
484        {
485            return false;
486        }
487
488        let (panels, active) = match self.nodes.get(source_tabs) {
489            Some(DockNode::Tabs { tabs, active }) if !tabs.is_empty() => (tabs.clone(), *active),
490            _ => return false,
491        };
492        let active = active.min(panels.len().saturating_sub(1));
493
494        if let Some(DockNode::Tabs { tabs, active }) = self.nodes.get_mut(source_tabs) {
495            tabs.clear();
496            *active = 0;
497        }
498        if self.window_root(source_window) == Some(source_tabs) {
499            let _ = self.remove_window_root(source_window);
500        }
501        self.simplify_window_forest(source_window);
502
503        let tabs = self.insert_node(DockNode::Tabs {
504            tabs: panels,
505            active,
506        });
507        let floating = self.insert_node(DockNode::Floating { child: tabs });
508        self.floating_windows_mut(target_window)
509            .push(DockFloatingWindow { floating, rect });
510        self.simplify_window_forest(target_window);
511        true
512    }
513
514    pub fn set_floating_rect(
515        &mut self,
516        window: AppWindowId,
517        floating: DockNodeId,
518        rect: Rect,
519    ) -> bool {
520        let Some(list) = self.window_floatings.get_mut(&window) else {
521            return false;
522        };
523        let Some(entry) = list.iter_mut().find(|w| w.floating == floating) else {
524            return false;
525        };
526        entry.rect = rect;
527        true
528    }
529
530    pub fn raise_floating(&mut self, window: AppWindowId, floating: DockNodeId) -> bool {
531        let Some(list) = self.window_floatings.get_mut(&window) else {
532            return false;
533        };
534        let Some(index) = list.iter().position(|w| w.floating == floating) else {
535            return false;
536        };
537        if index + 1 == list.len() {
538            return true;
539        }
540        let entry = list.remove(index);
541        list.push(entry);
542        true
543    }
544
545    pub fn merge_floating_into(
546        &mut self,
547        window: AppWindowId,
548        floating: DockNodeId,
549        target_tabs: DockNodeId,
550    ) -> bool {
551        let Some(list) = self.window_floatings.get(&window) else {
552            return false;
553        };
554        if !list.iter().any(|w| w.floating == floating) {
555            return false;
556        }
557
558        let Some(DockNode::Tabs { .. }) = self.nodes.get(target_tabs) else {
559            return false;
560        };
561        let Some(target_root) = self.root_for_node_in_window_forest(window, target_tabs) else {
562            return false;
563        };
564        // Reject merges into the same floating subtree (would drop panels by removing the floating
565        // entry without actually moving anything).
566        if target_root == floating {
567            return false;
568        }
569
570        let panels = self.collect_panels_in_subtree(floating);
571        for panel in panels {
572            let _ = self.move_panel_between_windows(
573                window,
574                panel,
575                window,
576                target_tabs,
577                DropZone::Center,
578                None,
579            );
580        }
581
582        if let Some(list) = self.window_floatings.get_mut(&window)
583            && let Some(index) = list.iter().position(|w| w.floating == floating)
584        {
585            list.remove(index);
586        }
587        self.simplify_window_forest(window);
588        true
589    }
590
591    pub fn set_active_tab(&mut self, tabs: DockNodeId, active: usize) -> bool {
592        let Some(DockNode::Tabs {
593            tabs: list,
594            active: cur,
595        }) = self.nodes.get_mut(tabs)
596        else {
597            return false;
598        };
599        if list.is_empty() {
600            *cur = 0;
601            return true;
602        }
603        *cur = active.min(list.len() - 1);
604        true
605    }
606
607    pub fn update_split_two(&mut self, split: DockNodeId, first_fraction: f32) -> bool {
608        let Some(DockNode::Split {
609            children,
610            fractions,
611            ..
612        }) = self.nodes.get_mut(split)
613        else {
614            return false;
615        };
616        if children.len() != 2 || fractions.len() != 2 {
617            return false;
618        }
619        let f0 = first_fraction.clamp(0.0, 1.0);
620        fractions[0] = f0;
621        fractions[1] = 1.0 - f0;
622        true
623    }
624
625    pub fn update_split_fractions(&mut self, split: DockNodeId, mut next: Vec<f32>) -> bool {
626        let Some(DockNode::Split {
627            children,
628            fractions,
629            ..
630        }) = self.nodes.get_mut(split)
631        else {
632            return false;
633        };
634        if children.len() < 2 || next.len() != children.len() {
635            return false;
636        }
637
638        for f in &mut next {
639            if !f.is_finite() {
640                *f = 0.0;
641            }
642            *f = (*f).max(0.0);
643        }
644        let sum: f32 = next.iter().sum();
645        if !sum.is_finite() || sum <= f32::EPSILON {
646            next = vec![1.0 / next.len() as f32; next.len()];
647        } else {
648            for f in &mut next {
649                *f /= sum;
650            }
651            let len = next.len();
652            if len >= 1 {
653                let rest: f32 = next.iter().take(len.saturating_sub(1)).sum();
654                next[len - 1] = (1.0 - rest).clamp(0.0, 1.0);
655            }
656        }
657
658        *fractions = next;
659        true
660    }
661
662    fn insert_panel_into_tabs_at(
663        &mut self,
664        tabs: DockNodeId,
665        panel: PanelKey,
666        index: Option<usize>,
667    ) -> bool {
668        let Some(DockNode::Tabs { tabs: list, active }) = self.nodes.get_mut(tabs) else {
669            return false;
670        };
671        if list.contains(&panel) {
672            return true;
673        }
674
675        match index {
676            Some(i) => {
677                let i = i.min(list.len());
678                list.insert(i, panel);
679                *active = i;
680            }
681            None => {
682                list.push(panel);
683                *active = list.len().saturating_sub(1);
684            }
685        }
686        true
687    }
688
689    fn insert_panels_into_tabs_at(
690        &mut self,
691        tabs: DockNodeId,
692        panels: &[PanelKey],
693        index: Option<usize>,
694        active_in_group: usize,
695    ) -> bool {
696        let Some(DockNode::Tabs { tabs: list, active }) = self.nodes.get_mut(tabs) else {
697            return false;
698        };
699        if panels.is_empty() {
700            return true;
701        }
702
703        let mut insert_at = index.unwrap_or(list.len()).min(list.len());
704        for panel in panels {
705            if list.contains(panel) {
706                continue;
707            }
708            list.insert(insert_at, panel.clone());
709            insert_at = insert_at.saturating_add(1);
710        }
711
712        if let Some(active_panel) = panels.get(active_in_group)
713            && let Some(ix) = list.iter().position(|p| p == active_panel)
714        {
715            *active = ix;
716        }
717        if list.is_empty() {
718            *active = 0;
719        } else if *active >= list.len() {
720            *active = list.len().saturating_sub(1);
721        }
722
723        true
724    }
725
726    fn remove_panel_from_tabs(&mut self, tabs: DockNodeId, index: usize) -> bool {
727        let Some(DockNode::Tabs { tabs: list, active }) = self.nodes.get_mut(tabs) else {
728            return false;
729        };
730        if index >= list.len() {
731            return false;
732        }
733
734        list.remove(index);
735        if list.is_empty() {
736            *active = 0;
737        } else if *active >= list.len() {
738            *active = list.len().saturating_sub(1);
739        } else if index < *active {
740            *active = active.saturating_sub(1);
741        }
742        true
743    }
744
745    fn insert_edge_child_prefer_same_axis_split(
746        &mut self,
747        window: AppWindowId,
748        target: DockNodeId,
749        axis: Axis,
750        zone: DropZone,
751        new_child: DockNodeId,
752    ) -> bool {
753        // Keep core commit semantics and docking preview semantics aligned: use the shared, pure
754        // decision helper.
755        let Some(decision) = self.edge_dock_decision(window, target, zone) else {
756            return false;
757        };
758        let EdgeDockDecision::InsertIntoSplit {
759            split,
760            anchor_index,
761            insert_index,
762        } = decision
763        else {
764            return false;
765        };
766
767        let Some(DockNode::Split {
768            axis: split_axis,
769            children,
770            fractions,
771        }) = self.nodes.get_mut(split)
772        else {
773            return false;
774        };
775        if *split_axis != axis || children.len() != fractions.len() || children.is_empty() {
776            return false;
777        }
778
779        split_share_and_insert(children, fractions, anchor_index, insert_index, new_child);
780        true
781    }
782
783    fn replace_node_in_window_tree(
784        &mut self,
785        window: AppWindowId,
786        old: DockNodeId,
787        new: DockNodeId,
788    ) {
789        if self.window_root(window) == Some(old) {
790            self.set_window_root(window, new);
791            return;
792        }
793        if let Some(list) = self.window_floatings.get_mut(&window) {
794            for w in list {
795                if w.floating == old {
796                    w.floating = new;
797                    return;
798                }
799            }
800        }
801
802        if let Some(root) = self.window_root(window)
803            && let Some(parent) = self.find_parent_in_subtree(root, old)
804        {
805            self.replace_child_in_node(parent, old, new);
806            return;
807        }
808
809        // Window roots are optional (e.g. a window may only contain floating dock containers).
810        // We still need to be able to replace nodes within floating subtrees in that case.
811        let floating_roots: Vec<DockNodeId> = self
812            .window_floatings
813            .get(&window)
814            .map(|list| list.iter().map(|w| w.floating).collect())
815            .unwrap_or_default();
816        for floating in floating_roots {
817            if let Some(parent) = self.find_parent_in_subtree(floating, old) {
818                self.replace_child_in_node(parent, old, new);
819                return;
820            }
821        }
822    }
823
824    fn replace_child_in_node(
825        &mut self,
826        node: DockNodeId,
827        old: DockNodeId,
828        new: DockNodeId,
829    ) -> bool {
830        let Some(n) = self.nodes.get_mut(node) else {
831            return false;
832        };
833        match n {
834            DockNode::Split { children, .. } => {
835                let Some(index) = children.iter().position(|c| *c == old) else {
836                    return false;
837                };
838                children[index] = new;
839                true
840            }
841            DockNode::Floating { child } => {
842                if *child != old {
843                    return false;
844                }
845                *child = new;
846                true
847            }
848            DockNode::Tabs { .. } => false,
849        }
850    }
851
852    fn find_parent_in_subtree(&self, node: DockNodeId, target: DockNodeId) -> Option<DockNodeId> {
853        let n = self.nodes.get(node)?;
854        match n {
855            DockNode::Tabs { .. } => None,
856            DockNode::Split { children, .. } => {
857                if children.contains(&target) {
858                    return Some(node);
859                }
860                children
861                    .iter()
862                    .copied()
863                    .find_map(|child| self.find_parent_in_subtree(child, target))
864            }
865            DockNode::Floating { child } => {
866                if *child == target {
867                    return Some(node);
868                }
869                self.find_parent_in_subtree(*child, target)
870            }
871        }
872    }
873
874    fn collapse_empty_tabs_upwards(&mut self, window: AppWindowId, start_tabs: DockNodeId) {
875        let Some(_root) = self.root_for_node_in_window_forest(window, start_tabs) else {
876            return;
877        };
878
879        // Historical helper: this used to assume binary splits. Keep the API, but delegate to the
880        // canonical simplifier (which is N-ary safe).
881        let _ = start_tabs;
882        self.simplify_window_forest(window);
883    }
884
885    pub(super) fn root_for_node_in_window_forest(
886        &self,
887        window: AppWindowId,
888        target: DockNodeId,
889    ) -> Option<DockNodeId> {
890        fn contains(graph: &DockGraph, root: DockNodeId, target: DockNodeId) -> bool {
891            if root == target {
892                return true;
893            }
894            let Some(n) = graph.nodes.get(root) else {
895                return false;
896            };
897            match n {
898                DockNode::Tabs { .. } => false,
899                DockNode::Split { children, .. } => {
900                    children.iter().copied().any(|c| contains(graph, c, target))
901                }
902                DockNode::Floating { child } => contains(graph, *child, target),
903            }
904        }
905
906        if let Some(root) = self.window_root(window)
907            && contains(self, root, target)
908        {
909            return Some(root);
910        }
911        if let Some(list) = self.window_floatings.get(&window) {
912            for w in list {
913                if contains(self, w.floating, target) {
914                    return Some(w.floating);
915                }
916            }
917        }
918        None
919    }
920
921    fn remove_empty_floating_windows(&mut self, window: AppWindowId) {
922        // Kept for compatibility with older call sites; the canonical simplifier already prunes
923        // empty floatings.
924        self.simplify_window_forest(window);
925    }
926}
927
928fn normalize_shares(shares: &mut Vec<f32>) {
929    for f in shares.iter_mut() {
930        if !f.is_finite() {
931            *f = 0.0;
932        }
933        if *f < 0.0 {
934            *f = 0.0;
935        }
936    }
937
938    let sum: f32 = shares.iter().sum();
939    if !sum.is_finite() || sum <= f32::EPSILON {
940        let n = shares.len().max(1);
941        *shares = vec![1.0 / n as f32; n];
942        return;
943    }
944
945    for f in shares.iter_mut() {
946        *f /= sum;
947    }
948
949    // Clamp drift on the last element to keep sum stable.
950    let len = shares.len();
951    if len >= 1 {
952        let rest: f32 = shares.iter().take(len.saturating_sub(1)).sum();
953        shares[len - 1] = (1.0 - rest).clamp(0.0, 1.0);
954    }
955}
956
957fn split_share_and_insert(
958    children: &mut Vec<DockNodeId>,
959    fractions: &mut Vec<f32>,
960    anchor_index: usize,
961    insert_index: usize,
962    new_child: DockNodeId,
963) {
964    debug_assert!(!children.is_empty());
965    debug_assert_eq!(children.len(), fractions.len());
966    debug_assert!(anchor_index < fractions.len());
967    debug_assert!(insert_index <= fractions.len());
968
969    // Default ratio for v1: split the anchor child share in half.
970    let k = 0.5_f32;
971
972    let old = fractions[anchor_index];
973    let keep = old * (1.0 - k);
974    let take = old * k;
975
976    fractions[anchor_index] = keep;
977    children.insert(insert_index, new_child);
978    fractions.insert(insert_index, take);
979}