Skip to main content

gpui_base/dock/layout/
normalize.rs

1use gpui::Pixels;
2
3use super::node::{NodeKind, PaneNode};
4use super::tree::{PaneTree, RootKind};
5
6/// Upper bound on `normalize` passes. Every pass that changes anything
7/// strictly reduces node count or nesting depth, so real dock layouts
8/// converge in a small handful of passes; this is a generous ceiling against
9/// a future rule change that fights another rule rather than a bound tuned
10/// to today's rule set.
11const MAX_NORMALIZE_PASSES: u32 = 64;
12
13impl PaneTree {
14    /// Collapse the tree to canonical shape.
15    ///
16    /// One post-order pass repeated to a fixpoint. This is the only place a
17    /// container is removed for being empty, replacing the mutually recursive
18    /// `remove_self_if_empty` pair the old implementation used. It needs no
19    /// parent pointers and no deferred work, so the tree is self-consistent
20    /// the instant an edit returns.
21    ///
22    /// Rules, applied bottom up:
23    ///
24    /// 1. An empty `Tabs` or `Split` is removed from its parent.
25    /// 2. A `Split` with one child is replaced by that child. The child keeps
26    ///    its own `NodeId` and inherits the split's slot size.
27    /// 3. A `Split` whose child is a `Split` of the same axis splices that
28    ///    child's children into itself.
29    /// 4. `active_ix` is clamped.
30    /// 5. The root is preserved according to [`RootKind`].
31    ///
32    /// Idempotent: `normalize(normalize(t)) == normalize(t)`.
33    pub fn normalize(&mut self) {
34        self.normalize_reporting();
35    }
36
37    /// [`Self::normalize`], reporting whether it changed anything.
38    ///
39    /// `edit` uses this instead of comparing whole trees: collapse is the only
40    /// thing that can change a tree after a mutation has already reported what
41    /// it did, so the two booleans together are exactly the answer, and no
42    /// snapshot of the previous tree has to be kept to reach it.
43    pub(crate) fn normalize_reporting(&mut self) -> bool {
44        let changed = self.run_normalize_passes().1;
45        debug_assert!(self.is_normalized(), "normalize did not reach a fixpoint");
46        changed
47    }
48
49    /// Run passes until nothing changes, or until [`MAX_NORMALIZE_PASSES`] is
50    /// exhausted. Returns the number of passes run, and whether any pass
51    /// changed the tree.
52    ///
53    /// Split out from [`Self::normalize`] so a `#[cfg(test)]` caller can pin
54    /// how many passes convergence actually takes, without widening the
55    /// public API with a pass count nobody outside tests needs.
56    fn run_normalize_passes(&mut self) -> (u32, bool) {
57        let mut passes = 0;
58        let mut changed = true;
59        let mut any_change = false;
60        // Bounded because every pass that changes anything strictly reduces
61        // node count or nesting depth.
62        while changed && passes < MAX_NORMALIZE_PASSES {
63            changed = false;
64            normalize_node(self.root_mut(), &mut changed);
65            collapse_root(self, &mut changed);
66            any_change |= changed;
67            passes += 1;
68        }
69
70        // `debug_assert!` in `normalize` disappears in release builds, so a
71        // desktop build left silently short of the fixpoint would otherwise
72        // render a non-canonical layout with no trace of why. This keeps the
73        // failure observable without turning it into a user-facing panic:
74        // rendering a slightly non-canonical layout beats crashing the app.
75        if changed {
76            tracing::warn!(
77                passes,
78                "PaneTree::normalize exhausted {MAX_NORMALIZE_PASSES} passes without reaching \
79                 a fixpoint; the tree may still contain an empty container, a single-child \
80                 split, same-axis split nesting, or an unclamped Tabs active_ix"
81            );
82        }
83
84        (passes, any_change)
85    }
86
87    /// Test-only hook so a test can pin how many passes convergence takes,
88    /// without exposing a pass count through the public `normalize` API.
89    #[cfg(test)]
90    pub(crate) fn normalize_pass_count_for_test(&mut self) -> u32 {
91        self.run_normalize_passes().0
92    }
93
94    /// Whether the tree satisfies every structural invariant.
95    pub(crate) fn is_normalized(&self) -> bool {
96        let mut ok = true;
97        let root_id = self.root().id();
98        self.root().walk(&mut |node| match node.kind_ref() {
99            NodeKind::Split {
100                children,
101                sizes,
102                axis,
103            } => {
104                ok &= children.len() == sizes.len();
105                // The root may legitimately be an empty or single-child split.
106                if node.id() != root_id {
107                    ok &= children.len() > 1;
108                }
109                ok &= !children.iter().any(|child| {
110                    matches!(child.kind_ref(), NodeKind::Split { axis: inner, .. } if inner == axis)
111                });
112            }
113            NodeKind::Tabs { panels, active_ix } => {
114                ok &= panels.is_empty() || *active_ix < panels.len();
115                if node.id() != root_id {
116                    ok &= !panels.is_empty();
117                }
118            }
119        });
120        ok
121    }
122}
123
124fn normalize_node(node: &mut PaneNode, changed: &mut bool) {
125    match node.kind_mut() {
126        NodeKind::Tabs { panels, active_ix } => {
127            let clamped = (*active_ix).min(panels.len().saturating_sub(1));
128            if *active_ix != clamped {
129                *active_ix = clamped;
130                *changed = true;
131            }
132        }
133        NodeKind::Split {
134            axis,
135            children,
136            sizes,
137        } => {
138            let axis = *axis;
139
140            for child in children.iter_mut() {
141                normalize_node(child, changed);
142            }
143
144            // Rule 1: drop empty children.
145            let mut ix = 0;
146            while ix < children.len() {
147                if is_empty_container(&children[ix]) {
148                    children.remove(ix);
149                    sizes.remove(ix);
150                    *changed = true;
151                } else {
152                    ix += 1;
153                }
154            }
155
156            // Rule 2: a single-child split child is replaced by its child,
157            // which inherits the slot size the split occupied. The child is
158            // moved out rather than cloned — it can carry an arbitrarily deep
159            // subtree, and this runs on every edit.
160            for ix in 0..children.len() {
161                let is_single = matches!(
162                    children[ix].kind_ref(),
163                    NodeKind::Split { children: inner, .. } if inner.len() == 1
164                );
165                if !is_single {
166                    continue;
167                }
168                let NodeKind::Split {
169                    children: inner, ..
170                } = children[ix].kind_mut()
171                else {
172                    continue;
173                };
174                let replacement = inner.remove(0);
175                children[ix] = replacement;
176                *changed = true;
177            }
178
179            // Rule 3: splice same-axis nesting.
180            let mut ix = 0;
181            while ix < children.len() {
182                let same_axis = matches!(
183                    children[ix].kind_ref(),
184                    NodeKind::Split { axis: inner, .. } if *inner == axis
185                );
186                if !same_axis {
187                    ix += 1;
188                    continue;
189                }
190
191                // Taken, not cloned: the spliced children move up a level
192                // rather than being copied and discarded.
193                let NodeKind::Split {
194                    children: inner,
195                    sizes: inner_sizes,
196                    ..
197                } = children[ix].kind_mut()
198                else {
199                    ix += 1;
200                    continue;
201                };
202                let inner = std::mem::take(inner);
203                let inner_sizes = std::mem::take(inner_sizes);
204
205                let slot = sizes[ix];
206                let inner_sizes = distribute_slot(slot, inner_sizes);
207                let count = inner.len();
208                children.splice(ix..=ix, inner);
209                sizes.splice(ix..=ix, inner_sizes);
210                ix += count;
211                *changed = true;
212            }
213        }
214    }
215}
216
217/// Spread an outer slot size across the inner sizes that replace it.
218///
219/// When the outer slot is unconstrained the inner sizes pass through. When it
220/// is fixed and every inner size is known, they are scaled to fill the slot;
221/// otherwise the slot is dropped, matching how an unconstrained child behaves.
222fn distribute_slot(slot: Option<Pixels>, inner: Vec<Option<Pixels>>) -> Vec<Option<Pixels>> {
223    let Some(slot) = slot else { return inner };
224    // `Option<Pixels>` has no `Sum` impl; fold so one unknown size makes the
225    // whole total unknown.
226    let total = inner
227        .iter()
228        .try_fold(Pixels::ZERO, |acc, size| size.map(|size| acc + size));
229    match total {
230        Some(total) if total > Pixels::ZERO => inner
231            .into_iter()
232            .map(|size| size.map(|size| size * (slot / total)))
233            .collect(),
234        _ => inner,
235    }
236}
237
238fn is_empty_container(node: &PaneNode) -> bool {
239    match node.kind_ref() {
240        NodeKind::Split { children, .. } => children.is_empty(),
241        NodeKind::Tabs { panels, .. } => panels.is_empty(),
242    }
243}
244
245/// Rule 5. A `RootKind::Split` tree keeps a split root no matter what, so an
246/// empty center still serializes as a `StackPanel`. A `RootKind::Any` tree
247/// lets rule 2 collapse the root like any other node.
248fn collapse_root(tree: &mut PaneTree, changed: &mut bool) {
249    if tree.root_kind() == RootKind::Split {
250        return;
251    }
252
253    let replacement = match tree.root().kind_ref() {
254        NodeKind::Split { children, .. } if children.len() == 1 => Some(children[0].clone()),
255        _ => None,
256    };
257
258    if let Some(replacement) = replacement {
259        tree.replace_root(replacement);
260        *changed = true;
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::super::*;
267    use gpui::{Axis, Pixels, px};
268
269    fn panel(n: u64) -> PanelId {
270        PanelId::from_u64(n)
271    }
272
273    #[test]
274    fn empty_tab_groups_are_dropped() {
275        let mut tree = PaneTree::new(RootKind::Split);
276        let root = tree.root().id();
277        tree.push_tabs_for_test(root, vec![]);
278        tree.push_tabs_for_test(root, vec![panel(1)]);
279
280        tree.normalize();
281
282        // The empty tab group is dropped by rule 1, leaving the root split
283        // holding the one surviving child.
284        assert!(
285            matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.len() == 1)
286        );
287        assert_eq!(tree.panels().collect::<Vec<_>>(), vec![panel(1)]);
288    }
289
290    #[test]
291    fn a_single_child_split_is_replaced_by_its_child_keeping_the_child_id() {
292        let mut tree = PaneTree::new(RootKind::Any);
293        let outer = tree.set_root_split_for_test(Axis::Horizontal);
294        let inner = tree.push_split_for_test(outer, Axis::Vertical, Some(px(120.)));
295        let tabs = tree.push_tabs_for_test(inner, vec![panel(1)]);
296
297        tree.normalize();
298
299        assert_eq!(tree.root().id(), tabs, "child keeps its own NodeId");
300        assert!(tree.find_node(inner).is_none());
301    }
302
303    #[test]
304    fn a_collapsing_split_hands_its_slot_size_to_the_child() {
305        let mut tree = PaneTree::new(RootKind::Split);
306        let root = tree.root().id();
307        let inner = tree.push_split_for_test(root, Axis::Vertical, Some(px(300.)));
308        tree.push_tabs_for_test(inner, vec![panel(1)]);
309        tree.push_tabs_for_test(root, vec![panel(2)]);
310
311        tree.normalize();
312
313        let PaneRef::Split { sizes, .. } = tree.root().kind() else {
314            panic!()
315        };
316        assert_eq!(
317            sizes[0],
318            Some(px(300.)),
319            "the child inherits the collapsed split's slot"
320        );
321    }
322
323    #[test]
324    fn same_axis_nesting_is_spliced_into_the_parent() {
325        let mut tree = PaneTree::new(RootKind::Split);
326        tree.set_root_axis_for_test(Axis::Horizontal);
327        let root = tree.root().id();
328        tree.push_tabs_for_test(root, vec![panel(1)]);
329        let inner = tree.push_split_for_test(root, Axis::Horizontal, None);
330        tree.push_tabs_for_test(inner, vec![panel(2)]);
331        tree.push_tabs_for_test(inner, vec![panel(3)]);
332
333        tree.normalize();
334
335        let PaneRef::Split { children, axis, .. } = tree.root().kind() else {
336            panic!()
337        };
338        assert_eq!(axis, Axis::Horizontal);
339        assert_eq!(
340            children.len(),
341            3,
342            "the inner split's children are spliced in"
343        );
344        assert_eq!(
345            tree.panels().collect::<Vec<_>>(),
346            vec![panel(1), panel(2), panel(3)],
347            "order is preserved"
348        );
349    }
350
351    #[test]
352    fn active_index_is_clamped_to_the_panel_count() {
353        let mut tree = PaneTree::new(RootKind::Any);
354        let tabs = tree.set_root_tabs_for_test(vec![panel(1), panel(2)], 9);
355
356        tree.normalize();
357
358        let PaneRef::Tabs { active_ix, .. } = tree.find_node(tabs).unwrap().kind() else {
359            panic!()
360        };
361        assert_eq!(active_ix, 1);
362    }
363
364    #[test]
365    fn a_split_root_survives_being_emptied() {
366        let mut tree = PaneTree::new(RootKind::Split);
367        let root = tree.root().id();
368        tree.push_tabs_for_test(root, vec![]);
369
370        tree.normalize();
371
372        assert!(
373            matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.is_empty()),
374            "the center must still serialize as a StackPanel when empty"
375        );
376    }
377
378    /// Rule 3 splices a same-axis child's slots into the parent, scaling them
379    /// to the slot they replace. When one inner slot is unconstrained there is
380    /// no total to scale against, so they pass through — and then the known
381    /// ones are absolute values that no longer relate to the space they landed
382    /// in. This pins what actually happens, so a future change to
383    /// `distribute_slot` has to decide about this case deliberately.
384    #[test]
385    fn a_same_axis_splice_with_one_unknown_inner_size_passes_them_through() {
386        let mut tree = PaneTree::new(RootKind::Split);
387        let root = tree.root().id();
388        let inner = tree.push_split_for_test(root, Axis::Horizontal, Some(px(400.)));
389        tree.push_sized_tabs_for_test(inner, vec![panel(1)], Some(px(100.)));
390        tree.push_sized_tabs_for_test(inner, vec![panel(2)], None);
391
392        tree.normalize();
393
394        let PaneRef::Split { sizes, .. } = tree.root().kind() else {
395            panic!()
396        };
397        assert_eq!(
398            sizes,
399            &[Some(px(100.)), None],
400            "an unknown inner size leaves every sibling unscaled; the 400px \
401             slot they replaced constrains nothing"
402        );
403    }
404
405    /// Dropping a container mid-row hands its space to nobody in the tree —
406    /// the surviving slots keep their absolute sizes and no longer sum to
407    /// anything in particular. The renderer's own resizable state is what
408    /// redistributes on the next layout pass.
409    #[test]
410    fn removing_a_middle_container_leaves_its_siblings_untouched() {
411        let mut tree = PaneTree::new(RootKind::Split);
412        let root = tree.root().id();
413        tree.push_sized_tabs_for_test(root, vec![panel(1)], Some(px(400.)));
414        tree.push_sized_tabs_for_test(root, vec![], Some(px(800.)));
415        tree.push_sized_tabs_for_test(root, vec![panel(3)], Some(px(400.)));
416
417        tree.normalize();
418
419        let PaneRef::Split {
420            sizes, children, ..
421        } = tree.root().kind()
422        else {
423            panic!()
424        };
425        assert_eq!(children.len(), 2);
426        assert_eq!(
427            sizes,
428            &[Some(px(400.)), Some(px(400.))],
429            "the survivors keep their own sizes; the 800px the empty group \
430             held is not handed to either of them here"
431        );
432    }
433
434    #[test]
435    fn normalize_is_idempotent() {
436        let mut tree = PaneTree::new(RootKind::Split);
437        let root = tree.root().id();
438        let inner = tree.push_split_for_test(root, Axis::Horizontal, None);
439        tree.push_tabs_for_test(inner, vec![panel(1)]);
440        tree.push_tabs_for_test(inner, vec![]);
441        tree.push_tabs_for_test(root, vec![panel(2)]);
442
443        tree.normalize();
444        let once = tree.clone();
445        tree.normalize();
446
447        assert_eq!(once, tree);
448    }
449
450    #[test]
451    fn same_axis_splice_scales_inner_sizes_to_fill_the_outer_slot() {
452        // Every other test that reaches a same-axis splice pushes children
453        // with an unknown (`None`) size, so it only ever exercises
454        // `distribute_slot`'s pass-through branches. This is the one test
455        // that gives every sibling a known size, forcing the scaling arm.
456        let mut tree = PaneTree::new(RootKind::Split);
457        let root = tree.root().id();
458        let inner = tree.push_split_for_test(root, Axis::Horizontal, Some(px(400.)));
459        tree.push_sized_tabs_for_test(inner, vec![panel(1)], Some(px(50.)));
460        tree.push_sized_tabs_for_test(inner, vec![panel(2)], Some(px(150.)));
461
462        tree.normalize();
463
464        let PaneRef::Split { sizes, .. } = tree.root().kind() else {
465            panic!()
466        };
467        assert_eq!(
468            sizes,
469            &[Some(px(100.)), Some(px(300.))],
470            "sizes scale by the outer/inner ratio (400/200 = 2x), not by its reverse"
471        );
472        let total: Pixels = sizes.iter().flatten().copied().sum();
473        assert_eq!(
474            total,
475            px(400.),
476            "the scaled sizes sum back to the outer slot"
477        );
478    }
479
480    #[test]
481    fn normalize_converges_within_two_passes_on_an_adversarial_tree() {
482        // root(H) -> D(V) -> A(H) -> { empty, B(V) -> C(V) -> [leaf1, leaf2] }
483        //
484        // `RootKind::Any` lets rule 5 collapse the root itself, so this tree
485        // combines every rule at once: single-child splits nested five
486        // levels deep (root, D, A, B all start single-child), an empty
487        // container dropped mid-chain (under A), and same-axis nesting
488        // spliced twice (C into B, then the surviving B into D). Everything
489        // still has to bottom out at a fixpoint within 2 passes: one pass
490        // that resolves every rule bottom-up plus the root collapse, one
491        // pass that confirms nothing is left to change.
492        let mut tree = PaneTree::new(RootKind::Any);
493        let root = tree.root().id();
494        let d = tree.push_split_for_test(root, Axis::Vertical, None);
495        let a = tree.push_split_for_test(d, Axis::Horizontal, None);
496        tree.push_tabs_for_test(a, vec![]);
497        let b = tree.push_split_for_test(a, Axis::Vertical, None);
498        let c = tree.push_split_for_test(b, Axis::Vertical, None);
499        tree.push_tabs_for_test(c, vec![panel(1)]);
500        tree.push_tabs_for_test(c, vec![panel(2)]);
501
502        let passes = tree.normalize_pass_count_for_test();
503
504        assert!(
505            passes <= 2,
506            "expected the fixpoint within 2 passes, took {passes}"
507        );
508        assert!(tree.is_normalized());
509        assert_eq!(
510            tree.panels().collect::<Vec<_>>(),
511            vec![panel(1), panel(2)],
512            "every panel survives the collapse, in order"
513        );
514    }
515}