1use std::sync::Arc;
2
3use crate::error::{PaneError, TreeError};
4use crate::node::{Node, NodeId, PanelKey};
5use crate::overlay::{OverlayDef, SnapshotOverlay};
6use crate::panel::Axis;
7use crate::panel::Constraints;
8use crate::strategy::{ActivePanelVariant, CardSpan, GridColumnMode, SlotDef, StrategyKind};
9use crate::tree::LayoutTree;
10use crate::validate::{check_f32_non_negative, float_invalid_to_constraint};
11
12#[derive(Debug, Clone)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct LayoutSnapshot {
35 source: SnapshotSource,
36 focused: Option<Box<str>>,
37 collapsed: Box<[Box<str>]>,
38 #[cfg_attr(
41 feature = "serde",
42 serde(default, skip_serializing_if = "Option::is_none")
43 )]
44 focused_key: Option<PanelKey>,
45 #[cfg_attr(
48 feature = "serde",
49 serde(default, skip_serializing_if = "is_box_slice_empty")
50 )]
51 collapsed_keys: Box<[PanelKey]>,
52 #[cfg_attr(
53 feature = "serde",
54 serde(default, skip_serializing_if = "is_box_slice_empty")
55 )]
56 overlays: Box<[SnapshotOverlay]>,
57}
58
59#[cfg(feature = "serde")]
60fn is_box_slice_empty<T>(s: &[T]) -> bool {
61 s.is_empty()
62}
63
64impl LayoutSnapshot {
65 pub fn source(&self) -> &SnapshotSource {
67 &self.source
68 }
69
70 pub fn focused(&self) -> Option<&str> {
72 self.focused.as_deref()
73 }
74
75 pub fn collapsed(&self) -> &[Box<str>] {
77 &self.collapsed
78 }
79
80 pub fn focused_key(&self) -> Option<PanelKey> {
82 self.focused_key
83 }
84
85 pub fn collapsed_keys(&self) -> &[PanelKey] {
87 &self.collapsed_keys
88 }
89
90 pub fn overlays(&self) -> &[SnapshotOverlay] {
92 &self.overlays
93 }
94
95 pub fn into_overlays(self) -> Vec<SnapshotOverlay> {
97 self.overlays.into_vec()
98 }
99}
100
101#[derive(Debug, Clone)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106pub enum SnapshotSource {
107 Strategy {
109 strategy: StrategyConfig,
110 panels: Box<[Box<str>]>,
111 },
112 Tree { root: SnapshotNode },
114 Adaptive {
116 breakpoints: Box<[SnapshotBreakpoint]>,
117 panels: Box<[Box<str>]>,
118 active_index: usize,
119 },
120}
121
122#[derive(Debug, Clone)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
125pub struct SnapshotBreakpoint {
126 pub min_width: u32,
127 pub strategy: StrategyConfig,
128}
129
130#[derive(Debug, Clone)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub enum StrategyConfig {
138 Sequence {
139 axis: Axis,
140 gap: f32,
141 #[cfg_attr(
142 feature = "serde",
143 serde(default, skip_serializing_if = "Option::is_none")
144 )]
145 ratio: Option<f32>,
146 },
147 MasterStack {
148 master_ratio: f32,
149 gap: f32,
150 },
151 Deck {
152 master_ratio: f32,
153 gap: f32,
154 },
155 CenteredMaster {
156 master_ratio: f32,
157 gap: f32,
158 },
159 BinarySplit {
160 spiral: bool,
161 ratio: f32,
162 gap: f32,
163 },
164 Dashboard {
165 columns: GridColumnMode,
166 gap: f32,
167 spans: Box<[CardSpan]>,
168 #[cfg_attr(feature = "serde", serde(default))]
169 auto_rows: bool,
170 },
171 ActivePanel {
172 variant: ActivePanelVariant,
173 bar_height: f32,
174 },
175 Window {
176 panel_count: usize,
177 gap: f32,
178 },
179 Slotted {
180 slots: Box<[SnapshotSlotDef]>,
181 gap: f32,
182 axis: Axis,
183 },
184}
185
186#[derive(Debug, Clone)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189pub struct SnapshotSlotDef {
190 pub kind: Box<str>,
191 pub constraints: Constraints,
192}
193
194#[derive(Debug, Clone)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200pub struct SnapshotGridItem {
201 #[cfg_attr(
202 feature = "serde",
203 serde(default, skip_serializing_if = "Option::is_none")
204 )]
205 pub span: Option<CardSpan>,
206 pub node: SnapshotNode,
207}
208
209#[derive(Debug, Clone)]
214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
215pub enum SnapshotNode {
216 Panel {
217 kind: Box<str>,
218 constraints: Constraints,
219 },
220 Row {
221 gap: f32,
222 #[cfg_attr(
223 feature = "serde",
224 serde(default, skip_serializing_if = "Option::is_none")
225 )]
226 constraints: Option<Constraints>,
227 children: Box<[SnapshotNode]>,
228 },
229 Col {
230 gap: f32,
231 #[cfg_attr(
232 feature = "serde",
233 serde(default, skip_serializing_if = "Option::is_none")
234 )]
235 constraints: Option<Constraints>,
236 children: Box<[SnapshotNode]>,
237 },
238 Grid {
239 columns: GridColumnMode,
240 gap: f32,
241 #[cfg_attr(feature = "serde", serde(default))]
242 auto_rows: bool,
243 children: Box<[SnapshotGridItem]>,
244 },
245}
246
247macro_rules! strategy_convert {
256 (
257 copy: [ $( $variant:ident { $($field:ident),* } ),* $(,)? ],
259 custom_to_config: [ $($to_config_arm:tt)* ],
261 custom_to_kind: [ $($to_kind_arm:tt)* ],
262 ) => {
263 impl From<&StrategyKind> for StrategyConfig {
264 fn from(sk: &StrategyKind) -> Self {
265 match sk {
266 $(
267 StrategyKind::$variant { $($field),* } =>
268 StrategyConfig::$variant { $($field: *$field),* },
269 )*
270 $($to_config_arm)*
271 }
272 }
273 }
274
275 impl From<&StrategyConfig> for StrategyKind {
276 fn from(sc: &StrategyConfig) -> Self {
277 match sc {
278 $(
279 StrategyConfig::$variant { $($field),* } =>
280 StrategyKind::$variant { $($field: *$field),* },
281 )*
282 $($to_kind_arm)*
283 }
284 }
285 }
286 };
287}
288
289strategy_convert! {
290 copy: [
291 Sequence { axis, gap, ratio },
292 MasterStack { master_ratio, gap },
293 Deck { master_ratio, gap },
294 CenteredMaster { master_ratio, gap },
295 BinarySplit { spiral, ratio, gap },
296 ActivePanel { variant, bar_height },
297 Window { panel_count, gap },
298 ],
299 custom_to_config: [
300 StrategyKind::Dashboard { columns, gap, spans, auto_rows } => StrategyConfig::Dashboard {
301 columns: *columns, gap: *gap, spans: Box::from(&**spans), auto_rows: *auto_rows,
302 },
303 StrategyKind::Slotted { slots, gap, axis } => StrategyConfig::Slotted {
304 slots: slots.iter().map(|s| SnapshotSlotDef {
305 kind: Box::from(&*s.kind), constraints: s.constraints,
306 }).collect::<Box<[_]>>(),
307 gap: *gap, axis: *axis,
308 },
309 ],
310 custom_to_kind: [
311 StrategyConfig::Dashboard { columns, gap, spans, auto_rows } => StrategyKind::Dashboard {
312 columns: *columns, gap: *gap, spans: Arc::from(&**spans), auto_rows: *auto_rows,
313 },
314 StrategyConfig::Slotted { slots, gap, axis } => StrategyKind::Slotted {
315 slots: slots.iter().map(|s| SlotDef {
316 kind: Arc::from(&*s.kind), constraints: s.constraints,
317 }).collect::<Arc<[_]>>(),
318 gap: *gap, axis: *axis,
319 },
320 ],
321}
322
323const MAX_SNAPSHOT_DEPTH: usize = 64;
329
330pub(crate) fn tree_to_snapshot(tree: &LayoutTree) -> Result<Option<SnapshotNode>, PaneError> {
333 let Some(root) = tree.root() else {
334 return Ok(None);
335 };
336 node_to_snapshot(tree, root, 0).map(Some)
337}
338
339fn container_snapshot(
340 is_row: bool,
341 gap: f32,
342 constraints: Option<Constraints>,
343 children: Box<[SnapshotNode]>,
344) -> SnapshotNode {
345 match is_row {
346 true => SnapshotNode::Row {
347 gap,
348 constraints,
349 children,
350 },
351 false => SnapshotNode::Col {
352 gap,
353 constraints,
354 children,
355 },
356 }
357}
358
359fn node_to_snapshot(
360 tree: &LayoutTree,
361 nid: NodeId,
362 depth: usize,
363) -> Result<SnapshotNode, PaneError> {
364 if depth > MAX_SNAPSHOT_DEPTH {
365 return Err(PaneError::InvalidTree(TreeError::SnapshotTooDeep(
366 MAX_SNAPSHOT_DEPTH,
367 )));
368 }
369 let Some(node) = tree.node(nid) else {
370 return Err(PaneError::NodeNotFound(nid));
371 };
372 match node {
373 Node::Grid {
374 columns,
375 gap,
376 auto_rows,
377 children,
378 } => {
379 let snap_children = children
380 .iter()
381 .map(|&child_nid| grid_child_to_snapshot(tree, child_nid, depth + 1))
382 .collect::<Result<Vec<_>, _>>()?
383 .into_boxed_slice();
384 Ok(SnapshotNode::Grid {
385 columns: *columns,
386 gap: *gap,
387 auto_rows: *auto_rows,
388 children: snap_children,
389 })
390 }
391 Node::TaffyPassthrough { .. } | Node::GridItemWrapper { .. } => Err(
392 PaneError::InvalidTree(TreeError::UnsupportedSnapshotNode(nid)),
393 ),
394 Node::Panel {
395 kind, constraints, ..
396 } => Ok(SnapshotNode::Panel {
397 kind: Box::from(&**kind),
398 constraints: *constraints,
399 }),
400 Node::Row {
401 gap,
402 constraints,
403 children,
404 }
405 | Node::Col {
406 gap,
407 constraints,
408 children,
409 } => {
410 let is_row = matches!(node, Node::Row { .. });
411 let snap_children = children
412 .iter()
413 .map(|&child_id| node_to_snapshot(tree, child_id, depth + 1))
414 .collect::<Result<Vec<_>, _>>()?
415 .into_boxed_slice();
416 Ok(container_snapshot(
417 is_row,
418 *gap,
419 *constraints,
420 snap_children,
421 ))
422 }
423 }
424}
425
426fn grid_child_to_snapshot(
431 tree: &LayoutTree,
432 nid: NodeId,
433 depth: usize,
434) -> Result<SnapshotGridItem, PaneError> {
435 let Some(node) = tree.node(nid) else {
436 return Err(PaneError::NodeNotFound(nid));
437 };
438 match node {
439 Node::GridItemWrapper { span, child } => {
440 let inner = node_to_snapshot(tree, *child, depth)?;
441 Ok(SnapshotGridItem {
442 span: Some(*span),
443 node: inner,
444 })
445 }
446 _ => {
447 let inner = node_to_snapshot(tree, nid, depth)?;
448 Ok(SnapshotGridItem {
449 span: None,
450 node: inner,
451 })
452 }
453 }
454}
455
456pub(crate) fn snapshot_to_tree(root: &SnapshotNode) -> Result<LayoutTree, PaneError> {
462 let mut tree = LayoutTree::new();
463 let root_id = snapshot_node_to_tree(&mut tree, root, 0)?;
464 tree.set_root(root_id);
465 tree.validate()?;
466 Ok(tree)
467}
468
469fn snapshot_node_to_tree(
470 tree: &mut LayoutTree,
471 snapshot_node: &SnapshotNode,
472 depth: usize,
473) -> Result<NodeId, PaneError> {
474 if depth > MAX_SNAPSHOT_DEPTH {
475 return Err(PaneError::InvalidTree(TreeError::SnapshotTooDeep(
476 MAX_SNAPSHOT_DEPTH,
477 )));
478 }
479
480 match snapshot_node {
481 SnapshotNode::Panel { kind, constraints } => {
482 let (_, node_id) = tree.add_panel(&**kind, *constraints)?;
483 Ok(node_id)
484 }
485 SnapshotNode::Row {
486 gap,
487 constraints,
488 children,
489 } => {
490 validate_snapshot_gap(*gap)?;
491 let child_ids = snapshot_children_to_tree(tree, children, depth)?;
492 tree.add_row_constrained(*gap, *constraints, child_ids)
493 }
494 SnapshotNode::Col {
495 gap,
496 constraints,
497 children,
498 } => {
499 validate_snapshot_gap(*gap)?;
500 let child_ids = snapshot_children_to_tree(tree, children, depth)?;
501 tree.add_col_constrained(*gap, *constraints, child_ids)
502 }
503 SnapshotNode::Grid {
504 columns,
505 gap,
506 auto_rows,
507 children,
508 } => {
509 crate::preset::validate_grid_columns(*columns)?;
510 validate_snapshot_gap(*gap)?;
511 let child_ids = grid_children_to_tree(tree, children, depth)?;
512 tree.add_grid(*columns, *gap, *auto_rows, child_ids)
513 }
514 }
515}
516
517fn snapshot_children_to_tree(
518 tree: &mut LayoutTree,
519 children: &[SnapshotNode],
520 depth: usize,
521) -> Result<Vec<NodeId>, PaneError> {
522 children
523 .iter()
524 .map(|child| snapshot_node_to_tree(tree, child, depth + 1))
525 .collect()
526}
527
528fn grid_children_to_tree(
529 tree: &mut LayoutTree,
530 children: &[SnapshotGridItem],
531 depth: usize,
532) -> Result<Vec<NodeId>, PaneError> {
533 children
534 .iter()
535 .map(|grid_item| grid_item_to_tree(tree, grid_item, depth + 1))
536 .collect()
537}
538
539fn grid_item_to_tree(
540 tree: &mut LayoutTree,
541 grid_item: &SnapshotGridItem,
542 depth: usize,
543) -> Result<NodeId, PaneError> {
544 if depth > MAX_SNAPSHOT_DEPTH {
545 return Err(PaneError::InvalidTree(TreeError::SnapshotTooDeep(
546 MAX_SNAPSHOT_DEPTH,
547 )));
548 }
549
550 match (&grid_item.span, &grid_item.node) {
551 (Some(span), SnapshotNode::Panel { kind, constraints }) => {
552 crate::preset::validate_grid_span(*span)?;
553 let (_, panel_id) = tree.add_panel(&**kind, *constraints)?;
554 tree.add_grid_item(*span, panel_id)
555 }
556 (Some(_), snapshot_node) => Err(PaneError::InvalidTree(
557 TreeError::SnapshotSpanRequiresPanel(snapshot_node_kind(snapshot_node)),
558 )),
559 (None, snapshot_node) => snapshot_node_to_tree(tree, snapshot_node, depth),
560 }
561}
562
563fn validate_snapshot_gap(gap: f32) -> Result<(), PaneError> {
564 check_f32_non_negative(gap)
565 .map_err(|error| PaneError::InvalidConstraint(float_invalid_to_constraint("gap", error)))
566}
567
568fn snapshot_node_kind(snapshot_node: &SnapshotNode) -> &'static str {
569 match snapshot_node {
570 SnapshotNode::Panel { .. } => "panel",
571 SnapshotNode::Row { .. } => "row",
572 SnapshotNode::Col { .. } => "col",
573 SnapshotNode::Grid { .. } => "grid",
574 }
575}
576
577pub(crate) fn capture(
583 tree: &LayoutTree,
584 strategy: Option<&StrategyKind>,
585 sequence: &crate::sequence::PanelSequence,
586 viewport: &crate::viewport::ViewportState,
587 overlay_defs: &[OverlayDef],
588 breakpoints: Option<(&[crate::breakpoint::BreakpointEntry], usize)>,
589) -> Result<LayoutSnapshot, PaneError> {
590 let focused = viewport
591 .focus
592 .map(|pid| tree.panel_kind(pid).map(Box::from))
593 .transpose()?;
594
595 let focused_key = match sequence.is_empty() {
596 true => None,
597 false => viewport
598 .focus
599 .map(|pid| {
600 sequence
601 .index_of(pid)
602 .map(|idx| PanelKey::from_raw(idx as u32))
603 .ok_or(PaneError::InvalidTree(
604 TreeError::SnapshotFocusedMissingFromSequence(pid),
605 ))
606 })
607 .transpose()?,
608 };
609
610 let collapsed: Box<[Box<str>]> = viewport
611 .collapsed
612 .iter()
613 .map(|&pid| tree.panel_kind(pid).map(Box::from))
614 .collect::<Result<Vec<_>, _>>()?
615 .into_boxed_slice();
616
617 let collapsed_keys: Box<[PanelKey]> = match sequence.is_empty() {
618 true => Box::default(),
619 false => viewport
620 .collapsed
621 .iter()
622 .map(|&pid| {
623 sequence
624 .index_of(pid)
625 .map(|idx| PanelKey::from_raw(idx as u32))
626 .ok_or(PaneError::InvalidTree(
627 TreeError::SnapshotCollapsedMissingFromSequence(pid),
628 ))
629 })
630 .collect::<Result<Vec<_>, _>>()?
631 .into_boxed_slice(),
632 };
633
634 let panels_box = || -> Result<Box<[Box<str>]>, PaneError> {
635 Ok(sequence
636 .iter()
637 .map(|pid| tree.panel_kind(pid).map(Box::from))
638 .collect::<Result<Vec<_>, _>>()?
639 .into_boxed_slice())
640 };
641
642 let source = match (breakpoints, strategy) {
643 (Some((bps, active_index)), _) => {
644 let snap_bps: Box<[SnapshotBreakpoint]> = bps
645 .iter()
646 .map(|bp| SnapshotBreakpoint {
647 min_width: bp.min_width,
648 strategy: StrategyConfig::from(&bp.strategy),
649 })
650 .collect();
651 SnapshotSource::Adaptive {
652 breakpoints: snap_bps,
653 panels: panels_box()?,
654 active_index,
655 }
656 }
657 (None, Some(sk)) => SnapshotSource::Strategy {
658 strategy: StrategyConfig::from(sk),
659 panels: panels_box()?,
660 },
661 (None, None) => {
662 let root =
663 tree_to_snapshot(tree)?.ok_or(PaneError::InvalidTree(TreeError::SnapshotNoRoot))?;
664 SnapshotSource::Tree { root }
665 }
666 };
667
668 let overlays: Box<[SnapshotOverlay]> = overlay_defs
669 .iter()
670 .map(|def| SnapshotOverlay {
671 kind: Box::from(&*def.kind),
672 anchor: def.anchor.clone(),
673 width: def.width,
674 height: def.height,
675 visible: def.visible,
676 })
677 .collect();
678
679 Ok(LayoutSnapshot {
680 source,
681 focused,
682 collapsed,
683 focused_key,
684 collapsed_keys,
685 overlays,
686 })
687}