Skip to main content

cranpose_core/
emit.rs

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