Skip to main content

gpui_base/dock/layout/
edit.rs

1use std::collections::HashMap;
2
3use gpui::{Bounds, Pixels};
4
5use crate::Placement;
6
7use super::node::{NodeId, NodeKind, PaneNode, PanelId, TilePanel};
8use super::tree::PaneTree;
9
10/// Where a panel should land.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub enum InsertTarget {
13    /// Into an existing tab group, optionally at a specific index.
14    Tabs {
15        node: NodeId,
16        ix: Option<usize>,
17        activate: bool,
18    },
19    /// Beside an existing node, creating a new tab group for the panel.
20    Split {
21        node: NodeId,
22        placement: Placement,
23        size: Option<Pixels>,
24    },
25    /// Onto a tiles canvas at the given bounds.
26    Tile {
27        node: NodeId,
28        bounds: Bounds<Pixels>,
29    },
30}
31
32/// What one edit changed.
33///
34/// Only whether anything changed, for now. An earlier revision also carried
35/// the created and removed nodes, the removed panels, and the activation
36/// edges — but nothing outside tests ever read them, and computing them meant
37/// cloning the whole tree on every edit to diff against, on a path that a
38/// tile drag walks once per mouse move. Fields are private, so any of them
39/// can come back the day something needs one.
40#[derive(Clone, Debug, Default, PartialEq)]
41pub struct EditResult {
42    changed: bool,
43}
44
45impl EditResult {
46    pub fn changed(&self) -> bool {
47        self.changed
48    }
49}
50
51impl PaneTree {
52    pub fn insert_panel(&mut self, panel: PanelId, target: InsertTarget) -> EditResult {
53        self.edit(|tree| tree.apply_insert(panel, target))
54    }
55
56    pub fn remove_panel(&mut self, panel: PanelId) -> EditResult {
57        self.edit(|tree| tree.detach_panel(panel))
58    }
59
60    /// Move a panel to a new home without ever removing it from the tree's
61    /// perspective, so the caller never fires `on_removed` for a drag.
62    pub fn move_panel(&mut self, panel: PanelId, target: InsertTarget) -> EditResult {
63        self.edit(|tree| {
64            let detached = tree.detach_panel(panel);
65            let inserted = tree.apply_insert(panel, target);
66            detached || inserted
67        })
68    }
69
70    pub fn split(
71        &mut self,
72        at: NodeId,
73        panel: PanelId,
74        placement: Placement,
75        size: Option<Pixels>,
76    ) -> EditResult {
77        self.insert_panel(
78            panel,
79            InsertTarget::Split {
80                node: at,
81                placement,
82                size,
83            },
84        )
85    }
86
87    pub fn set_active(&mut self, node: NodeId, ix: usize) -> EditResult {
88        self.edit(|tree| {
89            let Some(path) = tree.path_of_node(node) else {
90                return false;
91            };
92            let NodeKind::Tabs { active_ix, .. } = tree.node_at_mut(&path).kind_mut() else {
93                return false;
94            };
95            if *active_ix == ix {
96                return false;
97            }
98            *active_ix = ix;
99            true
100        })
101    }
102
103    /// Replace a split's slot sizes wholesale.
104    ///
105    /// A no-op, like every other operation given input it cannot resolve, if
106    /// `new_sizes.len()` does not match the split's child count: no rule in
107    /// `normalize` repairs a length mismatch, so applying it would otherwise
108    /// leave `children.len() != sizes.len()` and trip `normalize`'s
109    /// `debug_assert!`.
110    pub fn set_sizes(&mut self, node: NodeId, new_sizes: Vec<Option<Pixels>>) -> EditResult {
111        self.edit(|tree| {
112            let Some(path) = tree.path_of_node(node) else {
113                return false;
114            };
115            let NodeKind::Split {
116                children, sizes, ..
117            } = tree.node_at_mut(&path).kind_mut()
118            else {
119                return false;
120            };
121            if new_sizes.len() != children.len() || *sizes == new_sizes {
122                return false;
123            }
124            *sizes = new_sizes;
125            true
126        })
127    }
128
129    pub fn set_tile_bounds(&mut self, panel: PanelId, bounds: Bounds<Pixels>) -> EditResult {
130        self.edit(|tree| {
131            let mut changed = false;
132            tree.with_tile(panel, |tile| {
133                if tile.bounds() != bounds {
134                    *tile = tile.with_bounds(bounds);
135                    changed = true;
136                }
137            });
138            changed
139        })
140    }
141
142    pub fn bring_to_front(&mut self, panel: PanelId) -> EditResult {
143        self.edit(|tree| {
144            let top = tree.max_z_index();
145            let mut changed = false;
146            tree.with_tile(panel, |tile| {
147                // Already on top is a no-op rather than another increment, so
148                // repeatedly grabbing the front tile neither churns the tree
149                // nor lets z-indices climb forever.
150                if tile.z_index() < top {
151                    *tile = tile.with_z_index(top + 1);
152                    changed = true;
153                }
154            });
155            changed
156        })
157    }
158}
159
160impl PaneTree {
161    /// Replace every split's slot sizes with what that split is actually drawn
162    /// at.
163    ///
164    /// A layout is usually built with unconstrained slots, and `None` stays
165    /// `None` in the tree no matter how the split is later measured or
166    /// dragged. That is fine until an edit has to divide space — dropping a
167    /// panel beside another one has to halve *something*, and it cannot halve
168    /// an unknown. Adopting the measured sizes first turns the question into
169    /// arithmetic on real pixels, and it matches what the person dragging
170    /// sees: the layout on screen is the layout being divided.
171    pub(crate) fn adopt_measured_sizes(&mut self, measured: &HashMap<NodeId, Vec<Pixels>>) {
172        fn walk(node: &mut PaneNode, measured: &HashMap<NodeId, Vec<Pixels>>) {
173            let id = node.id();
174            let NodeKind::Split {
175                children, sizes, ..
176            } = node.kind_mut()
177            else {
178                return;
179            };
180
181            if let Some(actual) = measured.get(&id) {
182                if actual.len() == sizes.len() {
183                    for (slot, size) in sizes.iter_mut().zip(actual) {
184                        // A zero means the split has not been laid out yet, so
185                        // the slot keeps whatever it already had.
186                        if *size > Pixels::ZERO {
187                            *slot = Some(*size);
188                        }
189                    }
190                }
191            }
192
193            for child in children.iter_mut() {
194                walk(child, measured);
195            }
196        }
197
198        walk(self.root_mut(), measured);
199    }
200}
201
202impl PaneTree {
203    /// Apply one mutation, then collapse.
204    ///
205    /// `apply` reports whether it changed anything and normalization reports
206    /// the same, so no snapshot of the previous tree is needed to answer the
207    /// only question a caller asks. A mutation that cannot resolve its target
208    /// returns `false` and the whole edit is a no-op.
209    fn edit(&mut self, apply: impl FnOnce(&mut Self) -> bool) -> EditResult {
210        let mutated = apply(self);
211        let collapsed = self.normalize_reporting();
212
213        EditResult {
214            changed: mutated || collapsed,
215        }
216    }
217
218    /// Whether `panel` is anywhere in this tree.
219    pub fn contains_panel(&self, panel: PanelId) -> bool {
220        self.panels().any(|candidate| candidate == panel)
221    }
222}
223
224impl PaneTree {
225    /// Returns whether the panel actually landed. A target this tree cannot
226    /// resolve — a stale node id, or one whose kind does not match the
227    /// target — is a no-op rather than an error, and reports `false`.
228    fn apply_insert(&mut self, panel: PanelId, target: InsertTarget) -> bool {
229        match target {
230            InsertTarget::Tabs { node, ix, activate } => {
231                let Some(path) = self.path_of_node(node) else {
232                    return false;
233                };
234                let NodeKind::Tabs { panels, active_ix } = self.node_at_mut(&path).kind_mut()
235                else {
236                    return false;
237                };
238                let ix = ix.unwrap_or(panels.len()).min(panels.len());
239                panels.insert(ix, panel);
240                if activate {
241                    *active_ix = ix;
242                } else if ix <= *active_ix && panels.len() > 1 {
243                    // Keep the displayed panel displayed.
244                    *active_ix += 1;
245                }
246                true
247            }
248            InsertTarget::Split {
249                node,
250                placement,
251                size,
252            } => self.insert_beside(node, panel, placement, size),
253            InsertTarget::Tile { node, bounds } => {
254                let Some(path) = self.path_of_node(node) else {
255                    return false;
256                };
257                let top = self.max_z_index();
258                let NodeKind::Tiles { panels } = self.node_at_mut(&path).kind_mut() else {
259                    return false;
260                };
261                panels.push(TilePanel::new(panel, bounds).with_z_index(top + 1));
262                true
263            }
264        }
265    }
266
267    /// Place `panel` in a new tab group beside `node`.
268    ///
269    /// When the parent split already runs along the placement's axis the new
270    /// group becomes a sibling. Otherwise `node` is wrapped in a fresh split of
271    /// the placement's axis. Rule 3 of `normalize` then flattens any redundant
272    /// nesting this creates, which is why no "reuse the parent split" special
273    /// case is needed here.
274    fn insert_beside(
275        &mut self,
276        node: NodeId,
277        panel: PanelId,
278        placement: Placement,
279        size: Option<Pixels>,
280    ) -> bool {
281        let Some(path) = self.path_of_node(node) else {
282            return false;
283        };
284        let group_id = self.allocate_node_id();
285        let group = PaneNode::new(
286            group_id,
287            NodeKind::Tabs {
288                panels: vec![panel],
289                active_ix: 0,
290            },
291        );
292        let before = matches!(placement, Placement::Left | Placement::Top);
293
294        if let Some((parent_path, ix)) = split_parent_of(&path) {
295            let parent_axis = match self.node_at(&parent_path).kind_ref() {
296                NodeKind::Split { axis, .. } => Some(*axis),
297                _ => None,
298            };
299
300            if parent_axis == Some(placement.axis()) {
301                let NodeKind::Split {
302                    children, sizes, ..
303                } = self.node_at_mut(&parent_path).kind_mut()
304                else {
305                    return false;
306                };
307                // The new group splits the slot it landed beside, rather
308                // than the whole row being re-divided: drop a panel next to
309                // one neighbour and it is that neighbour's space you take.
310                // A caller that named a size gets exactly it, and the
311                // neighbour is left alone.
312                //
313                // An unconstrained neighbour halves to another `None`, which
314                // is right for the same reason — the two of them go on
315                // sharing whatever the fixed slots leave over, now between
316                // themselves.
317                let share = match size {
318                    Some(size) => Some(size),
319                    None => {
320                        let half = sizes[ix].map(|slot| slot / 2.);
321                        sizes[ix] = half;
322                        half
323                    }
324                };
325                let at = if before { ix } else { ix + 1 };
326                children.insert(at, group);
327                sizes.insert(at, share);
328                return true;
329            }
330        }
331
332        // Wrap the target in a new split of the placement's axis. The target
333        // is swapped out rather than cloned: it can be a whole subtree, and
334        // the placeholder left behind is overwritten two statements later.
335        let wrapper_id = self.allocate_node_id();
336        let target = std::mem::replace(
337            self.node_at_mut(&path),
338            PaneNode::new(
339                wrapper_id,
340                NodeKind::Tabs {
341                    panels: Vec::new(),
342                    active_ix: 0,
343                },
344            ),
345        );
346        let (children, sizes) = if before {
347            (vec![group, target], vec![size, None])
348        } else {
349            (vec![target, group], vec![None, size])
350        };
351        let wrapper = PaneNode::new(
352            wrapper_id,
353            NodeKind::Split {
354                axis: placement.axis(),
355                children,
356                sizes,
357            },
358        );
359        *self.node_at_mut(&path) = wrapper;
360        true
361    }
362
363    /// Remove `panel` wherever it lives. Returns whether it was found.
364    fn detach_panel(&mut self, panel: PanelId) -> bool {
365        let Some(node) = self.find_panel_node(panel) else {
366            return false;
367        };
368        let Some(path) = self.path_of_node(node) else {
369            return false;
370        };
371
372        match self.node_at_mut(&path).kind_mut() {
373            NodeKind::Tabs { panels, active_ix } => {
374                let Some(ix) = panels.iter().position(|p| *p == panel) else {
375                    return false;
376                };
377                panels.remove(ix);
378                if ix < *active_ix {
379                    *active_ix -= 1;
380                }
381                true
382            }
383            NodeKind::Tiles { panels } => {
384                let Some(ix) = panels.iter().position(|p| p.panel() == panel) else {
385                    return false;
386                };
387                panels.remove(ix);
388                true
389            }
390            NodeKind::Split { .. } => false,
391        }
392    }
393
394    fn with_tile(&mut self, panel: PanelId, f: impl FnOnce(&mut TilePanel)) {
395        let Some(node) = self.find_panel_node(panel) else {
396            return;
397        };
398        let Some(path) = self.path_of_node(node) else {
399            return;
400        };
401        if let NodeKind::Tiles { panels } = self.node_at_mut(&path).kind_mut() {
402            if let Some(tile) = panels.iter_mut().find(|tile| tile.panel() == panel) {
403                f(tile);
404            }
405        }
406    }
407
408    /// Highest z-index across every tiles canvas in the tree.
409    fn max_z_index(&self) -> usize {
410        let mut top = 0;
411        self.root().walk(&mut |node| {
412            if let NodeKind::Tiles { panels } = node.kind_ref() {
413                top = top.max(panels.iter().map(TilePanel::z_index).max().unwrap_or(0));
414            }
415        });
416        top
417    }
418}
419
420/// Split the path into its parent path and the child index, or `None` at the root.
421fn split_parent_of(path: &super::tree::NodePath) -> Option<(super::tree::NodePath, usize)> {
422    let (&ix, parent) = path.split_last()?;
423    Some((parent.iter().copied().collect(), ix))
424}
425
426#[cfg(test)]
427mod tests {
428    use super::super::*;
429    use crate::Placement;
430    use gpui::{Axis, Bounds, Pixels, point, px, size};
431
432    fn panel(n: u64) -> PanelId {
433        PanelId::from_u64(n)
434    }
435
436    fn tree_with_one_group() -> (PaneTree, NodeId) {
437        let mut tree = PaneTree::new(RootKind::Split);
438        let tabs = tree.push_tabs_for_test(tree.root().id(), vec![panel(1)]);
439        tree.normalize();
440        (tree, tabs)
441    }
442
443    #[test]
444    fn inserting_into_a_tab_group_appends_and_can_activate() {
445        let (mut tree, tabs) = tree_with_one_group();
446        let result = tree.insert_panel(
447            panel(2),
448            InsertTarget::Tabs {
449                node: tabs,
450                ix: None,
451                activate: true,
452            },
453        );
454
455        assert!(result.changed());
456        let PaneRef::Tabs { panels, active_ix } = tree.find_node(tabs).unwrap().kind() else {
457            panic!()
458        };
459        assert_eq!(panels, [panel(1), panel(2)]);
460        assert_eq!(active_ix, 1, "the inserted panel becomes the displayed one");
461    }
462
463    #[test]
464    fn a_background_insert_leaves_the_active_panel_alone() {
465        let (mut tree, tabs) = tree_with_one_group();
466        let result = tree.insert_panel(
467            panel(2),
468            InsertTarget::Tabs {
469                node: tabs,
470                ix: None,
471                activate: false,
472            },
473        );
474
475        assert!(result.changed());
476        let PaneRef::Tabs { panels, active_ix } = tree.find_node(tabs).unwrap().kind() else {
477            panic!()
478        };
479        assert_eq!(panels, [panel(1), panel(2)]);
480        assert_eq!(active_ix, 0, "the displayed panel is left alone");
481    }
482
483    #[test]
484    fn removing_the_last_panel_collapses_the_group_and_reports_it() {
485        let (mut tree, tabs) = tree_with_one_group();
486        let result = tree.remove_panel(panel(1));
487
488        assert!(result.changed());
489        assert!(!tree.contains_panel(panel(1)), "the panel left the tree");
490        assert!(tree.find_node(tabs).is_none(), "its empty group collapsed");
491        assert!(
492            matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.is_empty())
493        );
494    }
495
496    #[test]
497    fn splitting_creates_a_sibling_group_on_the_requested_side() {
498        let (mut tree, tabs) = tree_with_one_group();
499        let result = tree.split(tabs, panel(2), Placement::Right, Some(px(240.)));
500
501        assert!(result.changed());
502        let PaneRef::Split {
503            axis,
504            children,
505            sizes,
506        } = tree.root().kind()
507        else {
508            panic!()
509        };
510        assert_eq!(axis, Axis::Horizontal);
511        assert_eq!(children.len(), 2);
512        assert_eq!(sizes[1], Some(px(240.)));
513
514        let PaneRef::Tabs { panels, .. } = children[0].kind() else {
515            panic!()
516        };
517        assert_eq!(panels, [panel(1)]);
518        let PaneRef::Tabs { panels, .. } = children[1].kind() else {
519            panic!()
520        };
521        assert_eq!(panels, [panel(2)]);
522    }
523
524    #[test]
525    fn splitting_left_puts_the_new_group_first() {
526        let (mut tree, tabs) = tree_with_one_group();
527        tree.split(tabs, panel(2), Placement::Left, None);
528
529        let PaneRef::Split { children, .. } = tree.root().kind() else {
530            panic!()
531        };
532        let PaneRef::Tabs { panels, .. } = children[0].kind() else {
533            panic!()
534        };
535        assert_eq!(panels, [panel(2)]);
536    }
537
538    #[test]
539    fn splitting_across_the_parent_axis_nests_a_new_split() {
540        let (mut tree, tabs) = tree_with_one_group();
541        tree.push_tabs_for_test(tree.root().id(), vec![panel(9)]);
542        tree.normalize();
543
544        tree.split(tabs, panel(2), Placement::Bottom, None);
545
546        let PaneRef::Split { axis, children, .. } = tree.root().kind() else {
547            panic!()
548        };
549        assert_eq!(axis, Axis::Horizontal);
550        let PaneRef::Split {
551            axis: inner,
552            children: inner_children,
553            ..
554        } = children[0].kind()
555        else {
556            panic!("the split target is wrapped in a vertical split")
557        };
558        assert_eq!(inner, Axis::Vertical);
559        assert_eq!(inner_children.len(), 2);
560
561        // `Bottom` puts the new group after the original target: the
562        // wrapper's first child is still the target, the second is new.
563        let PaneRef::Tabs { panels, .. } = inner_children[0].kind() else {
564            panic!()
565        };
566        assert_eq!(panels, [panel(1)], "the original target stays first");
567        let PaneRef::Tabs { panels, .. } = inner_children[1].kind() else {
568            panic!()
569        };
570        assert_eq!(panels, [panel(2)], "the new group lands second, below");
571    }
572
573    #[test]
574    fn splitting_top_across_the_parent_axis_puts_the_new_group_first() {
575        let (mut tree, tabs) = tree_with_one_group();
576        tree.push_tabs_for_test(tree.root().id(), vec![panel(9)]);
577        tree.normalize();
578
579        tree.split(tabs, panel(2), Placement::Top, None);
580
581        let PaneRef::Split { children, .. } = tree.root().kind() else {
582            panic!()
583        };
584        let PaneRef::Split {
585            axis: inner,
586            children: inner_children,
587            ..
588        } = children[0].kind()
589        else {
590            panic!("the split target is wrapped in a vertical split")
591        };
592        assert_eq!(inner, Axis::Vertical);
593        assert_eq!(inner_children.len(), 2);
594
595        // `Top` is the mirror of `Bottom`: the new group lands first, above
596        // the original target.
597        let PaneRef::Tabs { panels, .. } = inner_children[0].kind() else {
598            panic!()
599        };
600        assert_eq!(panels, [panel(2)], "the new group lands first, above");
601        let PaneRef::Tabs { panels, .. } = inner_children[1].kind() else {
602            panic!()
603        };
604        assert_eq!(panels, [panel(1)], "the original target moves second");
605    }
606
607    #[test]
608    fn the_split_target_keeps_its_node_id_so_its_entity_survives() {
609        let (mut tree, tabs) = tree_with_one_group();
610        tree.split(tabs, panel(2), Placement::Right, None);
611
612        assert!(
613            tree.find_node(tabs).is_some(),
614            "the target group is reused, not rebuilt"
615        );
616    }
617
618    #[test]
619    fn moving_a_panel_between_groups_preserves_its_identity() {
620        let (mut tree, tabs) = tree_with_one_group();
621        assert!(tree.split(tabs, panel(2), Placement::Right, None).changed());
622        let other = tree
623            .find_panel_node(panel(2))
624            .expect("the split put panel 2 in a group of its own");
625
626        let result = tree.move_panel(
627            panel(1),
628            InsertTarget::Tabs {
629                node: other,
630                ix: None,
631                activate: true,
632            },
633        );
634
635        assert!(result.changed());
636        assert_eq!(
637            tree.panels().collect::<Vec<_>>(),
638            vec![panel(2), panel(1)],
639            "a move is not a removal; both panels are still in the tree"
640        );
641        assert!(
642            tree.find_node(tabs).is_none(),
643            "the emptied group collapses"
644        );
645    }
646
647    #[test]
648    fn a_no_op_edit_reports_no_change() {
649        let (mut tree, tabs) = tree_with_one_group();
650        let result = tree.set_active(tabs, 0);
651        assert!(!result.changed());
652    }
653
654    #[test]
655    fn set_sizes_replaces_a_matching_length_vector() {
656        let (mut tree, tabs) = tree_with_one_group();
657        tree.split(tabs, panel(2), Placement::Right, None);
658        let root = tree.root().id();
659
660        let result = tree.set_sizes(root, vec![Some(px(100.)), Some(px(200.))]);
661
662        assert!(result.changed());
663        let PaneRef::Split { sizes, .. } = tree.root().kind() else {
664            panic!()
665        };
666        assert_eq!(sizes, &[Some(px(100.)), Some(px(200.))]);
667    }
668
669    #[test]
670    fn set_sizes_ignores_a_mismatched_length_vector() {
671        let (mut tree, tabs) = tree_with_one_group();
672        tree.split(tabs, panel(2), Placement::Right, None);
673        let root = tree.root().id();
674        let PaneRef::Split {
675            sizes: before_sizes,
676            ..
677        } = tree.root().kind()
678        else {
679            panic!()
680        };
681        let before_sizes = before_sizes.to_vec();
682
683        // The split has 2 children; hand it 3 sizes.
684        let result = tree.set_sizes(root, vec![Some(px(10.)), Some(px(20.)), Some(px(30.))]);
685
686        assert!(
687            !result.changed(),
688            "a mismatched vector must not report a change"
689        );
690        let PaneRef::Split { sizes, .. } = tree.root().kind() else {
691            panic!()
692        };
693        assert_eq!(
694            sizes,
695            before_sizes.as_slice(),
696            "the mismatched vector is rejected"
697        );
698    }
699
700    fn tile_bounds(x: f32) -> Bounds<Pixels> {
701        Bounds {
702            origin: point(px(x), px(0.)),
703            size: size(px(10.), px(10.)),
704        }
705    }
706
707    #[test]
708    fn inserting_a_tile_places_it_at_the_given_bounds_on_top() {
709        let mut tree = PaneTree::new(RootKind::Any);
710        let canvas = tree.set_root_tiles_for_test(vec![
711            TilePanel::new(panel(1), tile_bounds(0.)).with_z_index(4),
712        ]);
713
714        let result = tree.insert_panel(
715            panel(2),
716            InsertTarget::Tile {
717                node: canvas,
718                bounds: tile_bounds(70.),
719            },
720        );
721
722        assert!(result.changed());
723        let PaneRef::Tiles { panels } = tree.root().kind() else {
724            panic!("the canvas stays a canvas rather than being split")
725        };
726        assert_eq!(panels.len(), 2);
727        let added = panels.iter().find(|tile| tile.panel() == panel(2)).unwrap();
728        assert_eq!(added.bounds(), tile_bounds(70.), "the bounds are honoured");
729        assert!(
730            added.z_index() > panels[0].z_index(),
731            "a new tile lands on top of the ones already there"
732        );
733    }
734
735    #[test]
736    fn set_tile_bounds_moves_one_tile_and_leaves_its_peers_alone() {
737        let mut tree = PaneTree::new(RootKind::Any);
738        tree.set_root_tiles_for_test(vec![
739            TilePanel::new(panel(1), tile_bounds(0.)).with_z_index(1),
740            TilePanel::new(panel(2), tile_bounds(40.)).with_z_index(2),
741        ]);
742
743        let result = tree.set_tile_bounds(panel(1), tile_bounds(90.));
744
745        assert!(result.changed());
746        let PaneRef::Tiles { panels } = tree.root().kind() else {
747            panic!()
748        };
749        let moved = panels.iter().find(|tile| tile.panel() == panel(1)).unwrap();
750        let other = panels.iter().find(|tile| tile.panel() == panel(2)).unwrap();
751        assert_eq!(moved.bounds(), tile_bounds(90.));
752        assert_eq!(
753            moved.z_index(),
754            1,
755            "moving a tile does not raise it; that is `bring_to_front`'s job"
756        );
757        assert_eq!(other.bounds(), tile_bounds(40.), "its peer does not move");
758    }
759
760    #[test]
761    fn bring_to_front_raises_the_tile_above_its_peers() {
762        let mut tree = PaneTree::new(RootKind::Any);
763        let bounds = Bounds {
764            origin: point(px(0.), px(0.)),
765            size: size(px(10.), px(10.)),
766        };
767        tree.set_root_tiles_for_test(vec![
768            TilePanel::new(panel(1), bounds).with_z_index(0),
769            TilePanel::new(panel(2), bounds).with_z_index(1),
770        ]);
771
772        tree.bring_to_front(panel(1));
773
774        let PaneRef::Tiles { panels } = tree.root().kind() else {
775            panic!()
776        };
777        let raised = panels.iter().find(|p| p.panel() == panel(1)).unwrap();
778        let other = panels.iter().find(|p| p.panel() == panel(2)).unwrap();
779        assert!(raised.z_index() > other.z_index());
780    }
781
782    #[test]
783    fn every_edit_leaves_the_tree_normalized() {
784        let (mut tree, tabs) = tree_with_one_group();
785        tree.split(tabs, panel(2), Placement::Bottom, None);
786        tree.insert_panel(
787            panel(3),
788            InsertTarget::Tabs {
789                node: tabs,
790                ix: None,
791                activate: false,
792            },
793        );
794        tree.remove_panel(panel(2));
795        tree.remove_panel(panel(3));
796
797        assert!(tree.is_normalized());
798    }
799}