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`, `Tiles`, 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            NodeKind::Tiles { panels } => {
120                if node.id() != root_id {
121                    ok &= !panels.is_empty();
122                }
123            }
124        });
125        ok
126    }
127}
128
129fn normalize_node(node: &mut PaneNode, changed: &mut bool) {
130    match node.kind_mut() {
131        NodeKind::Tabs { panels, active_ix } => {
132            let clamped = (*active_ix).min(panels.len().saturating_sub(1));
133            if *active_ix != clamped {
134                *active_ix = clamped;
135                *changed = true;
136            }
137        }
138        NodeKind::Tiles { .. } => {}
139        NodeKind::Split {
140            axis,
141            children,
142            sizes,
143        } => {
144            let axis = *axis;
145
146            for child in children.iter_mut() {
147                normalize_node(child, changed);
148            }
149
150            // Rule 1: drop empty children.
151            let mut ix = 0;
152            while ix < children.len() {
153                if is_empty_container(&children[ix]) {
154                    children.remove(ix);
155                    sizes.remove(ix);
156                    *changed = true;
157                } else {
158                    ix += 1;
159                }
160            }
161
162            // Rule 2: a single-child split child is replaced by its child,
163            // which inherits the slot size the split occupied. The child is
164            // moved out rather than cloned — it can carry an arbitrarily deep
165            // subtree, and this runs on every edit.
166            for ix in 0..children.len() {
167                let is_single = matches!(
168                    children[ix].kind_ref(),
169                    NodeKind::Split { children: inner, .. } if inner.len() == 1
170                );
171                if !is_single {
172                    continue;
173                }
174                let NodeKind::Split {
175                    children: inner, ..
176                } = children[ix].kind_mut()
177                else {
178                    continue;
179                };
180                let replacement = inner.remove(0);
181                children[ix] = replacement;
182                *changed = true;
183            }
184
185            // Rule 3: splice same-axis nesting.
186            let mut ix = 0;
187            while ix < children.len() {
188                let same_axis = matches!(
189                    children[ix].kind_ref(),
190                    NodeKind::Split { axis: inner, .. } if *inner == axis
191                );
192                if !same_axis {
193                    ix += 1;
194                    continue;
195                }
196
197                // Taken, not cloned: the spliced children move up a level
198                // rather than being copied and discarded.
199                let NodeKind::Split {
200                    children: inner,
201                    sizes: inner_sizes,
202                    ..
203                } = children[ix].kind_mut()
204                else {
205                    ix += 1;
206                    continue;
207                };
208                let inner = std::mem::take(inner);
209                let inner_sizes = std::mem::take(inner_sizes);
210
211                let slot = sizes[ix];
212                let inner_sizes = distribute_slot(slot, inner_sizes);
213                let count = inner.len();
214                children.splice(ix..=ix, inner);
215                sizes.splice(ix..=ix, inner_sizes);
216                ix += count;
217                *changed = true;
218            }
219        }
220    }
221}
222
223/// Spread an outer slot size across the inner sizes that replace it.
224///
225/// When the outer slot is unconstrained the inner sizes pass through. When it
226/// is fixed and every inner size is known, they are scaled to fill the slot;
227/// otherwise the slot is dropped, matching how an unconstrained child behaves.
228fn distribute_slot(slot: Option<Pixels>, inner: Vec<Option<Pixels>>) -> Vec<Option<Pixels>> {
229    let Some(slot) = slot else { return inner };
230    // `Option<Pixels>` has no `Sum` impl; fold so one unknown size makes the
231    // whole total unknown.
232    let total = inner
233        .iter()
234        .try_fold(Pixels::ZERO, |acc, size| size.map(|size| acc + size));
235    match total {
236        Some(total) if total > Pixels::ZERO => inner
237            .into_iter()
238            .map(|size| size.map(|size| size * (slot / total)))
239            .collect(),
240        _ => inner,
241    }
242}
243
244fn is_empty_container(node: &PaneNode) -> bool {
245    match node.kind_ref() {
246        NodeKind::Split { children, .. } => children.is_empty(),
247        NodeKind::Tabs { panels, .. } => panels.is_empty(),
248        NodeKind::Tiles { panels } => panels.is_empty(),
249    }
250}
251
252/// Rule 5. A `RootKind::Split` tree keeps a split root no matter what, so an
253/// empty center still serializes as a `StackPanel`. A `RootKind::Any` tree
254/// lets rule 2 collapse the root like any other node.
255fn collapse_root(tree: &mut PaneTree, changed: &mut bool) {
256    if tree.root_kind() == RootKind::Split {
257        return;
258    }
259
260    let replacement = match tree.root().kind_ref() {
261        NodeKind::Split { children, .. } if children.len() == 1 => Some(children[0].clone()),
262        _ => None,
263    };
264
265    if let Some(replacement) = replacement {
266        tree.replace_root(replacement);
267        *changed = true;
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::super::*;
274    use gpui::{Axis, Pixels, px};
275
276    fn panel(n: u64) -> PanelId {
277        PanelId::from_u64(n)
278    }
279
280    #[test]
281    fn empty_tab_groups_are_dropped() {
282        let mut tree = PaneTree::new(RootKind::Split);
283        let root = tree.root().id();
284        tree.push_tabs_for_test(root, vec![]);
285        tree.push_tabs_for_test(root, vec![panel(1)]);
286
287        tree.normalize();
288
289        // The empty tab group is dropped by rule 1, leaving the root split
290        // holding the one surviving child.
291        assert!(
292            matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.len() == 1)
293        );
294        assert_eq!(tree.panels().collect::<Vec<_>>(), vec![panel(1)]);
295    }
296
297    #[test]
298    fn a_single_child_split_is_replaced_by_its_child_keeping_the_child_id() {
299        let mut tree = PaneTree::new(RootKind::Any);
300        let outer = tree.set_root_split_for_test(Axis::Horizontal);
301        let inner = tree.push_split_for_test(outer, Axis::Vertical, Some(px(120.)));
302        let tabs = tree.push_tabs_for_test(inner, vec![panel(1)]);
303
304        tree.normalize();
305
306        assert_eq!(tree.root().id(), tabs, "child keeps its own NodeId");
307        assert!(tree.find_node(inner).is_none());
308    }
309
310    #[test]
311    fn a_collapsing_split_hands_its_slot_size_to_the_child() {
312        let mut tree = PaneTree::new(RootKind::Split);
313        let root = tree.root().id();
314        let inner = tree.push_split_for_test(root, Axis::Vertical, Some(px(300.)));
315        tree.push_tabs_for_test(inner, vec![panel(1)]);
316        tree.push_tabs_for_test(root, vec![panel(2)]);
317
318        tree.normalize();
319
320        let PaneRef::Split { sizes, .. } = tree.root().kind() else {
321            panic!()
322        };
323        assert_eq!(
324            sizes[0],
325            Some(px(300.)),
326            "the child inherits the collapsed split's slot"
327        );
328    }
329
330    #[test]
331    fn same_axis_nesting_is_spliced_into_the_parent() {
332        let mut tree = PaneTree::new(RootKind::Split);
333        tree.set_root_axis_for_test(Axis::Horizontal);
334        let root = tree.root().id();
335        tree.push_tabs_for_test(root, vec![panel(1)]);
336        let inner = tree.push_split_for_test(root, Axis::Horizontal, None);
337        tree.push_tabs_for_test(inner, vec![panel(2)]);
338        tree.push_tabs_for_test(inner, vec![panel(3)]);
339
340        tree.normalize();
341
342        let PaneRef::Split { children, axis, .. } = tree.root().kind() else {
343            panic!()
344        };
345        assert_eq!(axis, Axis::Horizontal);
346        assert_eq!(
347            children.len(),
348            3,
349            "the inner split's children are spliced in"
350        );
351        assert_eq!(
352            tree.panels().collect::<Vec<_>>(),
353            vec![panel(1), panel(2), panel(3)],
354            "order is preserved"
355        );
356    }
357
358    #[test]
359    fn active_index_is_clamped_to_the_panel_count() {
360        let mut tree = PaneTree::new(RootKind::Any);
361        let tabs = tree.set_root_tabs_for_test(vec![panel(1), panel(2)], 9);
362
363        tree.normalize();
364
365        let PaneRef::Tabs { active_ix, .. } = tree.find_node(tabs).unwrap().kind() else {
366            panic!()
367        };
368        assert_eq!(active_ix, 1);
369    }
370
371    #[test]
372    fn a_split_root_survives_being_emptied() {
373        let mut tree = PaneTree::new(RootKind::Split);
374        let root = tree.root().id();
375        tree.push_tabs_for_test(root, vec![]);
376
377        tree.normalize();
378
379        assert!(
380            matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.is_empty()),
381            "the center must still serialize as a StackPanel when empty"
382        );
383    }
384
385    /// Rule 3 splices a same-axis child's slots into the parent, scaling them
386    /// to the slot they replace. When one inner slot is unconstrained there is
387    /// no total to scale against, so they pass through — and then the known
388    /// ones are absolute values that no longer relate to the space they landed
389    /// in. This pins what actually happens, so a future change to
390    /// `distribute_slot` has to decide about this case deliberately.
391    #[test]
392    fn a_same_axis_splice_with_one_unknown_inner_size_passes_them_through() {
393        let mut tree = PaneTree::new(RootKind::Split);
394        let root = tree.root().id();
395        let inner = tree.push_split_for_test(root, Axis::Horizontal, Some(px(400.)));
396        tree.push_sized_tabs_for_test(inner, vec![panel(1)], Some(px(100.)));
397        tree.push_sized_tabs_for_test(inner, vec![panel(2)], None);
398
399        tree.normalize();
400
401        let PaneRef::Split { sizes, .. } = tree.root().kind() else {
402            panic!()
403        };
404        assert_eq!(
405            sizes,
406            &[Some(px(100.)), None],
407            "an unknown inner size leaves every sibling unscaled; the 400px \
408             slot they replaced constrains nothing"
409        );
410    }
411
412    /// Dropping a container mid-row hands its space to nobody in the tree —
413    /// the surviving slots keep their absolute sizes and no longer sum to
414    /// anything in particular. The renderer's own resizable state is what
415    /// redistributes on the next layout pass.
416    #[test]
417    fn removing_a_middle_container_leaves_its_siblings_untouched() {
418        let mut tree = PaneTree::new(RootKind::Split);
419        let root = tree.root().id();
420        tree.push_sized_tabs_for_test(root, vec![panel(1)], Some(px(400.)));
421        tree.push_sized_tabs_for_test(root, vec![], Some(px(800.)));
422        tree.push_sized_tabs_for_test(root, vec![panel(3)], Some(px(400.)));
423
424        tree.normalize();
425
426        let PaneRef::Split {
427            sizes, children, ..
428        } = tree.root().kind()
429        else {
430            panic!()
431        };
432        assert_eq!(children.len(), 2);
433        assert_eq!(
434            sizes,
435            &[Some(px(400.)), Some(px(400.))],
436            "the survivors keep their own sizes; the 800px the empty group \
437             held is not handed to either of them here"
438        );
439    }
440
441    #[test]
442    fn normalize_is_idempotent() {
443        let mut tree = PaneTree::new(RootKind::Split);
444        let root = tree.root().id();
445        let inner = tree.push_split_for_test(root, Axis::Horizontal, None);
446        tree.push_tabs_for_test(inner, vec![panel(1)]);
447        tree.push_tabs_for_test(inner, vec![]);
448        tree.push_tabs_for_test(root, vec![panel(2)]);
449
450        tree.normalize();
451        let once = tree.clone();
452        tree.normalize();
453
454        assert_eq!(once, tree);
455    }
456
457    #[test]
458    fn same_axis_splice_scales_inner_sizes_to_fill_the_outer_slot() {
459        // Every other test that reaches a same-axis splice pushes children
460        // with an unknown (`None`) size, so it only ever exercises
461        // `distribute_slot`'s pass-through branches. This is the one test
462        // that gives every sibling a known size, forcing the scaling arm.
463        let mut tree = PaneTree::new(RootKind::Split);
464        let root = tree.root().id();
465        let inner = tree.push_split_for_test(root, Axis::Horizontal, Some(px(400.)));
466        tree.push_sized_tabs_for_test(inner, vec![panel(1)], Some(px(50.)));
467        tree.push_sized_tabs_for_test(inner, vec![panel(2)], Some(px(150.)));
468
469        tree.normalize();
470
471        let PaneRef::Split { sizes, .. } = tree.root().kind() else {
472            panic!()
473        };
474        assert_eq!(
475            sizes,
476            &[Some(px(100.)), Some(px(300.))],
477            "sizes scale by the outer/inner ratio (400/200 = 2x), not by its reverse"
478        );
479        let total: Pixels = sizes.iter().flatten().copied().sum();
480        assert_eq!(
481            total,
482            px(400.),
483            "the scaled sizes sum back to the outer slot"
484        );
485    }
486
487    #[test]
488    fn normalize_converges_within_two_passes_on_an_adversarial_tree() {
489        // root(H) -> D(V) -> A(H) -> { empty, B(V) -> C(V) -> [leaf1, leaf2] }
490        //
491        // `RootKind::Any` lets rule 5 collapse the root itself, so this tree
492        // combines every rule at once: single-child splits nested five
493        // levels deep (root, D, A, B all start single-child), an empty
494        // container dropped mid-chain (under A), and same-axis nesting
495        // spliced twice (C into B, then the surviving B into D). Everything
496        // still has to bottom out at a fixpoint within 2 passes: one pass
497        // that resolves every rule bottom-up plus the root collapse, one
498        // pass that confirms nothing is left to change.
499        let mut tree = PaneTree::new(RootKind::Any);
500        let root = tree.root().id();
501        let d = tree.push_split_for_test(root, Axis::Vertical, None);
502        let a = tree.push_split_for_test(d, Axis::Horizontal, None);
503        tree.push_tabs_for_test(a, vec![]);
504        let b = tree.push_split_for_test(a, Axis::Vertical, None);
505        let c = tree.push_split_for_test(b, Axis::Vertical, None);
506        tree.push_tabs_for_test(c, vec![panel(1)]);
507        tree.push_tabs_for_test(c, vec![panel(2)]);
508
509        let passes = tree.normalize_pass_count_for_test();
510
511        assert!(
512            passes <= 2,
513            "expected the fixpoint within 2 passes, took {passes}"
514        );
515        assert!(tree.is_normalized());
516        assert_eq!(
517            tree.panels().collect::<Vec<_>>(),
518            vec![panel(1), panel(2)],
519            "every panel survives the collapse, in order"
520        );
521    }
522}