1use 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
11pub 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 pub fn h_split() -> Self {
42 Self::split(Axis::Horizontal)
43 }
44
45 pub fn v_split() -> Self {
47 Self::split(Axis::Vertical)
48 }
49
50 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 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 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 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 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 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 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 #[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}