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