Skip to main content

gpui_base/dock/layout/
edit.rs

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