1use gpui::{Axis, Pixels};
2
3use super::layout::{NodeKind, PaneNode, PaneRef, PaneTree, PanelId, RootKind, TilePanel};
4use super::state::{PanelInfo, PanelState, TileMeta};
5
6pub(crate) const STACK_PANEL_NAME: &str = "StackPanel";
9pub(crate) const TAB_PANEL_NAME: &str = "TabPanel";
10pub(crate) const TILES_PANEL_NAME: &str = "Tiles";
11
12pub trait PanelSource {
17 fn panel_name(&self, id: PanelId) -> &'static str;
18 fn is_visible(&self, id: PanelId) -> bool;
19 fn dump(&self, id: PanelId) -> PanelState;
20}
21
22impl PaneTree {
23 pub fn to_state(&self, source: &dyn PanelSource) -> PanelState {
24 node_to_state(self.persisted_root(), source)
25 }
26
27 fn persisted_root(&self) -> &PaneNode {
37 let root = self.root();
38 match root.kind() {
39 PaneRef::Split {
40 children: [only], ..
41 } if self.root_kind() == RootKind::Split
42 && matches!(only.kind(), PaneRef::Tiles { .. }) =>
43 {
44 only
45 }
46 _ => root,
47 }
48 }
49}
50
51fn node_to_state(node: &PaneNode, source: &dyn PanelSource) -> PanelState {
52 match node.kind() {
53 PaneRef::Split {
54 axis,
55 children,
56 sizes,
57 } => PanelState {
58 panel_name: STACK_PANEL_NAME.to_string(),
59 children: children
60 .iter()
61 .map(|child| node_to_state(child, source))
62 .collect(),
63 info: PanelInfo::stack(
75 sizes.iter().map(|size| size.unwrap_or_default()).collect(),
76 axis,
77 ),
78 },
79 PaneRef::Tabs { panels, active_ix } => PanelState {
80 panel_name: TAB_PANEL_NAME.to_string(),
81 children: panels.iter().map(|panel| source.dump(*panel)).collect(),
82 info: PanelInfo::tabs(active_ix),
85 },
86 PaneRef::Tiles { panels } => PanelState {
87 panel_name: TILES_PANEL_NAME.to_string(),
88 children: panels
89 .iter()
90 .map(|tile| source.dump(tile.panel()))
91 .collect(),
92 info: PanelInfo::tiles(
93 panels
94 .iter()
95 .map(|tile| TileMeta {
96 bounds: tile.bounds(),
97 z_index: tile.z_index(),
98 })
99 .collect(),
100 ),
101 },
102 }
103}
104
105pub trait PanelBuilder {
113 fn build(&mut self, state: &PanelState, info: &PanelInfo) -> PanelId;
114}
115
116impl PaneTree {
117 pub fn from_state(
138 state: &PanelState,
139 root_kind: RootKind,
140 builder: &mut dyn PanelBuilder,
141 ) -> Self {
142 let mut tree = PaneTree::new(root_kind);
143 let root = build_node(&mut tree, state, builder);
144
145 let root = match (root_kind, &root) {
146 (RootKind::Split, node) if !matches!(node.kind_ref(), NodeKind::Split { .. }) => {
147 let id = tree.allocate_node_id();
148 PaneNode::new(
149 id,
150 NodeKind::Split {
151 axis: Axis::Horizontal,
152 children: vec![node.clone()],
153 sizes: vec![None],
154 },
155 )
156 }
157 _ => root,
158 };
159
160 tree.replace_root(root);
161 tree.normalize();
162 tree
163 }
164}
165
166fn build_node(tree: &mut PaneTree, state: &PanelState, builder: &mut dyn PanelBuilder) -> PaneNode {
167 let id = tree.allocate_node_id();
168
169 match &state.info {
170 PanelInfo::Stack { sizes, axis } => {
171 let axis = if *axis == 0 {
172 Axis::Horizontal
173 } else {
174 Axis::Vertical
175 };
176 let children: Vec<PaneNode> = state
177 .children
178 .iter()
179 .map(|child| build_node(tree, child, builder))
180 .collect();
181 let sizes = (0..children.len())
182 .map(|ix| sizes.get(ix).copied().filter(|size| *size > Pixels::ZERO))
183 .collect();
184 PaneNode::new(
185 id,
186 NodeKind::Split {
187 axis,
188 children,
189 sizes,
190 },
191 )
192 }
193 PanelInfo::Tabs { active_index } => {
194 let panels = collect_tab_panels(&state.children, builder);
195 PaneNode::new(
196 id,
197 NodeKind::Tabs {
198 panels,
199 active_ix: *active_index,
200 },
201 )
202 }
203 PanelInfo::Tiles { metas } => {
204 let mut panels = Vec::new();
205 for (ix, child) in state.children.iter().enumerate() {
206 let meta = metas.get(ix).copied().unwrap_or_default();
210 for panel in tile_panels(child, builder) {
211 panels.push(TilePanel::new(panel, meta.bounds).with_z_index(meta.z_index));
212 }
213 }
214 PaneNode::new(id, NodeKind::Tiles { panels })
215 }
216 PanelInfo::Panel(_) => {
217 let panels = if state.panel_name == TAB_PANEL_NAME {
220 Vec::new()
221 } else {
222 vec![builder.build(state, &state.info)]
223 };
224 PaneNode::new(
225 id,
226 NodeKind::Tabs {
227 panels,
228 active_ix: 0,
229 },
230 )
231 }
232 }
233}
234
235fn tile_panels(child: &PanelState, builder: &mut dyn PanelBuilder) -> Vec<PanelId> {
252 match &child.info {
253 PanelInfo::Tabs { .. } => {
254 let panels = collect_tab_panels(&child.children, builder);
255 if panels.len() > 1 {
256 tracing::warn!(
257 panels = panels.len(),
258 "a tiles child held a tab group with more than one panel; \
259 expanding it to one tile per panel, all sharing the group's placement"
260 );
261 }
262 panels
263 }
264 PanelInfo::Panel(_) if child.panel_name == TAB_PANEL_NAME => Vec::new(),
267 _ => vec![builder.build(child, &child.info)],
268 }
269}
270
271fn collect_tab_panels(children: &[PanelState], builder: &mut dyn PanelBuilder) -> Vec<PanelId> {
273 children
274 .iter()
275 .flat_map(|child| match &child.info {
276 PanelInfo::Tabs { .. } => collect_tab_panels(&child.children, builder),
277 PanelInfo::Panel(_) if child.panel_name == TAB_PANEL_NAME => Vec::new(),
278 _ => vec![builder.build(child, &child.info)],
279 })
280 .collect()
281}
282
283#[cfg(test)]
284mod tests {
285 use gpui::{Bounds, point, px, size};
286
287 use super::super::layout::{RootKind, TilePanel};
288 use super::super::state::DockAreaState;
289 use super::*;
290
291 struct FakePanels(Vec<(PanelId, &'static str)>);
294
295 impl PanelSource for FakePanels {
296 fn panel_name(&self, id: PanelId) -> &'static str {
297 self.0
298 .iter()
299 .find(|(p, _)| *p == id)
300 .map(|(_, n)| *n)
301 .unwrap_or("Unknown")
302 }
303 fn is_visible(&self, _: PanelId) -> bool {
304 true
305 }
306 fn dump(&self, id: PanelId) -> PanelState {
307 PanelState {
308 panel_name: self.panel_name(id).to_string(),
309 children: Vec::new(),
310 info: PanelInfo::panel(serde_json::Value::Null),
311 }
312 }
313 }
314
315 #[test]
316 fn a_split_serializes_as_a_stack_panel() {
317 let mut tree = PaneTree::new(RootKind::Split);
318 tree.push_tabs_for_test(tree.root().id(), vec![PanelId::from_u64(1)]);
319 tree.push_tabs_for_test(tree.root().id(), vec![PanelId::from_u64(2)]);
320 tree.normalize();
321
322 let source = FakePanels(vec![
323 (PanelId::from_u64(1), "Alpha"),
324 (PanelId::from_u64(2), "Beta"),
325 ]);
326 let state = tree.to_state(&source);
327
328 assert_eq!(state.panel_name, "StackPanel");
329 let PanelInfo::Stack { sizes, .. } = &state.info else {
330 panic!("expected Stack info");
331 };
332 assert_eq!(sizes, &vec![px(0.), px(0.)]);
335 assert_eq!(state.children[0].panel_name, "TabPanel");
336 assert_eq!(state.children[0].children[0].panel_name, "Alpha");
337 }
338
339 #[test]
340 fn an_unresolved_slot_size_serializes_as_the_zero_sentinel() {
341 let mut tree = PaneTree::new(RootKind::Split);
342 let root = tree.root().id();
343 tree.push_sized_tabs_for_test(root, vec![PanelId::from_u64(1)], Some(px(120.)));
344 tree.push_sized_tabs_for_test(root, vec![PanelId::from_u64(2)], None);
345 tree.normalize();
346
347 let source = FakePanels(vec![
348 (PanelId::from_u64(1), "Alpha"),
349 (PanelId::from_u64(2), "Beta"),
350 ]);
351 let state = tree.to_state(&source);
352
353 let PanelInfo::Stack { sizes, .. } = state.info else {
354 panic!("expected Stack info");
355 };
356 assert_eq!(sizes, vec![px(120.), px(0.)]);
359 }
360
361 #[test]
362 fn an_empty_tab_group_serializes_as_tabs_not_as_a_panel() {
363 let mut tree = PaneTree::new(RootKind::Any);
364 tree.set_root_tabs_for_test(vec![], 0);
365
366 let state = tree.to_state(&FakePanels(vec![]));
367
368 assert_eq!(state.panel_name, "TabPanel");
369 assert!(
370 matches!(state.info, PanelInfo::Tabs { active_index: 0 }),
371 "the old writer emitted PanelInfo::Panel here, which failed to restore"
372 );
373 }
374
375 #[test]
376 fn an_empty_center_still_serializes_as_a_stack_panel() {
377 let tree = PaneTree::new(RootKind::Split);
378 let state = tree.to_state(&FakePanels(vec![]));
379 assert_eq!(state.panel_name, "StackPanel");
380 assert!(matches!(state.info, PanelInfo::Stack { .. }));
381 }
382
383 #[test]
384 fn tiles_serialize_with_their_metas_in_order() {
385 let mut tree = PaneTree::new(RootKind::Any);
386 let bounds = Bounds {
387 origin: point(px(5.), px(6.)),
388 size: size(px(7.), px(8.)),
389 };
390 tree.set_root_tiles_for_test(vec![
391 TilePanel::new(PanelId::from_u64(1), bounds).with_z_index(2),
392 ]);
393
394 let state = tree.to_state(&FakePanels(vec![(PanelId::from_u64(1), "Alpha")]));
395
396 assert_eq!(state.panel_name, "Tiles");
397 let PanelInfo::Tiles { metas } = state.info else {
398 panic!()
399 };
400 assert_eq!(metas[0].bounds, bounds);
401 assert_eq!(metas[0].z_index, 2);
402 }
403
404 #[derive(Default)]
407 struct RecordingBuilder {
408 built: Vec<String>,
409 }
410
411 impl PanelBuilder for RecordingBuilder {
412 fn build(&mut self, state: &PanelState, _: &PanelInfo) -> PanelId {
413 self.built.push(state.panel_name.clone());
414 PanelId::from_u64(self.built.len() as u64)
415 }
416 }
417
418 fn tabs_state(children: Vec<PanelState>, active_index: usize) -> PanelState {
419 PanelState {
420 panel_name: TAB_PANEL_NAME.to_string(),
421 children,
422 info: PanelInfo::tabs(active_index),
423 }
424 }
425
426 fn panel_state(name: &str) -> PanelState {
427 PanelState {
428 panel_name: name.to_string(),
429 children: Vec::new(),
430 info: PanelInfo::panel(serde_json::Value::Null),
431 }
432 }
433
434 #[test]
435 fn nested_tab_groups_are_flattened() {
436 let state = tabs_state(
437 vec![
438 tabs_state(vec![panel_state("Alpha")], 0),
439 tabs_state(vec![panel_state("Beta")], 0),
440 ],
441 1,
442 );
443
444 let mut builder = RecordingBuilder::default();
445 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
446
447 assert_eq!(builder.built, vec!["Alpha", "Beta"]);
448 let PaneRef::Tabs { panels, active_ix } = tree.root().kind() else {
449 panic!()
450 };
451 assert_eq!(panels.len(), 2);
452 assert_eq!(active_ix, 1);
453 }
454
455 #[test]
456 fn a_bare_panel_leaf_is_wrapped_in_a_tab_group() {
457 let mut builder = RecordingBuilder::default();
458 let tree = PaneTree::from_state(&panel_state("Alpha"), RootKind::Any, &mut builder);
459
460 assert!(matches!(tree.root().kind(), PaneRef::Tabs { panels, .. } if panels.len() == 1));
461 }
462
463 #[test]
464 fn a_tab_panel_carrying_panel_info_is_read_as_an_empty_group() {
465 let state = PanelState {
467 panel_name: TAB_PANEL_NAME.to_string(),
468 children: Vec::new(),
469 info: PanelInfo::panel(serde_json::Value::Null),
470 };
471
472 let mut builder = RecordingBuilder::default();
473 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
474
475 assert!(
476 builder.built.is_empty(),
477 "no panel is built for the phantom leaf"
478 );
479 assert!(matches!(tree.root().kind(), PaneRef::Tabs { panels, .. } if panels.is_empty()));
480 }
481
482 #[test]
483 fn a_split_root_is_forced_even_when_the_state_is_a_tab_group() {
484 let state = tabs_state(vec![panel_state("Alpha")], 0);
485 let mut builder = RecordingBuilder::default();
486 let tree = PaneTree::from_state(&state, RootKind::Split, &mut builder);
487
488 assert!(matches!(tree.root().kind(), PaneRef::Split { .. }));
489 }
490
491 #[test]
492 fn tile_metas_are_paired_with_children_by_index() {
493 let bounds = Bounds {
494 origin: point(px(1.), px(2.)),
495 size: size(px(3.), px(4.)),
496 };
497 let state = PanelState {
498 panel_name: TILES_PANEL_NAME.to_string(),
499 children: vec![panel_state("Alpha")],
500 info: PanelInfo::tiles(vec![TileMeta { bounds, z_index: 5 }]),
501 };
502
503 let mut builder = RecordingBuilder::default();
504 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
505
506 let PaneRef::Tiles { panels } = tree.root().kind() else {
507 panic!()
508 };
509 assert_eq!(panels[0].bounds(), bounds);
510 assert_eq!(panels[0].z_index(), 5);
511 }
512
513 #[test]
514 fn a_tile_child_missing_its_meta_falls_back_to_the_default_placement() {
515 let state = PanelState {
516 panel_name: TILES_PANEL_NAME.to_string(),
517 children: vec![panel_state("Alpha"), panel_state("Beta")],
518 info: PanelInfo::tiles(vec![TileMeta::default()]),
519 };
520
521 let mut builder = RecordingBuilder::default();
522 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
523
524 let PaneRef::Tiles { panels } = tree.root().kind() else {
525 panic!()
526 };
527 assert_eq!(panels.len(), 2, "a short metas list must not drop panels");
528 }
529
530 #[derive(Default)]
540 struct PreservingPanels {
541 states: Vec<PanelState>,
542 names: Vec<&'static str>,
543 }
544
545 impl PanelBuilder for PreservingPanels {
546 fn build(&mut self, state: &PanelState, _: &PanelInfo) -> PanelId {
547 self.states.push(state.clone());
548 self.names
549 .push(Box::leak(state.panel_name.clone().into_boxed_str()));
550 PanelId::from_u64(self.states.len() as u64)
551 }
552 }
553
554 impl PanelSource for PreservingPanels {
555 fn panel_name(&self, id: PanelId) -> &'static str {
556 self.names[id.as_u64() as usize - 1]
557 }
558 fn is_visible(&self, _: PanelId) -> bool {
559 true
560 }
561 fn dump(&self, id: PanelId) -> PanelState {
562 self.states[id.as_u64() as usize - 1].clone()
563 }
564 }
565
566 fn canonicalize(json: &str) -> PanelState {
567 let state: DockAreaState = serde_json::from_str(json).unwrap();
568 let mut panels = PreservingPanels::default();
569 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
570 tree.to_state(&panels)
571 }
572
573 #[test]
574 fn canonicalization_reaches_a_fixpoint_in_one_pass() {
575 for json in [
576 include_str!("fixtures/layout.json"),
577 include_str!("fixtures/tiles.json"),
578 include_str!("fixtures/nested_splits.json"),
579 include_str!("fixtures/legacy_empty_tab_group.json"),
580 include_str!("fixtures/unregistered_panel.json"),
581 include_str!("fixtures/zero_size_sentinel.json"),
582 include_str!("fixtures/tiles_tab_panel_children.json"),
583 ] {
584 let once = canonicalize(json);
585 let twice = {
586 let wrapped = DockAreaState {
587 center: once.clone(),
588 ..Default::default()
589 };
590 canonicalize(&serde_json::to_string(&wrapped).unwrap())
591 };
592 assert_eq!(once, twice, "r(r(x)) != r(x)");
593 }
594 }
595
596 #[test]
597 fn an_unregistered_panel_keeps_its_payload_through_a_round_trip() {
598 let state = canonicalize(include_str!("fixtures/unregistered_panel.json"));
599 let leaf = &state.children[0].children[0];
600
601 assert_eq!(leaf.panel_name, "PanelFromTheFuture");
602 assert_eq!(
603 leaf.info,
604 PanelInfo::panel(serde_json::json!({"keep": "me"}))
605 );
606 }
607
608 #[test]
609 fn the_legacy_empty_tab_group_is_rewritten_into_the_tabs_form() {
610 let state = canonicalize(include_str!("fixtures/legacy_empty_tab_group.json"));
611
612 assert_eq!(state.panel_name, "StackPanel");
614 assert!(state.children.is_empty());
615 }
616
617 #[test]
618 fn nested_same_axis_splits_are_flattened_and_single_child_splits_collapse() {
619 let state = canonicalize(include_str!("fixtures/nested_splits.json"));
620
621 assert_eq!(state.panel_name, "StackPanel");
622 assert_eq!(
623 state.children.len(),
624 3,
625 "the horizontal inner split splices in and the vertical single-child split collapses"
626 );
627 assert!(
628 state
629 .children
630 .iter()
631 .all(|child| child.panel_name == "TabPanel")
632 );
633 assert_eq!(state.children[0].children[0].panel_name, "Alpha");
637 assert_eq!(state.children[1].children[0].panel_name, "Beta");
638 assert_eq!(state.children[2].children[0].panel_name, "Gamma");
639
640 let PanelInfo::Stack { sizes, .. } = &state.info else {
647 panic!("expected Stack info");
648 };
649 assert_eq!(
650 sizes,
651 &vec![px(100.), px(300.), px(300.)],
652 "the spliced-in sizes are scaled by outer/inner (400/200 = 2x), and the \
653 collapsed split hands its own outer slot (300.0) to its surviving child"
654 );
655 }
656
657 #[test]
661 fn a_tiles_child_that_is_a_tab_group_is_unwrapped_to_its_panels() {
662 let json = include_str!("fixtures/tiles_tab_panel_children.json");
663 let state: DockAreaState = serde_json::from_str(json).unwrap();
664 let mut panels = PreservingPanels::default();
665 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
666
667 assert_eq!(
668 panels.names,
669 vec!["Alpha", "Beta", "Gamma"],
670 "the real panels are built; no `TabPanel` leaf is handed to the builder"
671 );
672
673 let dumped = tree.to_state(&panels);
674 let tiles = &dumped;
675 assert_eq!(tiles.panel_name, "Tiles");
676 assert_eq!(
677 tiles
678 .children
679 .iter()
680 .map(|child| child.panel_name.as_str())
681 .collect::<Vec<_>>(),
682 vec!["Alpha", "Beta", "Gamma"],
683 );
684
685 let PanelInfo::Tiles { metas } = &tiles.info else {
686 panic!("expected Tiles info");
687 };
688 assert_eq!(metas.len(), 3);
693 assert_eq!(
694 metas[0].bounds.origin.x,
695 px(10.),
696 "Alpha keeps child 0's meta"
697 );
698 assert_eq!(
699 metas[1].bounds.origin.x,
700 px(10.),
701 "Beta shares child 0's meta"
702 );
703 assert_eq!(metas[1].z_index, 0);
704 assert_eq!(
705 metas[2].bounds.origin.x,
706 px(400.),
707 "Gamma gets child 1's meta"
708 );
709 assert_eq!(metas[2].z_index, 3);
710 }
711
712 #[test]
713 fn an_empty_tab_group_on_a_tiles_canvas_contributes_no_tile() {
714 let state = PanelState {
715 panel_name: TILES_PANEL_NAME.to_string(),
716 children: vec![
717 PanelState {
719 panel_name: TAB_PANEL_NAME.to_string(),
720 children: Vec::new(),
721 info: PanelInfo::panel(serde_json::Value::Null),
722 },
723 tabs_state(vec![panel_state("Alpha")], 0),
724 ],
725 info: PanelInfo::tiles(vec![TileMeta::default(), TileMeta::default()]),
726 };
727
728 let mut builder = RecordingBuilder::default();
729 let tree = PaneTree::from_state(&state, RootKind::Any, &mut builder);
730
731 assert_eq!(builder.built, vec!["Alpha"]);
732 let PaneRef::Tiles { panels } = tree.root().kind() else {
733 panic!()
734 };
735 assert_eq!(panels.len(), 1);
736 }
737
738 #[test]
744 fn a_tiles_center_is_written_the_way_the_pre_tree_dock_wrote_it() {
745 let legacy = PanelState {
746 panel_name: TILES_PANEL_NAME.to_string(),
747 children: vec![tabs_state(vec![panel_state("Alpha")], 0)],
748 info: PanelInfo::tiles(vec![TileMeta::default()]),
749 };
750 let mut panels = PreservingPanels::default();
751 let tree = PaneTree::from_state(&legacy, RootKind::Split, &mut panels);
752
753 let dumped = tree.to_state(&panels);
754
755 assert_eq!(
756 dumped.panel_name, "Tiles",
757 "no `StackPanel` wrapper: the old reader hands the canvas to `StackPanel::insert_panel`"
758 );
759 assert_eq!(dumped.children[0].panel_name, "Alpha");
760 }
761
762 #[test]
763 fn tile_bounds_and_z_order_survive_a_round_trip() {
764 let state = canonicalize(include_str!("fixtures/tiles.json"));
765 let tiles = &state;
766
767 assert_eq!(tiles.panel_name, "Tiles");
768 assert_eq!(tiles.children[0].panel_name, "Alpha");
775 assert_eq!(tiles.children[1].panel_name, "Beta");
776
777 let PanelInfo::Tiles { metas } = &tiles.info else {
778 panic!()
779 };
780 assert_eq!(metas.len(), 2);
781 assert_eq!(metas[0].z_index, 0, "Alpha's meta");
782 assert_eq!(metas[0].bounds.origin.x, px(10.), "Alpha's meta");
783 assert_eq!(metas[1].z_index, 1, "Beta's meta");
784 assert_eq!(metas[1].bounds.origin.x, px(220.), "Beta's meta");
785 }
786
787 #[test]
788 fn a_tree_survives_a_round_trip_exactly() {
789 let json = include_str!("fixtures/nested_splits.json");
790 let state: DockAreaState = serde_json::from_str(json).unwrap();
791 let mut panels = PreservingPanels::default();
792 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
793
794 let dumped = tree.to_state(&panels);
795 let mut rebuilt_panels = PreservingPanels::default();
796 let rebuilt = PaneTree::from_state(&dumped, RootKind::Split, &mut rebuilt_panels);
797
798 assert_eq!(
799 tree.to_state(&panels),
800 rebuilt.to_state(&rebuilt_panels),
801 "load(dump(t)) must describe the same layout as t"
802 );
803 }
804
805 #[test]
813 fn a_zero_size_slot_round_trips_through_the_none_sentinel() {
814 let json = include_str!("fixtures/zero_size_sentinel.json");
815 let state: DockAreaState = serde_json::from_str(json).unwrap();
816 let mut panels = PreservingPanels::default();
817 let tree = PaneTree::from_state(&state.center, RootKind::Split, &mut panels);
818
819 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
820 panic!("expected the root to stay a split");
821 };
822 assert_eq!(
823 sizes,
824 &[None, Some(px(200.))],
825 "the 0.0 sentinel reads back as None, not as a real zero-width panel"
826 );
827
828 let dumped = tree.to_state(&panels);
829 let PanelInfo::Stack { sizes, .. } = &dumped.info else {
830 panic!("expected Stack info");
831 };
832 assert_eq!(
833 sizes,
834 &vec![px(0.), px(200.)],
835 "the unconstrained slot writes back out as the 0.0 sentinel"
836 );
837 }
838
839 #[test]
858 fn a_bare_tab_panel_root_is_not_wrapped_under_root_kind_any() {
859 let json = include_str!("fixtures/bare_tab_panel_root.json");
860 let state: PanelState = serde_json::from_str(json).unwrap();
861 let mut panels = PreservingPanels::default();
862 let tree = PaneTree::from_state(&state, RootKind::Any, &mut panels);
863
864 assert!(
865 matches!(tree.root().kind(), PaneRef::Tabs { .. }),
866 "a bare TabPanel root must stay a Tabs node under RootKind::Any, \
867 not get wrapped in a synthetic split"
868 );
869
870 let dumped = tree.to_state(&panels);
871 assert_eq!(dumped.panel_name, "TabPanel");
872 }
873}