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