Skip to main content

gpui_base/dock/layout/
builder.rs

1//! Describing a layout without building it.
2
3use std::sync::Arc;
4
5use gpui::{App, Axis, Entity, Pixels};
6
7use super::node::{NodeKind, PaneNode, PanelId};
8use super::tree::{PaneTree, RootKind};
9use crate::dock::panel::{Panel, PanelView};
10
11/// Describes a layout without constructing any entity.
12///
13/// Building a layout used to require `window` and `cx` because every node was
14/// an entity. Now a layout is data, and `DockArea` reconciles it into entities
15/// when it is installed.
16///
17/// Misuse — a panel added to a split, a child container added to a tab group,
18/// an active index on anything but a tab group — trips a `debug_assert!` and
19/// is otherwise ignored, matching how the old `DockItem::active_index` guarded
20/// itself. A type-state builder would reject the same mistakes at compile
21/// time, but it needs four marker types and leaks into every consumer
22/// signature; the runtime assertion is proportionate for a builder whose
23/// misuse shows up on the first debug run.
24pub struct DockLayout {
25    kind: BuilderKind,
26}
27
28enum BuilderKind {
29    Split {
30        axis: Axis,
31        children: Vec<(DockLayout, Option<Pixels>)>,
32    },
33    Tabs {
34        panels: Vec<(PanelId, Arc<dyn PanelView>)>,
35        active_ix: usize,
36    },
37}
38
39impl DockLayout {
40    /// A split whose children sit side by side.
41    pub fn h_split() -> Self {
42        Self::split(Axis::Horizontal)
43    }
44
45    /// A split whose children stack.
46    pub fn v_split() -> Self {
47        Self::split(Axis::Vertical)
48    }
49
50    /// A tab group: panels stacked one at a time, selected from a tab bar.
51    pub fn tabs() -> Self {
52        Self {
53            kind: BuilderKind::Tabs {
54                panels: Vec::new(),
55                active_ix: 0,
56            },
57        }
58    }
59
60    fn split(axis: Axis) -> Self {
61        Self {
62            kind: BuilderKind::Split {
63                axis,
64                children: Vec::new(),
65            },
66        }
67    }
68
69    /// Add a child container to a split. `size` is the child's slot along the
70    /// split's axis; `None` leaves it unconstrained.
71    pub fn child(mut self, child: DockLayout, size: Option<Pixels>) -> Self {
72        debug_assert!(
73            matches!(self.kind, BuilderKind::Split { .. }),
74            "child() is only valid on h_split() or v_split()"
75        );
76        if let BuilderKind::Split { children, .. } = &mut self.kind {
77            children.push((child, size));
78        }
79        self
80    }
81
82    /// Add a panel to a tab group.
83    pub fn panel<P: Panel>(mut self, panel: Entity<P>) -> Self {
84        debug_assert!(
85            matches!(self.kind, BuilderKind::Tabs { .. }),
86            "panel() is only valid on tabs()"
87        );
88        if let BuilderKind::Tabs { panels, .. } = &mut self.kind {
89            panels.push((PanelId::from(panel.entity_id()), Arc::new(panel)));
90        }
91        self
92    }
93
94    /// Add an already-wrapped panel handle to a tab group.
95    ///
96    /// The companion to [`Self::panel`], for a layer that hands base its own
97    /// concrete handle — see [`PanelView::as_any`] — rather than a bare
98    /// entity. `cx` is here because the id has to come from
99    /// [`PanelView::panel_id`]: unlike [`Self::panel`], there is no entity in
100    /// hand to take it from.
101    pub fn panel_view(mut self, panel: Arc<dyn PanelView>, cx: &App) -> Self {
102        debug_assert!(
103            matches!(self.kind, BuilderKind::Tabs { .. }),
104            "panel_view() is only valid on tabs()"
105        );
106        if let BuilderKind::Tabs { panels, .. } = &mut self.kind {
107            panels.push((panel.panel_id(cx), panel));
108        }
109        self
110    }
111
112    /// Which tab is displayed. Out-of-range values are clamped by
113    /// `normalize` once the layout is installed.
114    pub fn active_index(mut self, ix: usize) -> Self {
115        debug_assert!(
116            matches!(self.kind, BuilderKind::Tabs { .. }),
117            "active_index() is only valid on tabs()"
118        );
119        if let BuilderKind::Tabs { active_ix, .. } = &mut self.kind {
120            *active_ix = ix;
121        }
122        self
123    }
124
125    /// Lower into a tree plus the panel views the area must register.
126    ///
127    /// The views come back paired with the ids the tree was built from rather
128    /// than as bare views: recovering an id from a view means calling
129    /// [`PanelView::panel_id`], and nothing here would notice if a `PanelView`
130    /// implementation ever answered with something other than its entity id.
131    pub(crate) fn build(
132        self,
133        tree: &mut PaneTree,
134    ) -> (PaneNode, Vec<(PanelId, Arc<dyn PanelView>)>) {
135        let mut panels = Vec::new();
136        let node = self.build_node(tree, &mut panels);
137        (node, panels)
138    }
139
140    fn build_node(
141        self,
142        tree: &mut PaneTree,
143        collected: &mut Vec<(PanelId, Arc<dyn PanelView>)>,
144    ) -> PaneNode {
145        let id = tree.allocate_node_id();
146        match self.kind {
147            BuilderKind::Split { axis, children } => {
148                let mut nodes = Vec::with_capacity(children.len());
149                let mut sizes = Vec::with_capacity(children.len());
150                for (child, size) in children {
151                    nodes.push(child.build_node(tree, collected));
152                    sizes.push(size);
153                }
154                PaneNode::new(
155                    id,
156                    NodeKind::Split {
157                        axis,
158                        children: nodes,
159                        sizes,
160                    },
161                )
162            }
163            BuilderKind::Tabs { panels, active_ix } => {
164                let ids = panels.iter().map(|(id, _)| *id).collect();
165                collected.extend(panels);
166                PaneNode::new(
167                    id,
168                    NodeKind::Tabs {
169                        panels: ids,
170                        active_ix,
171                    },
172                )
173            }
174        }
175    }
176}
177
178impl PaneTree {
179    /// Build a whole tree from a described layout.
180    ///
181    /// The `RootKind::Split` wrap mirrors the one in
182    /// [`PaneTree::from_state`](crate::dock::PaneTree::from_state): a
183    /// center whose described root is a tab group still has to serialize as a
184    /// `StackPanel`.
185    pub(crate) fn from_layout(
186        layout: DockLayout,
187        root_kind: RootKind,
188    ) -> (Self, Vec<(PanelId, Arc<dyn PanelView>)>) {
189        let mut tree = PaneTree::new(root_kind);
190        let (root, panels) = layout.build(&mut tree);
191
192        let root = match (root_kind, root.kind_ref()) {
193            (RootKind::Split, NodeKind::Split { .. }) | (RootKind::Any, _) => root,
194            (RootKind::Split, _) => {
195                let id = tree.allocate_node_id();
196                PaneNode::new(
197                    id,
198                    NodeKind::Split {
199                        axis: Axis::Horizontal,
200                        children: vec![root],
201                        sizes: vec![None],
202                    },
203                )
204            }
205        };
206
207        tree.replace_root(root);
208        tree.normalize();
209        (tree, panels)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use gpui::{TestAppContext, px};
216
217    use super::super::PaneRef;
218    use super::*;
219    use crate::dock::test_support::TestPanel;
220
221    #[gpui::test]
222    fn a_described_split_lowers_to_a_split_of_tab_groups(cx: &mut TestAppContext) {
223        let (tree, panels) = cx.update(|cx| {
224            let alpha = TestPanel::new("Alpha", cx);
225            let beta = TestPanel::new("Beta", cx);
226            PaneTree::from_layout(
227                DockLayout::h_split()
228                    .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
229                    .child(DockLayout::tabs().panel(beta), None),
230                RootKind::Split,
231            )
232        });
233
234        assert_eq!(panels.len(), 2);
235        let PaneRef::Split {
236            axis,
237            children,
238            sizes,
239        } = tree.root().kind()
240        else {
241            panic!("expected a split root");
242        };
243        assert_eq!(axis, gpui::Axis::Horizontal);
244        assert_eq!(children.len(), 2);
245        assert_eq!(sizes, &[Some(px(300.)), None]);
246        assert!(matches!(children[0].kind(), PaneRef::Tabs { panels, .. } if panels.len() == 1));
247    }
248
249    /// Ported from `DockItem::split_with_sizes_adds_each_child_once`, which
250    /// guarded a `StackPanel` that once added every child twice.
251    #[gpui::test]
252    fn split_with_sizes_adds_each_child_once(cx: &mut TestAppContext) {
253        let (tree, _) = cx.update(|cx| {
254            let alpha = TestPanel::new("Alpha", cx);
255            let beta = TestPanel::new("Beta", cx);
256            PaneTree::from_layout(
257                DockLayout::h_split()
258                    .child(DockLayout::tabs().panel(alpha), None)
259                    .child(DockLayout::tabs().panel(beta), None),
260                RootKind::Split,
261            )
262        });
263
264        let PaneRef::Split {
265            children, sizes, ..
266        } = tree.root().kind()
267        else {
268            panic!("expected a split root");
269        };
270        assert_eq!(
271            children.len(),
272            2,
273            "each described child appears exactly once"
274        );
275        assert_eq!(sizes.len(), children.len());
276        assert_ne!(children[0].id(), children[1].id());
277    }
278
279    #[gpui::test]
280    fn a_bare_tab_group_is_wrapped_for_a_split_root(cx: &mut TestAppContext) {
281        let (tree, _) = cx.update(|cx| {
282            let alpha = TestPanel::new("Alpha", cx);
283            PaneTree::from_layout(DockLayout::tabs().panel(alpha), RootKind::Split)
284        });
285
286        assert!(matches!(tree.root().kind(), PaneRef::Split { .. }));
287    }
288
289    #[gpui::test]
290    fn an_active_index_selects_the_displayed_tab(cx: &mut TestAppContext) {
291        let (tree, _) = cx.update(|cx| {
292            let alpha = TestPanel::new("Alpha", cx);
293            let beta = TestPanel::new("Beta", cx);
294            PaneTree::from_layout(
295                DockLayout::tabs().panel(alpha).panel(beta).active_index(1),
296                RootKind::Any,
297            )
298        });
299
300        let PaneRef::Tabs { active_ix, .. } = tree.root().kind() else {
301            panic!("expected a tab group root");
302        };
303        assert_eq!(active_ix, 1);
304    }
305}