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    slot::NodeSlotUpdate,
7};
8
9impl Composer {
10    fn recorded_node_parent(&self, id: NodeId) -> Option<NodeId> {
11        let mut applier = self.borrow_applier();
12        applier.get_mut(id).ok().and_then(|node| node.parent())
13    }
14
15    fn queue_replaced_slot_node_removal(&self, old_id: NodeId, old_generation: u32) {
16        let current_generation = self.borrow_applier().node_generation(old_id);
17        if current_generation != old_generation {
18            log::trace!(
19                target: "cranpose::compose::emit",
20                "skipping stale replacement cleanup for node #{old_id} (slot_generation={old_generation} current_generation={current_generation})",
21            );
22            return;
23        }
24
25        log::trace!(
26            target: "cranpose::compose::emit",
27            "removing replaced node #{old_id} (generation={old_generation})",
28        );
29        self.commands_mut().push(Command::RemoveNode { id: old_id });
30    }
31
32    #[track_caller]
33    pub fn use_state<T: Clone + 'static>(&self, init: impl FnOnce() -> T) -> MutableState<T> {
34        let source = crate::caller_location_key();
35        let runtime = self.runtime_handle();
36        let state = self.with_slot_session_mut(|slots| {
37            slots.remember(source, || {
38                OwnedMutableState::with_runtime(init(), runtime.clone())
39            })
40        });
41        state.with(|state| state.handle())
42    }
43
44    fn emit_node_box<N: Node + 'static>(
45        &self,
46        source: crate::Key,
47        make_node: impl FnOnce(&mut dyn Applier) -> EmittedNode,
48    ) -> NodeId {
49        let adopted = {
50            let mut skip = 0;
51            loop {
52                let Some((id, slot_gen)) = self
53                    .with_slot_session_mut(|slots| slots.peek_node_record_by_source(source, skip))
54                else {
55                    break None;
56                };
57                let (type_ok, gen_ok) = {
58                    let mut applier = self.borrow_applier();
59                    let gen_ok = applier.node_generation(id) == slot_gen;
60                    let type_ok = match applier.get_mut(id) {
61                        Ok(node) => node.as_any_mut().downcast_ref::<N>().is_some(),
62                        Err(_) => false,
63                    };
64                    (type_ok, gen_ok)
65                };
66                if type_ok && gen_ok {
67                    let committed = self.with_slot_session_mut(|slots| {
68                        slots.adopt_node_record_by_source(source, skip)
69                    });
70                    debug_assert_eq!(committed, Some((id, slot_gen)));
71                    break Some((id, slot_gen));
72                }
73                skip += 1;
74            }
75        };
76
77        if let Some((id, slot_gen)) = adopted {
78            let scope_debug = self
79                .current_recompose_scope()
80                .map(|scope| (scope.id(), debug_scope_label(scope.id())))
81                .unwrap_or((0, None));
82            log::trace!(
83                target: "cranpose::compose::emit",
84                "reusing node #{id} as {} [scope_id={} scope_label={:?}]",
85                std::any::type_name::<N>(),
86                scope_debug.0,
87                scope_debug.1,
88            );
89            self.commands_mut().push(Command::update_node::<N>(id));
90            self.attach_to_parent(id);
91            let parent_id = self.recorded_node_parent(id);
92            let recorded = self.with_slot_session_mut(|slots| {
93                slots.record_node_with_parent(id, slot_gen, parent_id, source)
94            });
95            match recorded {
96                NodeSlotUpdate::Reused {
97                    id: recorded_id,
98                    generation,
99                } => {
100                    debug_assert_eq!(recorded_id, id);
101                    debug_assert_eq!(generation, slot_gen);
102                }
103                NodeSlotUpdate::Inserted { .. } => {
104                    log::warn!(
105                        target: "cranpose::compose::emit",
106                        "slot writer inserted node #{id} while reusing the same node identity",
107                    );
108                }
109                NodeSlotUpdate::Replaced {
110                    old_id,
111                    old_generation,
112                    ..
113                } => {
114                    log::warn!(
115                        target: "cranpose::compose::emit",
116                        "slot writer replaced node #{old_id} while reusing node #{id}",
117                    );
118                    self.queue_replaced_slot_node_removal(old_id, old_generation);
119                }
120            }
121            self.core.last_node_reused.set(Some(true));
122            return id;
123        }
124
125        // Type mismatch, stale generation, or no node: create new node
126        let (id, generation) = {
127            let mut applier = self.borrow_applier();
128            let emitted = make_node(&mut *applier);
129            let id = match emitted {
130                EmittedNode::Fresh(node) => applier.create(node),
131                EmittedNode::Recycled(recycled) => {
132                    let (stable_id, node, warm_origin) = recycled.into_parts();
133                    let insertion = applier.insert_recycled_node_or_create(stable_id, node);
134                    if let Some(error) = insertion.fallback_error.as_ref() {
135                        log::warn!(
136                            target: "cranpose::compose::emit",
137                            "discarding stale recycled stable id #{stable_id}: {error}",
138                        );
139                    }
140                    applier.set_recycled_node_origin(insertion.id, warm_origin);
141                    insertion.id
142                }
143            };
144            let generation = applier.node_generation(id);
145            (id, generation)
146        };
147        let scope_debug = self
148            .current_recompose_scope()
149            .map(|scope| (scope.id(), debug_scope_label(scope.id())))
150            .unwrap_or((0, None));
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_debug.0,
158            scope_debug.1,
159        );
160        self.commands_mut().push(Command::MountNode { id });
161        self.attach_to_parent(id);
162        let parent_id = self.recorded_node_parent(id);
163        let recorded = self.with_slot_session_mut(|slots| {
164            slots.record_node_with_parent(id, generation, parent_id, source)
165        });
166        match recorded {
167            NodeSlotUpdate::Inserted {
168                id: recorded_id,
169                generation: recorded_generation,
170            } => {
171                debug_assert_eq!(recorded_id, id);
172                debug_assert_eq!(recorded_generation, generation);
173            }
174            NodeSlotUpdate::Replaced {
175                old_id,
176                old_generation,
177                new_id,
178                new_generation,
179            } => {
180                debug_assert_eq!(new_id, id);
181                debug_assert_eq!(new_generation, generation);
182                self.queue_replaced_slot_node_removal(old_id, old_generation);
183            }
184            NodeSlotUpdate::Reused { .. } => {
185                log::warn!(
186                    target: "cranpose::compose::emit",
187                    "slot writer reported reuse for newly emitted node #{id}",
188                );
189            }
190        }
191        self.core.last_node_reused.set(Some(false));
192        id
193    }
194
195    #[track_caller]
196    pub fn emit_node<N: Node + 'static>(&self, init: impl FnOnce() -> N) -> NodeId {
197        let source = crate::caller_location_key();
198        self.emit_node_box::<N>(source, |_| EmittedNode::Fresh(Box::new(init())))
199    }
200
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 attach_to_parent(&self, id: NodeId) {
232        self.attach_to_parent_with_mode(id, false);
233    }
234
235    pub(crate) fn attach_to_parent_with_mode(
236        &self,
237        id: NodeId,
238        force_reparent_current_parent: bool,
239    ) {
240        // IMPORTANT: Check parent_stack FIRST.
241        // During subcomposition, if there's an active parent (e.g., Row),
242        // child nodes (e.g., Text) should attach to that parent, NOT to the
243        // subcompose frame. Only ROOT nodes (nodes with no active parent)
244        // should be added to the subcompose frame.
245        let mut parent_stack = self.parent_stack();
246        if let Some(parent_id) = parent_stack.last().map(|frame| frame.id) {
247            let stale_root_parent = self.core.root.get() == Some(parent_id) && {
248                let mut applier = self.borrow_applier();
249                applier.get_mut(parent_id).is_err()
250            };
251            if stale_root_parent {
252                parent_stack.pop();
253                self.set_root(None);
254            } else {
255                let Some(frame) = parent_stack.last_mut() else {
256                    return;
257                };
258                let attach_mode = frame.attach_mode;
259                if parent_id == id {
260                    return;
261                }
262                if matches!(attach_mode, ParentAttachMode::DeferredSync) {
263                    frame.new_children.push(id);
264                }
265                drop(parent_stack);
266
267                // KEY FIX: Set parent link IMMEDIATELY, matching Jetpack Compose's
268                // LayoutNode.insertAt pattern where _foldedParent is set synchronously.
269                // This ensures that when bubble_measure_dirty runs (in commands),
270                // the parent chain is already established.
271                //
272                // IMPORTANT: Only set parent if node doesn't have one or if the new parent
273                // is not the root. This prevents double-recomposition scenarios where a
274                // child scope (invalidated by CompositionLocalProvider during parent's
275                // recomposition) gets processed again with parent_stack=[root], which would
276                // incorrectly reparent nodes to root.
277                {
278                    let mut applier = self.borrow_applier();
279                    if let Ok(child_node) = applier.get_mut(id) {
280                        let existing_parent = child_node.parent();
281                        // Only set parent if:
282                        // 1. Node has no parent, OR
283                        // 2. New parent is NOT the root (parent_id != 0 or != self.root)
284                        // This prevents root from stealing children that belong to intermediate nodes.
285                        let should_set = if force_reparent_current_parent {
286                            existing_parent != Some(parent_id)
287                        } else {
288                            match existing_parent {
289                                None => true,
290                                Some(existing) => {
291                                    // Don't let root steal children from proper parents
292                                    let root_id = self.core.root.get();
293                                    parent_id != root_id.unwrap_or(0)
294                                        || existing == root_id.unwrap_or(0)
295                                }
296                            }
297                        };
298                        if should_set {
299                            child_node.set_parent_for_bubbling(parent_id);
300                        }
301                    }
302                }
303                if matches!(attach_mode, ParentAttachMode::ImmediateAppend) {
304                    self.commands_mut().push(Command::AttachChild {
305                        parent_id,
306                        child_id: id,
307                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
308                    });
309                }
310                return;
311            }
312        }
313        drop(parent_stack);
314
315        // No active parent - check if we're in subcompose
316        let in_subcompose = !self.subcompose_stack().is_empty();
317        if in_subcompose {
318            // During subcompose, only add ROOT nodes (nodes without a parent).
319            // Child nodes already have their parent-child relationship from composition;
320            // re-adding them to the subcompose frame would cause duplication.
321            let has_parent = {
322                let mut applier = self.borrow_applier();
323                applier
324                    .get_mut(id)
325                    .map(|node| node.parent().is_some())
326                    .unwrap_or(false)
327            };
328
329            if !has_parent {
330                let mut subcompose_stack = self.subcompose_stack();
331                if let Some(frame) = subcompose_stack.last_mut() {
332                    frame.nodes.push(id);
333                }
334            }
335            return;
336        }
337
338        // During recomposition, preserve the original parent when possible.
339        if let Some(parent_hint) = self.core.recompose_parent_hint.get() {
340            if parent_hint == id {
341                debug_assert_ne!(
342                    parent_hint, id,
343                    "a node cannot be attached as its own parent"
344                );
345                return;
346            }
347            let parent_status = {
348                let mut applier = self.borrow_applier();
349                applier
350                    .get_mut(id)
351                    .map(|node| node.parent())
352                    .unwrap_or(None)
353            };
354            match parent_status {
355                Some(existing) if existing == parent_hint => {}
356                None => {
357                    self.commands_mut().push(Command::AttachChild {
358                        parent_id: parent_hint,
359                        child_id: id,
360                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
361                    });
362                }
363                Some(_) => {}
364            }
365            return;
366        }
367
368        // Neither parent nor subcompose - check if this node already has a parent.
369        // During recomposition, reused nodes already have their correct parent from
370        // initial composition. We should NOT set them as root, as that would corrupt
371        // the tree structure and cause duplication.
372        let has_parent = {
373            let mut applier = self.borrow_applier();
374            applier
375                .get_mut(id)
376                .map(|node| node.parent().is_some())
377                .unwrap_or(false)
378        };
379        if has_parent {
380            // Node already has a parent, nothing to do
381            return;
382        }
383
384        // Node has no parent and is not in subcompose - must be root
385        self.set_root(Some(id));
386    }
387
388    pub fn with_node_mut<N: Node + 'static, R>(
389        &self,
390        id: NodeId,
391        f: impl FnOnce(&mut N) -> R,
392    ) -> Result<R, NodeError> {
393        let mut applier = self.borrow_applier();
394        let node = applier.get_mut(id)?;
395        let typed = node
396            .as_any_mut()
397            .downcast_mut::<N>()
398            .ok_or(NodeError::TypeMismatch {
399                id,
400                expected: std::any::type_name::<N>(),
401            })?;
402        Ok(f(typed))
403    }
404
405    pub fn push_parent(&self, id: NodeId) {
406        let reused = self.core.last_node_reused.take().unwrap_or(true);
407        let in_subcompose = !self.core.subcompose_stack.borrow().is_empty();
408
409        // Fresh parents usually append children directly, but a restored or otherwise
410        // non-reused node can still carry attached children in the applier. In that case
411        // we must diff against the live child list or stale descendants remain mounted.
412        let mut previous = ChildList::new();
413        if reused || in_subcompose {
414            previous.extend(self.get_node_children(id));
415        } else {
416            let existing_children = self.get_node_children(id);
417            if !existing_children.is_empty() {
418                previous.extend(existing_children);
419            }
420        }
421        let attach_mode = if in_subcompose || !previous.is_empty() {
422            ParentAttachMode::DeferredSync
423        } else {
424            ParentAttachMode::ImmediateAppend
425        };
426
427        self.parent_stack().push(ParentFrame {
428            id,
429            previous,
430            new_children: ChildList::new(),
431            new_children_membership: None,
432            attach_mode,
433            synthetic_root: false,
434        });
435    }
436
437    pub fn pop_parent(&self) {
438        let frame_opt = {
439            let mut stack = self.parent_stack();
440            stack.pop()
441        };
442        if let Some(frame) = frame_opt {
443            let ParentFrame {
444                id,
445                previous,
446                new_children,
447                new_children_membership: _new_children_membership,
448                attach_mode,
449                synthetic_root: _synthetic_root,
450            } = frame;
451
452            log::trace!(target: "cranpose::compose::parent", "pop_parent: node #{}", id);
453            log::trace!(
454                target: "cranpose::compose::parent",
455                "previous children: {:?}",
456                previous
457            );
458            log::trace!(
459                target: "cranpose::compose::parent",
460                "new children: {:?}",
461                new_children
462            );
463            if matches!(attach_mode, ParentAttachMode::DeferredSync) {
464                let _ = previous;
465                self.commands_mut().push(Command::SyncChildren {
466                    parent_id: id,
467                    expected_children: new_children,
468                });
469            }
470        }
471    }
472
473    pub(crate) fn take_commands(&self) -> CommandQueue {
474        std::mem::take(&mut *self.commands_mut())
475    }
476
477    /// Applies any pending applier commands and runtime updates.
478    ///
479    /// This is useful during measure-time subcomposition to ensure newly created
480    /// nodes are available for measurement before the full composition is committed.
481    pub fn apply_pending_commands(&self) -> Result<(), NodeError> {
482        let commands = self.take_commands();
483        let runtime_handle = self.runtime_handle();
484        let result = {
485            let mut applier = self.borrow_applier();
486            let mut result = commands.apply(&mut *applier);
487            if result.is_ok() {
488                for update in runtime_handle.take_updates() {
489                    if let Err(err) = update.apply(&mut *applier) {
490                        result = Err(err);
491                        break;
492                    }
493                }
494            }
495            result
496        };
497        if result.is_err() {
498            let host = self.active_slots_host();
499            if !host.has_active_pass() {
500                host.abandon_after_apply_failure();
501            }
502        }
503        result?;
504        runtime_handle.drain_ui();
505        Ok(())
506    }
507
508    pub fn register_side_effect(&self, effect: impl FnOnce() + 'static) {
509        self.side_effects_mut().push(Box::new(effect));
510    }
511
512    pub fn take_side_effects(&self) -> Vec<Box<dyn FnOnce()>> {
513        std::mem::take(&mut *self.side_effects_mut())
514    }
515
516    pub(crate) fn root(&self) -> Option<NodeId> {
517        self.core.root.get()
518    }
519
520    pub(crate) fn set_root(&self, node: Option<NodeId>) {
521        self.core.root.set(node);
522    }
523}