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