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