Skip to main content

cranpose_core/
emit.rs

1use std::any::TypeId;
2
3use crate::{
4    Applier, ChildList, Command, CommandQueue, Composer, DirtyBubble, EmittedNode, MutableState,
5    Node, NodeError, NodeId, OwnedMutableState, ParentAttachMode, ParentFrame, debug_scope_label,
6    recompose::RecomposeChildCursor, slot::NodeSlotUpdate,
7};
8
9impl Composer {
10    fn planned_node_parent(&self, id: NodeId) -> Option<NodeId> {
11        if let Some(parent) = self.current_parent_hint().filter(|parent| *parent != id) {
12            return Some(parent);
13        }
14        let mut applier = self.borrow_applier();
15        applier.get_mut(id).ok().and_then(|node| node.parent())
16    }
17
18    fn queue_replaced_slot_node_removal(&self, old_id: NodeId, old_generation: u32) {
19        let current_generation = self.borrow_applier().node_generation(old_id);
20        if current_generation != old_generation {
21            log::trace!(
22                target: "cranpose::compose::emit",
23                "skipping stale replacement cleanup for node #{old_id} (slot_generation={old_generation} current_generation={current_generation})",
24            );
25            return;
26        }
27
28        log::trace!(
29            target: "cranpose::compose::emit",
30            "removing replaced node #{old_id} (generation={old_generation})",
31        );
32        self.commands_mut().push(Command::RemoveNode { id: old_id });
33    }
34
35    /// Returns a [`MutableState`] holding the value `init` made on the first composition at this
36    /// call site. Later compositions return the same state, so its value survives recomposition.
37    #[track_caller]
38    pub fn use_state<T: Clone + 'static>(&self, init: impl FnOnce() -> T) -> MutableState<T> {
39        let source = crate::caller_location_key();
40        let runtime = self.runtime_handle();
41        let state = self.with_slot_session_mut(|slots| {
42            slots.remember(source, || {
43                OwnedMutableState::with_runtime(init(), runtime.clone())
44            })
45        });
46        state.with(super::state::OwnedMutableState::handle)
47    }
48
49    /// The current scope's id and debug label, for a trace line.
50    fn trace_scope(&self) -> (usize, Option<&'static str>) {
51        self.current_recompose_scope().map_or((0, None), |scope| {
52            (scope.id(), debug_scope_label(scope.id()))
53        })
54    }
55
56    fn emit_node_box<N: Node + 'static>(
57        &self,
58        source: crate::Key,
59        make_node: impl FnOnce(&mut dyn Applier) -> EmittedNode,
60    ) -> NodeId {
61        let adopted = {
62            let mut skip = 0;
63            loop {
64                let Some(found) = self
65                    .with_slot_session_mut(|slots| slots.peek_node_record_by_source(source, skip))
66                else {
67                    break None;
68                };
69                let (type_ok, gen_ok) = {
70                    let mut applier = self.borrow_applier();
71                    let gen_ok = applier.node_generation(found.id) == found.generation;
72                    let type_ok = match applier.get_mut(found.id) {
73                        Ok(node) => node.as_any_mut().downcast_ref::<N>().is_some(),
74                        Err(_) => false,
75                    };
76                    (type_ok, gen_ok)
77                };
78                if type_ok && gen_ok {
79                    break Some(found);
80                }
81                skip += 1;
82            }
83        };
84
85        if let Some(found) = adopted {
86            let id = found.id;
87            if log::log_enabled!(target: "cranpose::compose::emit", log::Level::Trace) {
88                let (scope_id, scope_label) = self.trace_scope();
89                log::trace!(
90                    target: "cranpose::compose::emit",
91                    "reusing node #{id} as {} [scope_id={scope_id} scope_label={scope_label:?}]",
92                    std::any::type_name::<N>(),
93                );
94            }
95            self.attach_to_parent(id);
96            let parent_id = self.planned_node_parent(id);
97            let recorded = self
98                .with_slot_session_mut(|slots| slots.adopt_node_record(found, parent_id, source));
99            match recorded {
100                NodeSlotUpdate::Reused {
101                    id: recorded_id,
102                    generation,
103                } => {
104                    debug_assert_eq!(recorded_id, id);
105                    debug_assert_eq!(generation, found.generation);
106                }
107                NodeSlotUpdate::Inserted { .. } => {
108                    log::warn!(
109                        target: "cranpose::compose::emit",
110                        "slot writer inserted node #{id} while reusing the same node identity",
111                    );
112                }
113                NodeSlotUpdate::Replaced {
114                    old_id,
115                    old_generation,
116                    ..
117                } => {
118                    log::warn!(
119                        target: "cranpose::compose::emit",
120                        "slot writer replaced node #{old_id} while reusing node #{id}",
121                    );
122                    self.queue_replaced_slot_node_removal(old_id, old_generation);
123                }
124            }
125            return id;
126        }
127
128        let (id, generation) = {
129            let mut applier = self.borrow_applier();
130            let emitted = make_node(&mut *applier);
131            let id = match emitted {
132                EmittedNode::Fresh(node) => applier.create(node),
133                EmittedNode::Recycled(recycled) => {
134                    let (stable_id, node, warm_origin) = recycled.into_parts();
135                    let insertion = applier.insert_recycled_node_or_create(stable_id, node);
136                    if let Some(error) = insertion.fallback_error.as_ref() {
137                        log::warn!(
138                            target: "cranpose::compose::emit",
139                            "discarding stale recycled stable id #{stable_id}: {error}",
140                        );
141                    }
142                    applier.set_recycled_node_origin(insertion.id, warm_origin);
143                    insertion.id
144                }
145            };
146            let generation = applier.node_generation(id);
147            (id, generation)
148        };
149        if log::log_enabled!(target: "cranpose::compose::emit", log::Level::Trace) {
150            let (scope_id, scope_label) = self.trace_scope();
151            log::trace!(
152                target: "cranpose::compose::emit",
153                "creating node #{} (gen={}) as {} [scope_id={} scope_label={:?}]",
154                id,
155                generation,
156                std::any::type_name::<N>(),
157                scope_id,
158                scope_label,
159            );
160        }
161        self.commands_mut().push(Command::MountNode { id });
162        self.attach_to_parent(id);
163        let parent_id = self.planned_node_parent(id);
164        let recorded = self.with_slot_session_mut(|slots| {
165            slots.record_node_with_parent(id, generation, parent_id, source)
166        });
167        match recorded {
168            NodeSlotUpdate::Inserted {
169                id: recorded_id,
170                generation: recorded_generation,
171            } => {
172                debug_assert_eq!(recorded_id, id);
173                debug_assert_eq!(recorded_generation, generation);
174            }
175            NodeSlotUpdate::Replaced {
176                old_id,
177                old_generation,
178                new_id,
179                new_generation,
180            } => {
181                debug_assert_eq!(new_id, id);
182                debug_assert_eq!(new_generation, generation);
183                self.queue_replaced_slot_node_removal(old_id, old_generation);
184            }
185            NodeSlotUpdate::Reused { .. } => {
186                log::warn!(
187                    target: "cranpose::compose::emit",
188                    "slot writer reported reuse for newly emitted node #{id}",
189                );
190            }
191        }
192        id
193    }
194
195    /// Emits a node of type `N` as a child of the current parent and returns its id. The first
196    /// composition at this call site calls `init` to make the node. Later compositions reuse that
197    /// node and do not call `init`.
198    #[track_caller]
199    pub fn emit_node<N: Node + 'static>(&self, init: impl FnOnce() -> N) -> NodeId {
200        let source = crate::caller_location_key();
201        self.emit_node_box::<N>(source, |_| EmittedNode::Fresh(Box::new(init())))
202    }
203
204    /// Emits a node of type `N` as `emit_node` does, reusing a pooled node when one exists. A
205    /// pooled `N` is cleared by `reset`, and otherwise `init` makes a new node.
206    #[track_caller]
207    pub fn emit_recyclable_node<N: Node + 'static>(
208        &self,
209        init: impl FnOnce() -> N,
210        reset: impl FnOnce(&mut N),
211    ) -> NodeId {
212        let source = crate::caller_location_key();
213        self.emit_node_box::<N>(source, |applier| {
214            let key = TypeId::of::<N>();
215            if let Some(mut recycled) = applier.take_recycled_node(key) {
216                if let Some(typed) = recycled.node_mut().as_any_mut().downcast_mut::<N>() {
217                    reset(typed);
218                    return EmittedNode::Recycled(recycled);
219                }
220                log::warn!(
221                    target: "cranpose::compose::emit",
222                    "discarding recycled node shell with mismatched type for {}",
223                    std::any::type_name::<N>(),
224                );
225            }
226
227            let node = Box::new(init());
228            applier.record_fresh_recyclable_creation(key);
229            if let Some(shell) = node.rehouse_for_recycle() {
230                applier.seed_recycled_node_shell(key, node.recycle_pool_limit(), shell);
231            }
232            EmittedNode::Fresh(node)
233        })
234    }
235
236    fn skip_recompose_child(&self) {
237        let cursor = match self.core.recompose_child_cursor.get() {
238            RecomposeChildCursor::After { first, placed } => RecomposeChildCursor::After {
239                first,
240                placed: placed + 1,
241            },
242            RecomposeChildCursor::At(index) => RecomposeChildCursor::At(index + 1),
243            RecomposeChildCursor::Unknown => return,
244        };
245        self.core.recompose_child_cursor.set(cursor);
246    }
247
248    fn next_recompose_insert_index(&self) -> Option<usize> {
249        let RecomposeChildCursor::At(index) = self.resolve_recompose_child_cursor() else {
250            return None;
251        };
252        self.core
253            .recompose_child_cursor
254            .set(RecomposeChildCursor::At(index + 1));
255        Some(index)
256    }
257
258    pub(crate) fn attach_to_parent(&self, id: NodeId) {
259        if self.attach_to_current_parent(id) {
260            return;
261        }
262        self.attach_without_parent_frame(id);
263    }
264
265    fn attach_to_current_parent(&self, id: NodeId) -> bool {
266        let mut parent_stack = self.parent_stack();
267        if let Some(parent_id) = parent_stack.last().map(|frame| frame.id) {
268            let stale_root_parent = self.core.root.get() == Some(parent_id) && {
269                let mut applier = self.borrow_applier();
270                applier.get_mut(parent_id).is_err()
271            };
272            if stale_root_parent {
273                parent_stack.pop();
274                self.set_root(None);
275            } else {
276                let Some(frame) = parent_stack.last_mut() else {
277                    return false;
278                };
279                let attach_mode = frame.attach_mode;
280                if parent_id == id {
281                    return true;
282                }
283                // A child where the parent held it before is already attached
284                // to the parent: its node needs no visit.
285                let attached_before = frame.previous.get(frame.new_children.len()) == Some(&id);
286                if matches!(attach_mode, ParentAttachMode::DeferredSync) {
287                    frame.new_children.push(id);
288                }
289                drop(parent_stack);
290
291                if attached_before {
292                    debug_assert!(
293                        self.borrow_applier()
294                            .get_mut(id)
295                            .map_or(true, |node| node.parent().is_none_or(|p| p == parent_id)),
296                        "a parent's previous child must name no other parent"
297                    );
298                } else {
299                    let mut applier = self.borrow_applier();
300                    if let Ok(child_node) = applier.get_mut(id) {
301                        child_node.set_parent_for_bubbling(parent_id);
302                    }
303                }
304                if matches!(attach_mode, ParentAttachMode::ImmediateAppend) {
305                    self.commands_mut().push(Command::AttachChild {
306                        parent_id,
307                        child_id: id,
308                        insert_index: None,
309                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
310                    });
311                }
312                return true;
313            }
314        }
315        false
316    }
317
318    fn attach_without_parent_frame(&self, id: NodeId) {
319        let in_subcompose = !self.subcompose_stack().is_empty();
320        if in_subcompose {
321            let has_parent = {
322                let mut applier = self.borrow_applier();
323                applier
324                    .get_mut(id)
325                    .is_ok_and(|node| node.parent().is_some())
326            };
327
328            if !has_parent {
329                let mut subcompose_stack = self.subcompose_stack();
330                if let Some(frame) = subcompose_stack.last_mut() {
331                    frame.nodes.push(id);
332                }
333            }
334            return;
335        }
336
337        if let Some(parent_hint) = self.core.recompose_parent_hint.get() {
338            if parent_hint == id {
339                debug_assert_ne!(
340                    parent_hint, id,
341                    "a node cannot be attached as its own parent"
342                );
343                return;
344            }
345            let parent_status = {
346                let mut applier = self.borrow_applier();
347                applier.get_mut(id).map_or(None, |node| node.parent())
348            };
349            match parent_status {
350                Some(existing) if existing == parent_hint => {
351                    self.skip_recompose_child();
352                }
353                None => {
354                    let insert_index = self.next_recompose_insert_index();
355                    self.commands_mut().push(Command::AttachChild {
356                        parent_id: parent_hint,
357                        child_id: id,
358                        insert_index,
359                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
360                    });
361                }
362                Some(_) => {}
363            }
364            return;
365        }
366
367        let has_parent = {
368            let mut applier = self.borrow_applier();
369            applier
370                .get_mut(id)
371                .is_ok_and(|node| node.parent().is_some())
372        };
373        if has_parent {
374            return;
375        }
376
377        self.set_root(Some(id));
378    }
379
380    /// Runs `f` on the node `id` as an `N`. Returns an error if the node does not exist or is not
381    /// an `N`.
382    pub fn with_node_mut<N: Node + 'static, R>(
383        &self,
384        id: NodeId,
385        f: impl FnOnce(&mut N) -> R,
386    ) -> Result<R, NodeError> {
387        let mut applier = self.borrow_applier();
388        let node = applier.get_mut(id)?;
389        let typed =
390            node.as_any_mut()
391                .downcast_mut::<N>()
392                .ok_or_else(|| NodeError::TypeMismatch {
393                    id,
394                    expected: std::any::type_name::<N>(),
395                })?;
396        Ok(f(typed))
397    }
398
399    /// Makes the node `id` the parent of the nodes emitted next. Each call needs a matching
400    /// `pop_parent`.
401    pub fn push_parent(&self, id: NodeId) {
402        let in_subcompose = !self.core.subcompose_stack.borrow().is_empty();
403
404        let children = self.get_node_children(id);
405        let previous = if children.is_empty() {
406            ChildList::new()
407        } else {
408            ChildList::from_slice(&children)
409        };
410        let attach_mode = if in_subcompose || !previous.is_empty() {
411            ParentAttachMode::DeferredSync
412        } else {
413            ParentAttachMode::ImmediateAppend
414        };
415
416        self.parent_stack().push(ParentFrame {
417            id,
418            previous,
419            new_children: ChildList::new(),
420            new_children_membership: None,
421            attach_mode,
422            synthetic_root: false,
423        });
424    }
425
426    /// Composes nothing under node `id`, which holds children when
427    /// `holds_children`: the parent frame [`Composer::push_parent`] and
428    /// [`Composer::pop_parent`] would open and close around nothing is left
429    /// out where closing it would sync nothing, which is outside a
430    /// subcomposition for a node that holds no children.
431    pub fn compose_no_children(&self, id: NodeId, holds_children: bool) {
432        if holds_children || !self.core.subcompose_stack.borrow().is_empty() {
433            self.push_parent(id);
434            self.pop_parent();
435        }
436    }
437
438    /// Closes the parent opened by the matching `push_parent` and sets that node's children to the
439    /// nodes emitted since.
440    pub fn pop_parent(&self) {
441        let frame_opt = {
442            let mut stack = self.parent_stack();
443            stack.pop()
444        };
445        if let Some(frame) = frame_opt {
446            let ParentFrame {
447                id,
448                previous,
449                new_children,
450                new_children_membership: _new_children_membership,
451                attach_mode,
452                synthetic_root: _synthetic_root,
453            } = frame;
454
455            log::trace!(target: "cranpose::compose::parent", "pop_parent: node #{id}");
456            log::trace!(
457                target: "cranpose::compose::parent",
458                "previous children: {previous:?}"
459            );
460            log::trace!(
461                target: "cranpose::compose::parent",
462                "new children: {new_children:?}"
463            );
464            if matches!(attach_mode, ParentAttachMode::DeferredSync) {
465                let _ = previous;
466                self.commands_mut().push(Command::SyncChildren {
467                    parent_id: id,
468                    expected_children: new_children,
469                });
470            }
471        }
472    }
473
474    pub(crate) fn take_commands(&self) -> CommandQueue {
475        std::mem::take(&mut *self.commands_mut())
476    }
477
478    /// Applies any pending applier commands and runtime updates.
479    ///
480    /// This is useful during measure-time subcomposition to ensure newly created
481    /// nodes are available for measurement before the full composition is committed.
482    pub fn apply_pending_commands(&self) -> Result<(), NodeError> {
483        self.resolve_recompose_child_cursor();
484        let mut commands = self.take_commands();
485        let runtime_handle = self.runtime_handle();
486        let result = {
487            let mut applier = self.borrow_applier();
488            let mut result = commands.apply(&mut *applier);
489            if result.is_ok() {
490                for update in runtime_handle.take_updates() {
491                    if let Err(err) = update.apply(&mut *applier) {
492                        result = Err(err);
493                        break;
494                    }
495                }
496            }
497            result
498        };
499        // The pass goes on queueing: hand it back the storage just applied.
500        let mut queued = self.commands_mut();
501        if queued.len() == 0 {
502            std::mem::swap(&mut *queued, &mut commands);
503        }
504        drop(queued);
505        if result.is_err() {
506            let host = self.active_slots_host();
507            if !host.has_active_pass() {
508                host.abandon_after_apply_failure();
509            }
510        }
511        result
512    }
513
514    /// Queues `effect` to run after this pass has applied its node changes.
515    pub fn register_side_effect(&self, effect: impl FnOnce() + 'static) {
516        self.side_effects_mut().push(Box::new(effect));
517    }
518
519    /// Removes and returns the side effects queued during this pass.
520    pub fn take_side_effects(&self) -> Vec<Box<dyn FnOnce()>> {
521        std::mem::take(&mut *self.side_effects_mut())
522    }
523
524    pub(crate) fn root(&self) -> Option<NodeId> {
525        self.core.root.get()
526    }
527
528    pub(crate) fn set_root(&self, node: Option<NodeId>) {
529        self.core.root.set(node);
530    }
531}