Skip to main content

cranpose_core/
emit.rs

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