rustmotion_core/engine/box_tree.rs
1//! Box tree — the intermediate representation between a JSON scenario and
2//! the layout/paint passes. Each node carries a resolved [`CssStyle`], a
3//! discriminator pointing to the source component, and an optional intrinsic
4//! measurement callback (text, image, codeblock, chart, ...).
5
6use std::sync::Arc;
7
8use crate::css::CssStyle;
9
10/// Stable identifier for a node within a single layout pass.
11pub type NodeId = u32;
12
13/// A renderable box. `kind` is opaque to the engine; the painter dispatch
14/// downcasts the inner `Arc<dyn Any>` to the concrete component type when
15/// invoked.
16pub struct BoxNode {
17 pub id: NodeId,
18 pub kind: BoxKind,
19 pub css: CssStyle,
20 pub children: Vec<BoxNode>,
21 /// Optional intrinsic measurement (used by taffy's `measure_fn` for
22 /// leaves like text / codeblock / image). `None` = pure container.
23 pub intrinsic: Option<Arc<dyn IntrinsicMeasure>>,
24 /// JSON path of this node relative to its scene's `children` array, e.g.
25 /// "/children/2/children/0". `None` for synthetic nodes (the scene root).
26 pub source_path: Option<String>,
27 /// Visibility window from the component's `start_at`/`end_at` (seconds,
28 /// scene-relative). Outside the window the node and its subtree are not
29 /// painted but still occupy layout space (CSS `visibility` semantics —
30 /// siblings must not jump when the component appears).
31 pub window: Option<PaintWindow>,
32}
33
34/// Half-open visibility window `[start, end)`; `None` bounds are unbounded.
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct PaintWindow {
37 pub start: Option<f64>,
38 pub end: Option<f64>,
39}
40
41impl PaintWindow {
42 pub fn contains(&self, t: f64) -> bool {
43 self.start.is_none_or(|s| t >= s) && self.end.is_none_or(|e| t < e)
44 }
45}
46
47impl BoxNode {
48 pub fn container(css: CssStyle, children: Vec<BoxNode>) -> Self {
49 Self {
50 id: 0,
51 kind: BoxKind::Container,
52 css,
53 children,
54 intrinsic: None,
55 source_path: None,
56 window: None,
57 }
58 }
59
60 pub fn leaf(css: CssStyle, intrinsic: Arc<dyn IntrinsicMeasure>) -> Self {
61 Self {
62 id: 0,
63 kind: BoxKind::Container,
64 css,
65 children: Vec::new(),
66 intrinsic: Some(intrinsic),
67 source_path: None,
68 window: None,
69 }
70 }
71
72 /// Walk the tree and assign sequential `id` values to each node.
73 /// Returns the next free id.
74 pub fn assign_ids(&mut self, mut next: NodeId) -> NodeId {
75 self.id = next;
76 next += 1;
77 for c in self.children.iter_mut() {
78 next = c.assign_ids(next);
79 }
80 next
81 }
82
83 /// Find a node by id.
84 pub fn find(&self, id: NodeId) -> Option<&BoxNode> {
85 if self.id == id {
86 return Some(self);
87 }
88 for c in &self.children {
89 if let Some(r) = c.find(id) {
90 return Some(r);
91 }
92 }
93 None
94 }
95}
96
97/// Discriminates the source component without coupling the engine to its
98/// concrete types.
99#[derive(Clone)]
100pub enum BoxKind {
101 /// Generic container — no custom paint, only box decorations.
102 Container,
103 /// Component-backed leaf or container. Holds an opaque payload that the
104 /// dispatcher knows how to handle.
105 Component(Arc<dyn std::any::Any + Send + Sync>),
106 /// Temporal ghost for motion-blur / trail effects. Painted exactly like
107 /// `Component` (same payload, same dispatcher dispatch) but excluded from
108 /// the hit-map so the studio never selects a ghost node.
109 Ghost(Arc<dyn std::any::Any + Send + Sync>),
110}
111
112/// Trait implemented by leaves whose intrinsic size depends on their content.
113/// Called by taffy during layout.
114pub trait IntrinsicMeasure: Send + Sync {
115 /// Measure intrinsic size given the available width/height.
116 /// Either side may be `None` if unconstrained (e.g. min-content pass).
117 fn measure(
118 &self,
119 known: (Option<f32>, Option<f32>),
120 available: (AvailableSpace, AvailableSpace),
121 ) -> (f32, f32);
122}
123
124/// CSS available-space hint, mirrored from taffy.
125#[derive(Debug, Clone, Copy, PartialEq)]
126pub enum AvailableSpace {
127 Definite(f32),
128 MinContent,
129 MaxContent,
130}
131
132impl From<taffy::AvailableSpace> for AvailableSpace {
133 fn from(v: taffy::AvailableSpace) -> Self {
134 match v {
135 taffy::AvailableSpace::Definite(p) => Self::Definite(p),
136 taffy::AvailableSpace::MinContent => Self::MinContent,
137 taffy::AvailableSpace::MaxContent => Self::MaxContent,
138 }
139 }
140}