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, Bounds, Entity, Pixels};
6
7use super::node::{NodeKind, PaneNode, PanelId, TilePanel};
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    Tiles {
38        panels: Vec<(PanelId, Arc<dyn PanelView>, Bounds<Pixels>)>,
39    },
40}
41
42impl DockLayout {
43    /// A split whose children sit side by side.
44    pub fn h_split() -> Self {
45        Self::split(Axis::Horizontal)
46    }
47
48    /// A split whose children stack.
49    pub fn v_split() -> Self {
50        Self::split(Axis::Vertical)
51    }
52
53    /// A tab group: panels stacked one at a time, selected from a tab bar.
54    pub fn tabs() -> Self {
55        Self {
56            kind: BuilderKind::Tabs {
57                panels: Vec::new(),
58                active_ix: 0,
59            },
60        }
61    }
62
63    /// A tiles canvas: panels placed at free coordinates, each dragged and
64    /// resized on its own.
65    pub fn tiles() -> Self {
66        Self {
67            kind: BuilderKind::Tiles { panels: Vec::new() },
68        }
69    }
70
71    fn split(axis: Axis) -> Self {
72        Self {
73            kind: BuilderKind::Split {
74                axis,
75                children: Vec::new(),
76            },
77        }
78    }
79
80    /// Add a child container to a split. `size` is the child's slot along the
81    /// split's axis; `None` leaves it unconstrained.
82    pub fn child(mut self, child: DockLayout, size: Option<Pixels>) -> Self {
83        debug_assert!(
84            matches!(self.kind, BuilderKind::Split { .. }),
85            "child() is only valid on h_split() or v_split()"
86        );
87        if let BuilderKind::Split { children, .. } = &mut self.kind {
88            children.push((child, size));
89        }
90        self
91    }
92
93    /// Add a panel to a tab group.
94    pub fn panel<P: Panel>(mut self, panel: Entity<P>) -> Self {
95        debug_assert!(
96            matches!(self.kind, BuilderKind::Tabs { .. }),
97            "panel() is only valid on tabs()"
98        );
99        if let BuilderKind::Tabs { panels, .. } = &mut self.kind {
100            panels.push((PanelId::from(panel.entity_id()), Arc::new(panel)));
101        }
102        self
103    }
104
105    /// Add an already-wrapped panel handle to a tab group.
106    ///
107    /// The companion to [`Self::panel`], for a layer that hands base its own
108    /// concrete handle — see [`PanelView::as_any`] — rather than a bare
109    /// entity. `cx` is here because the id has to come from
110    /// [`PanelView::panel_id`]: unlike [`Self::panel`], there is no entity in
111    /// hand to take it from.
112    pub fn panel_view(mut self, panel: Arc<dyn PanelView>, cx: &App) -> Self {
113        debug_assert!(
114            matches!(self.kind, BuilderKind::Tabs { .. }),
115            "panel_view() is only valid on tabs()"
116        );
117        if let BuilderKind::Tabs { panels, .. } = &mut self.kind {
118            panels.push((panel.panel_id(cx), panel));
119        }
120        self
121    }
122
123    /// Place a panel on a tiles canvas.
124    pub fn tile<P: Panel>(mut self, panel: Entity<P>, bounds: Bounds<Pixels>) -> Self {
125        debug_assert!(
126            matches!(self.kind, BuilderKind::Tiles { .. }),
127            "tile() is only valid on tiles()"
128        );
129        if let BuilderKind::Tiles { panels } = &mut self.kind {
130            panels.push((PanelId::from(panel.entity_id()), Arc::new(panel), bounds));
131        }
132        self
133    }
134
135    /// Place an already-wrapped panel handle on a tiles canvas. The companion
136    /// to [`Self::tile`], for the same reason [`Self::panel_view`] is the
137    /// companion to [`Self::panel`].
138    pub fn tile_view(
139        mut self,
140        panel: Arc<dyn PanelView>,
141        bounds: Bounds<Pixels>,
142        cx: &App,
143    ) -> Self {
144        debug_assert!(
145            matches!(self.kind, BuilderKind::Tiles { .. }),
146            "tile_view() is only valid on tiles()"
147        );
148        if let BuilderKind::Tiles { panels } = &mut self.kind {
149            panels.push((panel.panel_id(cx), panel, bounds));
150        }
151        self
152    }
153
154    /// Which tab is displayed. Out-of-range values are clamped by
155    /// `normalize` once the layout is installed.
156    pub fn active_index(mut self, ix: usize) -> Self {
157        debug_assert!(
158            matches!(self.kind, BuilderKind::Tabs { .. }),
159            "active_index() is only valid on tabs()"
160        );
161        if let BuilderKind::Tabs { active_ix, .. } = &mut self.kind {
162            *active_ix = ix;
163        }
164        self
165    }
166
167    /// Lower into a tree plus the panel views the area must register.
168    ///
169    /// The views come back paired with the ids the tree was built from rather
170    /// than as bare views: recovering an id from a view means calling
171    /// [`PanelView::panel_id`], and nothing here would notice if a `PanelView`
172    /// implementation ever answered with something other than its entity id.
173    pub(crate) fn build(
174        self,
175        tree: &mut PaneTree,
176    ) -> (PaneNode, Vec<(PanelId, Arc<dyn PanelView>)>) {
177        let mut panels = Vec::new();
178        let node = self.build_node(tree, &mut panels);
179        (node, panels)
180    }
181
182    fn build_node(
183        self,
184        tree: &mut PaneTree,
185        collected: &mut Vec<(PanelId, Arc<dyn PanelView>)>,
186    ) -> PaneNode {
187        let id = tree.allocate_node_id();
188        match self.kind {
189            BuilderKind::Split { axis, children } => {
190                let mut nodes = Vec::with_capacity(children.len());
191                let mut sizes = Vec::with_capacity(children.len());
192                for (child, size) in children {
193                    nodes.push(child.build_node(tree, collected));
194                    sizes.push(size);
195                }
196                PaneNode::new(
197                    id,
198                    NodeKind::Split {
199                        axis,
200                        children: nodes,
201                        sizes,
202                    },
203                )
204            }
205            BuilderKind::Tabs { panels, active_ix } => {
206                let ids = panels.iter().map(|(id, _)| *id).collect();
207                collected.extend(panels);
208                PaneNode::new(
209                    id,
210                    NodeKind::Tabs {
211                        panels: ids,
212                        active_ix,
213                    },
214                )
215            }
216            BuilderKind::Tiles { panels } => {
217                let tiles = panels
218                    .iter()
219                    .enumerate()
220                    .map(|(ix, (panel, _, bounds))| {
221                        TilePanel::new(*panel, *bounds).with_z_index(ix)
222                    })
223                    .collect();
224                collected.extend(panels.into_iter().map(|(id, view, _)| (id, view)));
225                PaneNode::new(id, NodeKind::Tiles { panels: tiles })
226            }
227        }
228    }
229}
230
231impl PaneTree {
232    /// Build a whole tree from a described layout.
233    ///
234    /// The `RootKind::Split` wrap mirrors the one in
235    /// [`PaneTree::from_state`](crate::dock::PaneTree::from_state): a
236    /// center whose described root is a tab group or a tiles canvas still has
237    /// to serialize as a `StackPanel`.
238    pub(crate) fn from_layout(
239        layout: DockLayout,
240        root_kind: RootKind,
241    ) -> (Self, Vec<(PanelId, Arc<dyn PanelView>)>) {
242        let mut tree = PaneTree::new(root_kind);
243        let (root, panels) = layout.build(&mut tree);
244
245        let root = match (root_kind, root.kind_ref()) {
246            (RootKind::Split, NodeKind::Split { .. }) | (RootKind::Any, _) => root,
247            (RootKind::Split, _) => {
248                let id = tree.allocate_node_id();
249                PaneNode::new(
250                    id,
251                    NodeKind::Split {
252                        axis: Axis::Horizontal,
253                        children: vec![root],
254                        sizes: vec![None],
255                    },
256                )
257            }
258        };
259
260        tree.replace_root(root);
261        tree.normalize();
262        (tree, panels)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use gpui::{TestAppContext, px};
269
270    use super::super::PaneRef;
271    use super::*;
272    use crate::dock::test_support::TestPanel;
273
274    #[gpui::test]
275    fn a_described_split_lowers_to_a_split_of_tab_groups(cx: &mut TestAppContext) {
276        let (tree, panels) = cx.update(|cx| {
277            let alpha = TestPanel::new("Alpha", cx);
278            let beta = TestPanel::new("Beta", cx);
279            PaneTree::from_layout(
280                DockLayout::h_split()
281                    .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
282                    .child(DockLayout::tabs().panel(beta), None),
283                RootKind::Split,
284            )
285        });
286
287        assert_eq!(panels.len(), 2);
288        let PaneRef::Split {
289            axis,
290            children,
291            sizes,
292        } = tree.root().kind()
293        else {
294            panic!("expected a split root");
295        };
296        assert_eq!(axis, gpui::Axis::Horizontal);
297        assert_eq!(children.len(), 2);
298        assert_eq!(sizes, &[Some(px(300.)), None]);
299        assert!(matches!(children[0].kind(), PaneRef::Tabs { panels, .. } if panels.len() == 1));
300    }
301
302    /// Ported from `DockItem::split_with_sizes_adds_each_child_once`, which
303    /// guarded a `StackPanel` that once added every child twice.
304    #[gpui::test]
305    fn split_with_sizes_adds_each_child_once(cx: &mut TestAppContext) {
306        let (tree, _) = cx.update(|cx| {
307            let alpha = TestPanel::new("Alpha", cx);
308            let beta = TestPanel::new("Beta", cx);
309            PaneTree::from_layout(
310                DockLayout::h_split()
311                    .child(DockLayout::tabs().panel(alpha), None)
312                    .child(DockLayout::tabs().panel(beta), None),
313                RootKind::Split,
314            )
315        });
316
317        let PaneRef::Split {
318            children, sizes, ..
319        } = tree.root().kind()
320        else {
321            panic!("expected a split root");
322        };
323        assert_eq!(
324            children.len(),
325            2,
326            "each described child appears exactly once"
327        );
328        assert_eq!(sizes.len(), children.len());
329        assert_ne!(children[0].id(), children[1].id());
330    }
331
332    #[gpui::test]
333    fn a_bare_tab_group_is_wrapped_for_a_split_root(cx: &mut TestAppContext) {
334        let (tree, _) = cx.update(|cx| {
335            let alpha = TestPanel::new("Alpha", cx);
336            PaneTree::from_layout(DockLayout::tabs().panel(alpha), RootKind::Split)
337        });
338
339        assert!(matches!(tree.root().kind(), PaneRef::Split { .. }));
340    }
341
342    #[gpui::test]
343    fn an_active_index_selects_the_displayed_tab(cx: &mut TestAppContext) {
344        let (tree, _) = cx.update(|cx| {
345            let alpha = TestPanel::new("Alpha", cx);
346            let beta = TestPanel::new("Beta", cx);
347            PaneTree::from_layout(
348                DockLayout::tabs().panel(alpha).panel(beta).active_index(1),
349                RootKind::Any,
350            )
351        });
352
353        let PaneRef::Tabs { active_ix, .. } = tree.root().kind() else {
354            panic!("expected a tab group root");
355        };
356        assert_eq!(active_ix, 1);
357    }
358
359    #[gpui::test]
360    fn tiles_are_stacked_in_the_order_they_are_placed(cx: &mut TestAppContext) {
361        let bounds = Bounds {
362            origin: gpui::point(px(0.), px(0.)),
363            size: gpui::size(px(100.), px(100.)),
364        };
365        let (tree, _) = cx.update(|cx| {
366            let alpha = TestPanel::new("Alpha", cx);
367            let beta = TestPanel::new("Beta", cx);
368            PaneTree::from_layout(
369                DockLayout::tiles().tile(alpha, bounds).tile(beta, bounds),
370                RootKind::Any,
371            )
372        });
373
374        let PaneRef::Tiles { panels } = tree.root().kind() else {
375            panic!("expected a tiles root");
376        };
377        assert_eq!(panels[0].z_index(), 0);
378        assert_eq!(panels[1].z_index(), 1);
379    }
380}