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(
97 state: &PanelState,
98 root_kind: RootKind,
99 builder: &mut dyn PanelBuilder,
100 ) -> Self {
101 let mut tree = PaneTree::new(root_kind);
102 let root = build_node(&mut tree, state, builder);
103
104 let root = match (root_kind, &root) {
105 (RootKind::Split, node) if !matches!(node.kind_ref(), NodeKind::Split { .. }) => {
106 let id = tree.allocate_node_id();
107 PaneNode::new(
108 id,
109 NodeKind::Split {
110 axis: Axis::Horizontal,
111 children: vec![node.clone()],
112 sizes: vec![None],
113 },
114 )
115 }
116 _ => root,
117 };
118
119 tree.replace_root(root);
120 tree.normalize();
121 tree
122 }
123}
124
125fn build_node(tree: &mut PaneTree, state: &PanelState, builder: &mut dyn PanelBuilder) -> PaneNode {
126 let id = tree.allocate_node_id();
127
128 match &state.info {
129 PanelInfo::Stack { sizes, axis } => {
130 let axis = if *axis == 0 {
131 Axis::Horizontal
132 } else {
133 Axis::Vertical
134 };
135 let children: Vec<PaneNode> = state
136 .children
137 .iter()
138 .map(|child| build_node(tree, child, builder))
139 .collect();
140 let sizes = (0..children.len())
141 .map(|ix| sizes.get(ix).copied().filter(|size| *size > Pixels::ZERO))
142 .collect();
143 PaneNode::new(
144 id,
145 NodeKind::Split {
146 axis,
147 children,
148 sizes,
149 },
150 )
151 }
152 PanelInfo::Tabs { active_index } => {
153 let panels = collect_tab_panels(&state.children, builder);
154 PaneNode::new(
155 id,
156 NodeKind::Tabs {
157 panels,
158 active_ix: *active_index,
159 },
160 )
161 }
162 PanelInfo::Panel(_) if state.panel_name == STACK_PANEL_NAME => PaneNode::new(
165 id,
166 NodeKind::Split {
167 axis: Axis::Horizontal,
168 children: Vec::new(),
169 sizes: Vec::new(),
170 },
171 ),
172 PanelInfo::Panel(_) => {
173 let panels = if state.panel_name == TAB_PANEL_NAME {
174 Vec::new()
175 } else {
176 vec![builder.build(state, &state.info)]
177 };
178 PaneNode::new(
179 id,
180 NodeKind::Tabs {
181 panels,
182 active_ix: 0,
183 },
184 )
185 }
186 }
187}
188
189fn is_container_name(panel_name: &str) -> bool {
190 panel_name == TAB_PANEL_NAME || panel_name == STACK_PANEL_NAME
191}
192
193fn collect_tab_panels(children: &[PanelState], builder: &mut dyn PanelBuilder) -> Vec<PanelId> {
195 children
196 .iter()
197 .flat_map(|child| match &child.info {
198 PanelInfo::Tabs { .. } => collect_tab_panels(&child.children, builder),
199 PanelInfo::Panel(_) if is_container_name(&child.panel_name) => Vec::new(),
200 _ => vec![builder.build(child, &child.info)],
201 })
202 .collect()
203}
204
205#[cfg(test)]
206mod tests {
207 use gpui::px;
208
209 use super::super::layout::RootKind;
210 use super::super::state::DockAreaState;
211 use super::*;
212
213 struct FakePanels(Vec<(PanelId, &'static str)>);
216
217 impl PanelSource for FakePanels {
218 fn panel_name(&self, id: PanelId) -> &'static str {
219 self.0
220 .iter()
221 .find(|(p, _)| *p == id)
222 .map(|(_, n)| *n)
223 .unwrap_or("Unknown")
224 }
225 fn is_visible(&self, _: PanelId) -> bool {
226 true
227 }
228 fn dump(&self, id: PanelId) -> PanelState {
229 PanelState {
230 panel_name: self.panel_name(id).to_string(),
231 children: Vec::new(),
232 info: PanelInfo::panel(serde_json::Value::Null),
233 }
234 }
235 }
236
237 #[test]
238 fn a_split_serializes_as_a_stack_panel() {
239 let mut tree = PaneTree::new(RootKind::Split);
240 tree.push_tabs_for_test(tree.root().id(), vec![PanelId::from_u64(1)]);
241 tree.push_tabs_for_test(tree.root().id(), vec![PanelId::from_u64(2)]);
242 tree.normalize();
243
244 let source = FakePanels(vec![
245 (PanelId::from_u64(1), "Alpha"),
246 (PanelId::from_u64(2), "Beta"),
247 ]);
248 let state = tree.to_state(&source);
249
250 assert_eq!(state.panel_name, "StackPanel");
251 let PanelInfo::Stack { sizes, .. } = &state.info else {
252 panic!("expected Stack info");
253 };
254 assert_eq!(sizes, &vec![px(0.), px(0.)]);
257 assert_eq!(state.children[0].panel_name, "TabPanel");
258 assert_eq!(state.children[0].children[0].panel_name, "Alpha");
259 }
260
261 #[test]
262 fn an_unresolved_slot_size_serializes_as_the_zero_sentinel() {
263 let mut tree = PaneTree::new(RootKind::Split);
264 let root = tree.root().id();
265 tree.push_sized_tabs_for_test(root, vec![PanelId::from_u64(1)], Some(px(120.)));
266 tree.push_sized_tabs_for_test(root, vec![PanelId::from_u64(2)], None);
267 tree.normalize();
268
269 let source = FakePanels(vec![
270 (PanelId::from_u64(1), "Alpha"),
271 (PanelId::from_u64(2), "Beta"),
272 ]);
273 let state = tree.to_state(&source);
274
275 let PanelInfo::Stack { sizes, .. } = state.info else {
276 panic!("expected Stack info");
277 };
278 assert_eq!(sizes, vec![px(120.), px(0.)]);
281 }
282
283 #[test]
284 fn an_empty_tab_group_serializes_as_tabs_not_as_a_panel() {
285 let mut tree = PaneTree::new(RootKind::Any);
286 tree.set_root_tabs_for_test(vec![], 0);
287
288 let state = tree.to_state(&FakePanels(vec![]));
289
290 assert_eq!(state.panel_name, "TabPanel");
291 assert!(
292 matches!(state.info, PanelInfo::Tabs { active_index: 0 }),
293 "the old writer emitted PanelInfo::Panel here, which failed to restore"
294 );
295 }
296
297 #[test]
298 fn an_empty_center_still_serializes_as_a_stack_panel() {
299 let tree = PaneTree::new(RootKind::Split);
300 let state = tree.to_state(&FakePanels(vec![]));
301 assert_eq!(state.panel_name, "StackPanel");
302 assert!(matches!(state.info, PanelInfo::Stack { .. }));
303 }
304
305 #[derive(Default)]
308 struct RecordingBuilder {
309 built: Vec<String>,
310 }
311
312 impl PanelBuilder for RecordingBuilder {
313 fn build(&mut self, state: &PanelState, _: &PanelInfo) -> PanelId {
314 self.built.push(state.panel_name.clone());
315 PanelId::from_u64(self.built.len() as u64)
316 }
317 }
318
319 fn tabs_state(children: Vec<PanelState>, active_index: usize) -> PanelState {
320 PanelState {
321 panel_name: TAB_PANEL_NAME.to_string(),
322 children,
323 info: PanelInfo::tabs(active_index),
324 }
325 }
326
327 fn panel_state(name: &str) -> PanelState {
328 PanelState {
329 panel_name: name.to_string(),
330 children: Vec::new(),
331 info: PanelInfo::panel(serde_json::Value::Null),
332 }
333 }
334
335 #[test]
336 fn nested_tab_groups_are_flattened() {
337 let state = tabs_state(
338 vec![
339 tabs_state(vec![panel_state("Alpha")], 0),
340 tabs_state(vec![panel_state("Beta")], 0),
341 ],
342 1,
343 );
344
345 let mut builder = RecordingBuilder::default();
346 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
347
348 assert_eq!(builder.built, vec!["Alpha", "Beta"]);
349 let PaneRef::Tabs { panels, active_ix } = tree.root().kind() else {
350 panic!()
351 };
352 assert_eq!(panels.len(), 2);
353 assert_eq!(active_ix, 1);
354 }
355
356 #[test]
357 fn a_bare_panel_leaf_is_wrapped_in_a_tab_group() {
358 let mut builder = RecordingBuilder::default();
359 let tree = PaneTree::from_state(&panel_state("Alpha"), RootKind::Any, &mut builder);
360
361 assert!(matches!(tree.root().kind(), PaneRef::Tabs { panels, .. } if panels.len() == 1));
362 }
363
364 #[test]
365 fn a_tab_panel_carrying_panel_info_is_read_as_an_empty_group() {
366 let state = PanelState {
368 panel_name: TAB_PANEL_NAME.to_string(),
369 children: Vec::new(),
370 info: PanelInfo::panel(serde_json::Value::Null),
371 };
372
373 let mut builder = RecordingBuilder::default();
374 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
375
376 assert!(
377 builder.built.is_empty(),
378 "no panel is built for the phantom leaf"
379 );
380 assert!(matches!(tree.root().kind(), PaneRef::Tabs { panels, .. } if panels.is_empty()));
381 }
382
383 #[test]
384 fn a_split_root_is_forced_even_when_the_state_is_a_tab_group() {
385 let state = tabs_state(vec![panel_state("Alpha")], 0);
386 let mut builder = RecordingBuilder::default();
387 let tree = PaneTree::from_state(&state, RootKind::Split, &mut builder);
388
389 assert!(matches!(tree.root().kind(), PaneRef::Split { .. }));
390 }
391
392 #[derive(Default)]
402 struct PreservingPanels {
403 states: Vec<PanelState>,
404 names: Vec<&'static str>,
405 }
406
407 impl PanelBuilder for PreservingPanels {
408 fn build(&mut self, state: &PanelState, _: &PanelInfo) -> PanelId {
409 self.states.push(state.clone());
410 self.names
411 .push(Box::leak(state.panel_name.clone().into_boxed_str()));
412 PanelId::from_u64(self.states.len() as u64)
413 }
414 }
415
416 impl PanelSource for PreservingPanels {
417 fn panel_name(&self, id: PanelId) -> &'static str {
418 self.names[id.as_u64() as usize - 1]
419 }
420 fn is_visible(&self, _: PanelId) -> bool {
421 true
422 }
423 fn dump(&self, id: PanelId) -> PanelState {
424 self.states[id.as_u64() as usize - 1].clone()
425 }
426 }
427
428 fn canonicalize(json: &str) -> PanelState {
429 let state: DockAreaState = serde_json::from_str(json).unwrap();
430 let mut panels = PreservingPanels::default();
431 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
432 tree.to_state(&panels)
433 }
434
435 #[test]
436 fn canonicalization_reaches_a_fixpoint_in_one_pass() {
437 for json in [
438 include_str!("fixtures/layout.json"),
439 include_str!("fixtures/nested_splits.json"),
440 include_str!("fixtures/legacy_empty_tab_group.json"),
441 include_str!("fixtures/unregistered_panel.json"),
442 include_str!("fixtures/zero_size_sentinel.json"),
443 include_str!("fixtures/legacy_empty_stack_panel.json"),
444 include_str!("fixtures/placeholder_wrapped_empty_stack.json"),
445 ] {
446 let once = canonicalize(json);
447 let twice = {
448 let wrapped = DockAreaState {
449 center: once.clone(),
450 ..Default::default()
451 };
452 canonicalize(&serde_json::to_string(&wrapped).unwrap())
453 };
454 assert_eq!(once, twice, "r(r(x)) != r(x)");
455 }
456 }
457
458 #[test]
459 fn an_unregistered_panel_keeps_its_payload_through_a_round_trip() {
460 let state = canonicalize(include_str!("fixtures/unregistered_panel.json"));
461 let leaf = &state.children[0].children[0];
462
463 assert_eq!(leaf.panel_name, "PanelFromTheFuture");
464 assert_eq!(
465 leaf.info,
466 PanelInfo::panel(serde_json::json!({"keep": "me"}))
467 );
468 }
469
470 #[test]
471 fn the_legacy_empty_tab_group_is_rewritten_into_the_tabs_form() {
472 let state = canonicalize(include_str!("fixtures/legacy_empty_tab_group.json"));
473
474 assert_eq!(state.panel_name, "StackPanel");
476 assert!(state.children.is_empty());
477 }
478
479 #[test]
480 fn the_legacy_empty_stack_panel_is_read_as_an_empty_split() {
481 let json = include_str!("fixtures/legacy_empty_stack_panel.json");
482 let state: DockAreaState = serde_json::from_str(json).unwrap();
483 let mut panels = PreservingPanels::default();
484 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
485
486 assert!(panels.states.is_empty(), "no placeholder is built for it");
487 let state = tree.to_state(&panels);
488 assert_eq!(state.panel_name, "StackPanel");
489 assert!(matches!(state.info, PanelInfo::Stack { .. }));
490 assert!(state.children.is_empty());
491 }
492
493 #[test]
496 fn an_empty_stack_panel_saved_inside_a_tab_group_is_dropped() {
497 let state = canonicalize(include_str!(
498 "fixtures/placeholder_wrapped_empty_stack.json"
499 ));
500 let group = &state.children[0];
501
502 assert_eq!(group.panel_name, "TabPanel");
503 assert_eq!(group.info, PanelInfo::tabs(0));
504 let names: Vec<_> = group
505 .children
506 .iter()
507 .map(|child| child.panel_name.as_str())
508 .collect();
509 assert_eq!(names, ["Alpha"]);
510 }
511
512 #[test]
513 fn nested_same_axis_splits_are_flattened_and_single_child_splits_collapse() {
514 let state = canonicalize(include_str!("fixtures/nested_splits.json"));
515
516 assert_eq!(state.panel_name, "StackPanel");
517 assert_eq!(
518 state.children.len(),
519 3,
520 "the horizontal inner split splices in and the vertical single-child split collapses"
521 );
522 assert!(
523 state
524 .children
525 .iter()
526 .all(|child| child.panel_name == "TabPanel")
527 );
528 assert_eq!(state.children[0].children[0].panel_name, "Alpha");
532 assert_eq!(state.children[1].children[0].panel_name, "Beta");
533 assert_eq!(state.children[2].children[0].panel_name, "Gamma");
534
535 let PanelInfo::Stack { sizes, .. } = &state.info else {
542 panic!("expected Stack info");
543 };
544 assert_eq!(
545 sizes,
546 &vec![px(100.), px(300.), px(300.)],
547 "the spliced-in sizes are scaled by outer/inner (400/200 = 2x), and the \
548 collapsed split hands its own outer slot (300.0) to its surviving child"
549 );
550 }
551
552 #[test]
553 fn a_tree_survives_a_round_trip_exactly() {
554 let json = include_str!("fixtures/nested_splits.json");
555 let state: DockAreaState = serde_json::from_str(json).unwrap();
556 let mut panels = PreservingPanels::default();
557 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
558
559 let dumped = tree.to_state(&panels);
560 let mut rebuilt_panels = PreservingPanels::default();
561 let rebuilt = PaneTree::from_state(&dumped, RootKind::Split, &mut rebuilt_panels);
562
563 assert_eq!(
564 tree.to_state(&panels),
565 rebuilt.to_state(&rebuilt_panels),
566 "load(dump(t)) must describe the same layout as t"
567 );
568 }
569
570 #[test]
578 fn a_zero_size_slot_round_trips_through_the_none_sentinel() {
579 let json = include_str!("fixtures/zero_size_sentinel.json");
580 let state: DockAreaState = serde_json::from_str(json).unwrap();
581 let mut panels = PreservingPanels::default();
582 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
583
584 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
585 panic!("expected the root to stay a split");
586 };
587 assert_eq!(
588 sizes,
589 &[None, Some(px(200.))],
590 "the 0.0 sentinel reads back as None, not as a real zero-width panel"
591 );
592
593 let dumped = tree.to_state(&panels);
594 let PanelInfo::Stack { sizes, .. } = &dumped.info else {
595 panic!("expected Stack info");
596 };
597 assert_eq!(
598 sizes,
599 &vec![px(0.), px(200.)],
600 "the unconstrained slot writes back out as the 0.0 sentinel"
601 );
602 }
603
604 #[test]
623 fn a_bare_tab_panel_root_is_not_wrapped_under_root_kind_any() {
624 let json = include_str!("fixtures/bare_tab_panel_root.json");
625 let state: PanelState = serde_json::from_str(json).unwrap();
626 let mut panels = PreservingPanels::default();
627 let tree = PaneTree::from_state(&state, RootKind::Any, &mut panels);
628
629 assert!(
630 matches!(tree.root().kind(), PaneRef::Tabs { .. }),
631 "a bare TabPanel root must stay a Tabs node under RootKind::Any, \
632 not get wrapped in a synthetic split"
633 );
634
635 let dumped = tree.to_state(&panels);
636 assert_eq!(dumped.panel_name, "TabPanel");
637 }
638}