cranpose_core/node.rs
1use std::any::{Any, TypeId};
2
3use smallvec::SmallVec;
4
5use crate::{
6 Applier, Command, Composer, DirtyBubble, NodeError, NodeId, composer_context,
7 with_current_composer,
8};
9
10/// Runs `f` on the node `id` as an `N`, using the current composer. Returns an error if the node
11/// does not exist or is not an `N`. Must be called during composition.
12pub fn with_node_mut<N: Node + 'static, R>(
13 id: NodeId,
14 f: impl FnOnce(&mut N) -> R,
15) -> Result<R, NodeError> {
16 with_current_composer(|composer| composer.with_node_mut(id, f))
17}
18
19/// Makes the node `id` the parent of the nodes emitted next, using the current composer. Needs a
20/// matching `pop_parent`. Must be called during composition.
21pub fn push_parent(id: NodeId) {
22 with_current_composer(|composer| composer.push_parent(id));
23}
24
25/// Closes the parent that the matching `push_parent` opened, using the current composer. Must be
26/// called during composition.
27pub fn pop_parent() {
28 with_current_composer(Composer::pop_parent);
29}
30
31/// A node of the tree that a composition builds. An applier stores the nodes by id. Every method
32/// has a default, so a simple node implements none of them.
33pub trait Node: Any {
34 /// Called once the node has its id, when the composition applies the node's creation.
35 fn mount(&mut self) {}
36 /// Called just before the node is removed from the applier.
37 fn unmount(&mut self) {}
38 /// Adds `child` to this node's child list, returning whether the list
39 /// actually changed. A node that already holds the child returns `false`:
40 /// callers record a structural change from this answer, and a structural
41 /// change re-lowers the whole subtree, so claiming one for a no-op costs a
42 /// full rebuild per call.
43 fn insert_child(&mut self, _child: NodeId) -> bool {
44 false
45 }
46 /// Removes `child` from this node's child list, returning whether the list
47 /// actually changed. Only this node can answer that: a caller inspecting
48 /// the child's parent pointer gets it wrong when the child was reparented
49 /// while still listed here.
50 fn remove_child(&mut self, _child: NodeId) -> bool {
51 false
52 }
53 /// Moves the child at the first index to the second index in the child list.
54 fn move_child(&mut self, _from: usize, _to: usize) {}
55 /// Replaces the child list with `children`, in order.
56 fn update_children(&mut self, _children: &[NodeId]) {}
57 /// Whether the node adds and drops its children itself, as a
58 /// subcomposition host does with its slot hosts: the child sync a
59 /// composition pass ends with then leaves its list alone. The default
60 /// keeps the list the passes compose.
61 fn keeps_own_children(&self) -> bool {
62 false
63 }
64 /// Copies child IDs into the provided scratch buffer without allocating a
65 /// fresh container for every traversal. A node without children leaves
66 /// the buffer empty.
67 fn collect_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
68 out.clear();
69 }
70 /// Runs `f` on each child id in the order [`Node::collect_children_into`]
71 /// lists them. A node that holds its list in place overrides this to
72 /// visit it without a copy.
73 fn for_each_child(&self, f: &mut dyn FnMut(NodeId)) {
74 let mut children = SmallVec::<[NodeId; 8]>::new();
75 self.collect_children_into(&mut children);
76 for child in children {
77 f(child);
78 }
79 }
80 /// Copies the ids of the children this node owns, in order, into `out`. A node whose
81 /// `collect_children_into` leaves some children out overrides this to list them all. The
82 /// default calls `collect_children_into`.
83 fn collect_owned_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
84 self.collect_children_into(out);
85 }
86 /// Finds a child in the order returned by [`Node::collect_owned_children_into`].
87 /// Nodes with slice-backed child storage should override this to avoid
88 /// materializing the whole list for a single lookup.
89 ///
90 /// ```
91 /// use cranpose_core::{Node, NodeId};
92 ///
93 /// fn child_is_first(parent: &dyn Node, child: NodeId) -> bool {
94 /// parent.owned_child_index(child) == Some(0)
95 /// }
96 /// ```
97 fn owned_child_index(&self, child: NodeId) -> Option<usize> {
98 let mut children = SmallVec::<[NodeId; 8]>::new();
99 self.collect_owned_children_into(&mut children);
100 children.iter().position(|&id| id == child)
101 }
102 /// Called after the node is created to record its own ID.
103 /// Useful for nodes that need to store their ID for later operations.
104 fn set_node_id(&mut self, _id: NodeId) {}
105 /// Called when this node is attached to a parent.
106 /// Nodes with parent tracking should set their parent reference here.
107 fn on_attached_to_parent(&mut self, _parent: NodeId) {}
108 /// Called when this node is removed from its parent.
109 /// Nodes with parent tracking should clear their parent reference here.
110 fn on_removed_from_parent(&mut self) {}
111 /// Get this node's parent ID (for nodes that track parents).
112 /// Returns None if node has no parent or doesn't track parents.
113 fn parent(&self) -> Option<NodeId> {
114 None
115 }
116 /// Mark this node as needing layout (for nodes with dirty flags).
117 /// Called during bubbling to propagate dirtiness up the tree.
118 fn mark_needs_layout(&self) {}
119 /// Check if this node needs layout (for nodes with dirty flags).
120 fn needs_layout(&self) -> bool {
121 false
122 }
123 /// Mark this node as needing measure (size may have changed).
124 /// Called during bubbling when children are added/removed.
125 fn mark_needs_measure(&self) {}
126 /// Check if this node needs measure (for nodes with dirty flags).
127 fn needs_measure(&self) -> bool {
128 false
129 }
130 /// Mark that a node below this one needs layout, or measure when
131 /// `measure` is set: a layout pass has to visit this node to reach it,
132 /// while this node's own measurement may still hold. A node that does not
133 /// track the difference marks itself dirty.
134 fn mark_descendant_needs_layout(&self, measure: bool) {
135 if measure {
136 self.mark_needs_measure();
137 } else {
138 self.mark_needs_layout();
139 }
140 }
141 /// Check if a node below this one needs layout or measure.
142 fn descendant_needs_layout(&self) -> bool {
143 false
144 }
145 /// Check if a node below this one needs measure.
146 fn descendant_needs_measure(&self) -> bool {
147 false
148 }
149 /// Check if a layout pass has to visit this node: the node or a node
150 /// below it needs layout or measure.
151 fn layout_dirty(&self) -> bool {
152 self.needs_measure() || self.needs_layout() || self.descendant_needs_layout()
153 }
154 /// Whether the node lays out nothing itself and its parent lays out its
155 /// children, so a change of its own is a change of its parent's.
156 fn is_virtual(&self) -> bool {
157 false
158 }
159 /// Mark this node as needing semantics recomputation.
160 fn mark_needs_semantics(&self) {}
161 /// Mark that the semantics tree has to read this node again while its
162 /// own semantics are unchanged: a node below it changed its semantics,
163 /// or its own placement or children changed.
164 fn mark_descendant_needs_semantics(&self) {
165 self.mark_needs_semantics();
166 }
167 /// Check if this node or a node below it needs semantics recomputation.
168 fn needs_semantics(&self) -> bool {
169 false
170 }
171 /// Set parent reference for dirty flag bubbling ONLY.
172 /// This is a minimal version of on_attached_to_parent that doesn't trigger
173 /// registry updates or other side effects. Used during measurement when we
174 /// need to establish parent connections for bubble_measure_dirty without
175 /// causing the full attachment lifecycle.
176 ///
177 /// An existing parent is preserved until the structural attach operation
178 /// removes the node from its previous container.
179 fn set_parent_for_bubbling(&mut self, parent: NodeId) {
180 if self.parent().is_none() {
181 self.on_attached_to_parent(parent);
182 }
183 }
184
185 /// Returns a recycle pool key when this node supports shell reuse.
186 fn recycle_key(&self) -> Option<TypeId> {
187 None
188 }
189
190 /// Bounds how many recyclable shells of this node type should be retained.
191 fn recycle_pool_limit(&self) -> Option<usize> {
192 None
193 }
194
195 /// Clears live attachments before the node shell enters a recycle pool.
196 fn prepare_for_recycle(&mut self) {}
197
198 /// Optionally provides a compact replacement box for this recycled shell.
199 ///
200 /// Returning `Some` lets nodes move pooled survivors onto fresh compact
201 /// storage so the recycle pool does not pin large spike-era allocations.
202 fn rehouse_for_recycle(&self) -> Option<Box<dyn Node>> {
203 None
204 }
205
206 /// Optionally moves a live node onto a fresh box during applier compaction.
207 ///
208 /// This is used after large-majority teardowns where a small surviving live
209 /// tree can otherwise pin allocator pages from a much larger spike-era node
210 /// population. Implementations must preserve the node's live state.
211 fn rehouse_for_live_compaction(&mut self) -> Option<Box<dyn Node>> {
212 None
213 }
214
215 /// Returns the node-owned heap retained beyond the node's own box allocation.
216 fn debug_heap_bytes(&self) -> usize {
217 0
218 }
219}
220
221// forwards on purpose: DirtyBubble is crate-private, and this is the public entry
222/// Unified API for bubbling layout dirty flags from a node to the root (Applier context).
223///
224/// This is the canonical function for dirty bubbling during the apply phase (structural changes).
225/// Call this after mutations like insert/remove/move that happen during apply.
226///
227/// # Behavior
228/// 1. Marks the starting node as needing layout
229/// 2. Walks up the parent chain, marking each ancestor
230/// 3. Stops at the root (node with no parent)
231///
232/// # Performance
233/// This function is O(height): a scoped layout repass can leave a dirty node
234/// under a clean ancestor, so the walk does not stop at a dirty ancestor.
235///
236/// # Usage
237/// - Call from composer mutations (insert/remove/move) during apply phase
238/// - Call from applier-level operations that modify the tree structure
239pub fn bubble_layout_dirty(applier: &mut dyn Applier, node_id: NodeId) {
240 DirtyBubble::LAYOUT.apply(applier, node_id);
241}
242
243// forwards on purpose: DirtyBubble is crate-private, and this is the public entry
244/// Unified API for bubbling measure dirty flags from a node to the root (Applier context).
245///
246/// Call this when a node's size may have changed (children added/removed, modifier changed).
247/// This ensures that measure_layout will increment the cache epoch and re-measure the subtree.
248///
249/// # Behavior
250/// 1. Marks the starting node as needing measure
251/// 2. Walks up the parent chain, marking each ancestor
252/// 3. Stops at the root (node with no parent)
253pub fn bubble_measure_dirty(applier: &mut dyn Applier, node_id: NodeId) {
254 DirtyBubble::MEASURE.apply(applier, node_id);
255}
256
257// forwards on purpose: DirtyBubble is crate-private, and this is the public entry
258/// Unified API for bubbling semantics dirty flags from a node to the root (Applier context).
259///
260/// This mirrors [`bubble_layout_dirty`] but toggles semantics-specific dirty
261/// flags instead of layout ones, allowing semantics updates to propagate during
262/// the apply phase without forcing layout work.
263pub fn bubble_semantics_dirty(applier: &mut dyn Applier, node_id: NodeId) {
264 DirtyBubble::SEMANTICS.apply(applier, node_id);
265}
266
267/// Schedules semantics bubbling for a node using the active composer if present.
268///
269/// This defers the work to the apply phase where we can safely mutate the
270/// applier tree without re-entrantly borrowing the composer during composition.
271pub fn queue_semantics_invalidation(node_id: NodeId) {
272 let _ = composer_context::try_with_composer(|composer| {
273 composer.enqueue_semantics_invalidation(node_id);
274 });
275}
276
277/// Unified API for bubbling layout dirty flags from a node to the root (Composer context).
278///
279/// This is the canonical function for dirty bubbling during composition (property changes).
280/// Call this after property changes that happen during composition via with_node_mut.
281///
282/// # Behavior
283/// 1. Marks the starting node as needing layout
284/// 2. Walks up the parent chain, marking each ancestor
285/// 3. Stops at the root (node with no parent)
286///
287/// # Performance
288/// This function is O(height): a scoped layout repass can leave a dirty node
289/// under a clean ancestor, so the walk does not stop at a dirty ancestor.
290///
291/// # Type Requirements
292/// The node type N must implement Node (which includes mark_needs_layout, parent, etc.).
293/// Typically this will be LayoutNode or similar layout-aware node types.
294///
295/// # Usage
296/// - Call from property setters during composition (e.g., set_modifier, set_measure_policy)
297/// - Call from widget composition when layout-affecting state changes
298pub fn bubble_layout_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
299 bubble_layout_dirty_composer::<N>(node_id);
300}
301
302/// Unified API for bubbling measure dirty flags from a node to the root during composition.
303///
304/// This queues a dirty-bubble command on the active composer so measure invalidation
305/// runs during the apply phase, avoiding re-entrant applier borrows while widgets are
306/// mutating nodes via `with_node_mut`.
307pub fn bubble_measure_dirty_in_composer(node_id: NodeId) {
308 with_current_composer(|composer| {
309 composer.commands_mut().push(Command::BubbleDirty {
310 node_id,
311 bubble: DirtyBubble {
312 layout: false,
313 measure: true,
314 semantics: false,
315 },
316 });
317 });
318}
319
320/// Unified API for bubbling semantics dirty flags from a node to the root (Composer context).
321///
322/// This mirrors [`bubble_layout_dirty_in_composer`] but routes through the semantics
323/// dirty flag instead of the layout one. Modifier nodes can request semantics
324/// invalidations without triggering measure/layout work, and the runtime can
325/// query the root to determine whether the semantics tree needs rebuilding.
326pub fn bubble_semantics_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
327 bubble_semantics_dirty_composer::<N>(node_id);
328}
329
330fn bubble_layout_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
331 let _ = with_node_mut(node_id, |node: &mut N| {
332 node.mark_needs_layout();
333 });
334 with_current_composer(|composer| composer.borrow_applier().note_layout_dirt(node_id));
335
336 while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
337 let parent_id = pid;
338
339 let advanced = with_node_mut(parent_id, |node: &mut N| {
340 node.mark_descendant_needs_layout(false);
341 true
342 })
343 .unwrap_or(false);
344
345 if advanced {
346 node_id = parent_id;
347 } else {
348 break;
349 }
350 }
351}
352
353fn bubble_semantics_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
354 let _ = with_node_mut(node_id, |node: &mut N| {
355 node.mark_needs_semantics();
356 });
357
358 while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
359 let parent_id = pid;
360
361 let advanced = with_node_mut(parent_id, |node: &mut N| {
362 if !node.needs_semantics() {
363 node.mark_descendant_needs_semantics();
364 }
365 true
366 })
367 .unwrap_or(false);
368
369 if advanced {
370 node_id = parent_id;
371 } else {
372 break;
373 }
374 }
375}
376
377impl dyn Node {
378 /// Returns the node as `Any`, for downcasting to its concrete type.
379 pub fn as_any_mut(&mut self) -> &mut dyn Any {
380 self
381 }
382}