1use gpui::{Axis, Pixels};
2
3use super::layout::{NodeKind, PaneNode, PaneRef, PaneTree, PanelId, RootKind};
4use super::state::{PanelInfo, PanelState};
5
6pub(crate) const STACK_PANEL_NAME: &str = "StackPanel";
9pub(crate) const TAB_PANEL_NAME: &str = "TabPanel";
10
11pub trait PanelSource {
16 fn panel_name(&self, id: PanelId) -> &'static str;
17 fn is_visible(&self, id: PanelId) -> bool;
18 fn dump(&self, id: PanelId) -> PanelState;
19}
20
21impl PaneTree {
22 pub fn to_state(&self, source: &dyn PanelSource) -> PanelState {
23 node_to_state(self.root(), source)
24 }
25}
26
27fn node_to_state(node: &PaneNode, source: &dyn PanelSource) -> PanelState {
28 match node.kind() {
29 PaneRef::Split {
30 axis,
31 children,
32 sizes,
33 } => PanelState {
34 panel_name: STACK_PANEL_NAME.to_string(),
35 children: children
36 .iter()
37 .map(|child| node_to_state(child, source))
38 .collect(),
39 info: PanelInfo::stack(
51 sizes.iter().map(|size| size.unwrap_or_default()).collect(),
52 axis,
53 ),
54 },
55 PaneRef::Tabs { panels, active_ix } => PanelState {
56 panel_name: TAB_PANEL_NAME.to_string(),
57 children: panels.iter().map(|panel| source.dump(*panel)).collect(),
58 info: PanelInfo::tabs(active_ix),
61 },
62 }
63}
64
65pub trait PanelBuilder {
73 fn build(&mut self, state: &PanelState, info: &PanelInfo) -> PanelId;
74}
75
76impl PaneTree {
77 pub fn from_state(
93 state: &PanelState,
94 root_kind: RootKind,
95 builder: &mut dyn PanelBuilder,
96 ) -> Self {
97 let mut tree = PaneTree::new(root_kind);
98 let root = build_node(&mut tree, state, builder);
99
100 let root = match (root_kind, &root) {
101 (RootKind::Split, node) if !matches!(node.kind_ref(), NodeKind::Split { .. }) => {
102 let id = tree.allocate_node_id();
103 PaneNode::new(
104 id,
105 NodeKind::Split {
106 axis: Axis::Horizontal,
107 children: vec![node.clone()],
108 sizes: vec![None],
109 },
110 )
111 }
112 _ => root,
113 };
114
115 tree.replace_root(root);
116 tree.normalize();
117 tree
118 }
119}
120
121fn build_node(tree: &mut PaneTree, state: &PanelState, builder: &mut dyn PanelBuilder) -> PaneNode {
122 let id = tree.allocate_node_id();
123
124 match &state.info {
125 PanelInfo::Stack { sizes, axis } => {
126 let axis = if *axis == 0 {
127 Axis::Horizontal
128 } else {
129 Axis::Vertical
130 };
131 let children: Vec<PaneNode> = state
132 .children
133 .iter()
134 .map(|child| build_node(tree, child, builder))
135 .collect();
136 let sizes = (0..children.len())
137 .map(|ix| sizes.get(ix).copied().filter(|size| *size > Pixels::ZERO))
138 .collect();
139 PaneNode::new(
140 id,
141 NodeKind::Split {
142 axis,
143 children,
144 sizes,
145 },
146 )
147 }
148 PanelInfo::Tabs { active_index } => {
149 let panels = collect_tab_panels(&state.children, builder);
150 PaneNode::new(
151 id,
152 NodeKind::Tabs {
153 panels,
154 active_ix: *active_index,
155 },
156 )
157 }
158 PanelInfo::Panel(_) => {
159 let panels = if state.panel_name == TAB_PANEL_NAME {
162 Vec::new()
163 } else {
164 vec![builder.build(state, &state.info)]
165 };
166 PaneNode::new(
167 id,
168 NodeKind::Tabs {
169 panels,
170 active_ix: 0,
171 },
172 )
173 }
174 }
175}
176
177fn collect_tab_panels(children: &[PanelState], builder: &mut dyn PanelBuilder) -> Vec<PanelId> {
179 children
180 .iter()
181 .flat_map(|child| match &child.info {
182 PanelInfo::Tabs { .. } => collect_tab_panels(&child.children, builder),
183 PanelInfo::Panel(_) if child.panel_name == TAB_PANEL_NAME => Vec::new(),
184 _ => vec![builder.build(child, &child.info)],
185 })
186 .collect()
187}
188
189#[cfg(test)]
190mod tests {
191 use gpui::px;
192
193 use super::super::layout::RootKind;
194 use super::super::state::DockAreaState;
195 use super::*;
196
197 struct FakePanels(Vec<(PanelId, &'static str)>);
200
201 impl PanelSource for FakePanels {
202 fn panel_name(&self, id: PanelId) -> &'static str {
203 self.0
204 .iter()
205 .find(|(p, _)| *p == id)
206 .map(|(_, n)| *n)
207 .unwrap_or("Unknown")
208 }
209 fn is_visible(&self, _: PanelId) -> bool {
210 true
211 }
212 fn dump(&self, id: PanelId) -> PanelState {
213 PanelState {
214 panel_name: self.panel_name(id).to_string(),
215 children: Vec::new(),
216 info: PanelInfo::panel(serde_json::Value::Null),
217 }
218 }
219 }
220
221 #[test]
222 fn a_split_serializes_as_a_stack_panel() {
223 let mut tree = PaneTree::new(RootKind::Split);
224 tree.push_tabs_for_test(tree.root().id(), vec![PanelId::from_u64(1)]);
225 tree.push_tabs_for_test(tree.root().id(), vec![PanelId::from_u64(2)]);
226 tree.normalize();
227
228 let source = FakePanels(vec![
229 (PanelId::from_u64(1), "Alpha"),
230 (PanelId::from_u64(2), "Beta"),
231 ]);
232 let state = tree.to_state(&source);
233
234 assert_eq!(state.panel_name, "StackPanel");
235 let PanelInfo::Stack { sizes, .. } = &state.info else {
236 panic!("expected Stack info");
237 };
238 assert_eq!(sizes, &vec![px(0.), px(0.)]);
241 assert_eq!(state.children[0].panel_name, "TabPanel");
242 assert_eq!(state.children[0].children[0].panel_name, "Alpha");
243 }
244
245 #[test]
246 fn an_unresolved_slot_size_serializes_as_the_zero_sentinel() {
247 let mut tree = PaneTree::new(RootKind::Split);
248 let root = tree.root().id();
249 tree.push_sized_tabs_for_test(root, vec![PanelId::from_u64(1)], Some(px(120.)));
250 tree.push_sized_tabs_for_test(root, vec![PanelId::from_u64(2)], None);
251 tree.normalize();
252
253 let source = FakePanels(vec![
254 (PanelId::from_u64(1), "Alpha"),
255 (PanelId::from_u64(2), "Beta"),
256 ]);
257 let state = tree.to_state(&source);
258
259 let PanelInfo::Stack { sizes, .. } = state.info else {
260 panic!("expected Stack info");
261 };
262 assert_eq!(sizes, vec![px(120.), px(0.)]);
265 }
266
267 #[test]
268 fn an_empty_tab_group_serializes_as_tabs_not_as_a_panel() {
269 let mut tree = PaneTree::new(RootKind::Any);
270 tree.set_root_tabs_for_test(vec![], 0);
271
272 let state = tree.to_state(&FakePanels(vec![]));
273
274 assert_eq!(state.panel_name, "TabPanel");
275 assert!(
276 matches!(state.info, PanelInfo::Tabs { active_index: 0 }),
277 "the old writer emitted PanelInfo::Panel here, which failed to restore"
278 );
279 }
280
281 #[test]
282 fn an_empty_center_still_serializes_as_a_stack_panel() {
283 let tree = PaneTree::new(RootKind::Split);
284 let state = tree.to_state(&FakePanels(vec![]));
285 assert_eq!(state.panel_name, "StackPanel");
286 assert!(matches!(state.info, PanelInfo::Stack { .. }));
287 }
288
289 #[derive(Default)]
292 struct RecordingBuilder {
293 built: Vec<String>,
294 }
295
296 impl PanelBuilder for RecordingBuilder {
297 fn build(&mut self, state: &PanelState, _: &PanelInfo) -> PanelId {
298 self.built.push(state.panel_name.clone());
299 PanelId::from_u64(self.built.len() as u64)
300 }
301 }
302
303 fn tabs_state(children: Vec<PanelState>, active_index: usize) -> PanelState {
304 PanelState {
305 panel_name: TAB_PANEL_NAME.to_string(),
306 children,
307 info: PanelInfo::tabs(active_index),
308 }
309 }
310
311 fn panel_state(name: &str) -> PanelState {
312 PanelState {
313 panel_name: name.to_string(),
314 children: Vec::new(),
315 info: PanelInfo::panel(serde_json::Value::Null),
316 }
317 }
318
319 #[test]
320 fn nested_tab_groups_are_flattened() {
321 let state = tabs_state(
322 vec![
323 tabs_state(vec![panel_state("Alpha")], 0),
324 tabs_state(vec![panel_state("Beta")], 0),
325 ],
326 1,
327 );
328
329 let mut builder = RecordingBuilder::default();
330 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
331
332 assert_eq!(builder.built, vec!["Alpha", "Beta"]);
333 let PaneRef::Tabs { panels, active_ix } = tree.root().kind() else {
334 panic!()
335 };
336 assert_eq!(panels.len(), 2);
337 assert_eq!(active_ix, 1);
338 }
339
340 #[test]
341 fn a_bare_panel_leaf_is_wrapped_in_a_tab_group() {
342 let mut builder = RecordingBuilder::default();
343 let tree = PaneTree::from_state(&panel_state("Alpha"), RootKind::Any, &mut builder);
344
345 assert!(matches!(tree.root().kind(), PaneRef::Tabs { panels, .. } if panels.len() == 1));
346 }
347
348 #[test]
349 fn a_tab_panel_carrying_panel_info_is_read_as_an_empty_group() {
350 let state = PanelState {
352 panel_name: TAB_PANEL_NAME.to_string(),
353 children: Vec::new(),
354 info: PanelInfo::panel(serde_json::Value::Null),
355 };
356
357 let mut builder = RecordingBuilder::default();
358 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
359
360 assert!(
361 builder.built.is_empty(),
362 "no panel is built for the phantom leaf"
363 );
364 assert!(matches!(tree.root().kind(), PaneRef::Tabs { panels, .. } if panels.is_empty()));
365 }
366
367 #[test]
368 fn a_split_root_is_forced_even_when_the_state_is_a_tab_group() {
369 let state = tabs_state(vec![panel_state("Alpha")], 0);
370 let mut builder = RecordingBuilder::default();
371 let tree = PaneTree::from_state(&state, RootKind::Split, &mut builder);
372
373 assert!(matches!(tree.root().kind(), PaneRef::Split { .. }));
374 }
375
376 #[derive(Default)]
386 struct PreservingPanels {
387 states: Vec<PanelState>,
388 names: Vec<&'static str>,
389 }
390
391 impl PanelBuilder for PreservingPanels {
392 fn build(&mut self, state: &PanelState, _: &PanelInfo) -> PanelId {
393 self.states.push(state.clone());
394 self.names
395 .push(Box::leak(state.panel_name.clone().into_boxed_str()));
396 PanelId::from_u64(self.states.len() as u64)
397 }
398 }
399
400 impl PanelSource for PreservingPanels {
401 fn panel_name(&self, id: PanelId) -> &'static str {
402 self.names[id.as_u64() as usize - 1]
403 }
404 fn is_visible(&self, _: PanelId) -> bool {
405 true
406 }
407 fn dump(&self, id: PanelId) -> PanelState {
408 self.states[id.as_u64() as usize - 1].clone()
409 }
410 }
411
412 fn canonicalize(json: &str) -> PanelState {
413 let state: DockAreaState = serde_json::from_str(json).unwrap();
414 let mut panels = PreservingPanels::default();
415 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
416 tree.to_state(&panels)
417 }
418
419 #[test]
420 fn canonicalization_reaches_a_fixpoint_in_one_pass() {
421 for json in [
422 include_str!("fixtures/layout.json"),
423 include_str!("fixtures/nested_splits.json"),
424 include_str!("fixtures/legacy_empty_tab_group.json"),
425 include_str!("fixtures/unregistered_panel.json"),
426 include_str!("fixtures/zero_size_sentinel.json"),
427 ] {
428 let once = canonicalize(json);
429 let twice = {
430 let wrapped = DockAreaState {
431 center: once.clone(),
432 ..Default::default()
433 };
434 canonicalize(&serde_json::to_string(&wrapped).unwrap())
435 };
436 assert_eq!(once, twice, "r(r(x)) != r(x)");
437 }
438 }
439
440 #[test]
441 fn an_unregistered_panel_keeps_its_payload_through_a_round_trip() {
442 let state = canonicalize(include_str!("fixtures/unregistered_panel.json"));
443 let leaf = &state.children[0].children[0];
444
445 assert_eq!(leaf.panel_name, "PanelFromTheFuture");
446 assert_eq!(
447 leaf.info,
448 PanelInfo::panel(serde_json::json!({"keep": "me"}))
449 );
450 }
451
452 #[test]
453 fn the_legacy_empty_tab_group_is_rewritten_into_the_tabs_form() {
454 let state = canonicalize(include_str!("fixtures/legacy_empty_tab_group.json"));
455
456 assert_eq!(state.panel_name, "StackPanel");
458 assert!(state.children.is_empty());
459 }
460
461 #[test]
462 fn nested_same_axis_splits_are_flattened_and_single_child_splits_collapse() {
463 let state = canonicalize(include_str!("fixtures/nested_splits.json"));
464
465 assert_eq!(state.panel_name, "StackPanel");
466 assert_eq!(
467 state.children.len(),
468 3,
469 "the horizontal inner split splices in and the vertical single-child split collapses"
470 );
471 assert!(
472 state
473 .children
474 .iter()
475 .all(|child| child.panel_name == "TabPanel")
476 );
477 assert_eq!(state.children[0].children[0].panel_name, "Alpha");
481 assert_eq!(state.children[1].children[0].panel_name, "Beta");
482 assert_eq!(state.children[2].children[0].panel_name, "Gamma");
483
484 let PanelInfo::Stack { sizes, .. } = &state.info else {
491 panic!("expected Stack info");
492 };
493 assert_eq!(
494 sizes,
495 &vec![px(100.), px(300.), px(300.)],
496 "the spliced-in sizes are scaled by outer/inner (400/200 = 2x), and the \
497 collapsed split hands its own outer slot (300.0) to its surviving child"
498 );
499 }
500
501 #[test]
502 fn a_tree_survives_a_round_trip_exactly() {
503 let json = include_str!("fixtures/nested_splits.json");
504 let state: DockAreaState = serde_json::from_str(json).unwrap();
505 let mut panels = PreservingPanels::default();
506 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
507
508 let dumped = tree.to_state(&panels);
509 let mut rebuilt_panels = PreservingPanels::default();
510 let rebuilt = PaneTree::from_state(&dumped, RootKind::Split, &mut rebuilt_panels);
511
512 assert_eq!(
513 tree.to_state(&panels),
514 rebuilt.to_state(&rebuilt_panels),
515 "load(dump(t)) must describe the same layout as t"
516 );
517 }
518
519 #[test]
527 fn a_zero_size_slot_round_trips_through_the_none_sentinel() {
528 let json = include_str!("fixtures/zero_size_sentinel.json");
529 let state: DockAreaState = serde_json::from_str(json).unwrap();
530 let mut panels = PreservingPanels::default();
531 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
532
533 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
534 panic!("expected the root to stay a split");
535 };
536 assert_eq!(
537 sizes,
538 &[None, Some(px(200.))],
539 "the 0.0 sentinel reads back as None, not as a real zero-width panel"
540 );
541
542 let dumped = tree.to_state(&panels);
543 let PanelInfo::Stack { sizes, .. } = &dumped.info else {
544 panic!("expected Stack info");
545 };
546 assert_eq!(
547 sizes,
548 &vec![px(0.), px(200.)],
549 "the unconstrained slot writes back out as the 0.0 sentinel"
550 );
551 }
552
553 #[test]
572 fn a_bare_tab_panel_root_is_not_wrapped_under_root_kind_any() {
573 let json = include_str!("fixtures/bare_tab_panel_root.json");
574 let state: PanelState = serde_json::from_str(json).unwrap();
575 let mut panels = PreservingPanels::default();
576 let tree = PaneTree::from_state(&state, RootKind::Any, &mut panels);
577
578 assert!(
579 matches!(tree.root().kind(), PaneRef::Tabs { .. }),
580 "a bare TabPanel root must stay a Tabs node under RootKind::Any, \
581 not get wrapped in a synthetic split"
582 );
583
584 let dumped = tree.to_state(&panels);
585 assert_eq!(dumped.panel_name, "TabPanel");
586 }
587}