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            let parent_status = {
302                let mut applier = self.borrow_applier();
303                applier
304                    .get_mut(id)
305                    .map(|node| node.parent())
306                    .unwrap_or(None)
307            };
308            match parent_status {
309                Some(existing) if existing == parent_hint => {}
310                None => {
311                    self.commands_mut().push(Command::AttachChild {
312                        parent_id: parent_hint,
313                        child_id: id,
314                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
315                    });
316                }
317                Some(_) => {}
318            }
319            return;
320        }
321
322        // Neither parent nor subcompose - check if this node already has a parent.
323        // During recomposition, reused nodes already have their correct parent from
324        // initial composition. We should NOT set them as root, as that would corrupt
325        // the tree structure and cause duplication.
326        let has_parent = {
327            let mut applier = self.borrow_applier();
328            applier
329                .get_mut(id)
330                .map(|node| node.parent().is_some())
331                .unwrap_or(false)
332        };
333        if has_parent {
334            // Node already has a parent, nothing to do
335            return;
336        }
337
338        // Node has no parent and is not in subcompose - must be root
339        self.set_root(Some(id));
340    }
341
342    pub fn with_node_mut<N: Node + 'static, R>(
343        &self,
344        id: NodeId,
345        f: impl FnOnce(&mut N) -> R,
346    ) -> Result<R, NodeError> {
347        let mut applier = self.borrow_applier();
348        let node = applier.get_mut(id)?;
349        let typed = node
350            .as_any_mut()
351            .downcast_mut::<N>()
352            .ok_or(NodeError::TypeMismatch {
353                id,
354                expected: std::any::type_name::<N>(),
355            })?;
356        Ok(f(typed))
357    }
358
359    pub fn push_parent(&self, id: NodeId) {
360        let reused = self.core.last_node_reused.take().unwrap_or(true);
361        let in_subcompose = !self.core.subcompose_stack.borrow().is_empty();
362
363        // Fresh parents usually append children directly, but a restored or otherwise
364        // non-reused node can still carry attached children in the applier. In that case
365        // we must diff against the live child list or stale descendants remain mounted.
366        let mut previous = ChildList::new();
367        if reused || in_subcompose {
368            previous.extend(self.get_node_children(id));
369        } else {
370            let existing_children = self.get_node_children(id);
371            if !existing_children.is_empty() {
372                previous.extend(existing_children);
373            }
374        }
375        let attach_mode = if in_subcompose || !previous.is_empty() {
376            ParentAttachMode::DeferredSync
377        } else {
378            ParentAttachMode::ImmediateAppend
379        };
380
381        self.parent_stack().push(ParentFrame {
382            id,
383            previous,
384            new_children: ChildList::new(),
385            new_children_membership: None,
386            attach_mode,
387        });
388    }
389
390    pub fn pop_parent(&self) {
391        let frame_opt = {
392            let mut stack = self.parent_stack();
393            stack.pop()
394        };
395        if let Some(frame) = frame_opt {
396            let ParentFrame {
397                id,
398                previous,
399                new_children,
400                new_children_membership: _new_children_membership,
401                attach_mode,
402            } = frame;
403
404            log::trace!(target: "cranpose::compose::parent", "pop_parent: node #{}", id);
405            log::trace!(
406                target: "cranpose::compose::parent",
407                "previous children: {:?}",
408                previous
409            );
410            log::trace!(
411                target: "cranpose::compose::parent",
412                "new children: {:?}",
413                new_children
414            );
415            if matches!(attach_mode, ParentAttachMode::DeferredSync) {
416                let _ = previous;
417                self.commands_mut().push(Command::SyncChildren {
418                    parent_id: id,
419                    expected_children: new_children,
420                });
421            }
422        }
423    }
424
425    pub(crate) fn take_commands(&self) -> CommandQueue {
426        std::mem::take(&mut *self.commands_mut())
427    }
428
429    /// Applies any pending applier commands and runtime updates.
430    ///
431    /// This is useful during measure-time subcomposition to ensure newly created
432    /// nodes are available for measurement before the full composition is committed.
433    pub fn apply_pending_commands(&self) -> Result<(), NodeError> {
434        let commands = self.take_commands();
435        let runtime_handle = self.runtime_handle();
436        let result = {
437            let mut applier = self.borrow_applier();
438            let mut result = commands.apply(&mut *applier);
439            if result.is_ok() {
440                for update in runtime_handle.take_updates() {
441                    if let Err(err) = update.apply(&mut *applier) {
442                        result = Err(err);
443                        break;
444                    }
445                }
446            }
447            result
448        };
449        if result.is_err() {
450            let host = self.active_slots_host();
451            if !host.has_active_pass() {
452                host.abandon_after_apply_failure();
453            }
454        }
455        result?;
456        runtime_handle.drain_ui();
457        Ok(())
458    }
459
460    pub fn register_side_effect(&self, effect: impl FnOnce() + 'static) {
461        self.side_effects_mut().push(Box::new(effect));
462    }
463
464    pub fn take_side_effects(&self) -> Vec<Box<dyn FnOnce()>> {
465        std::mem::take(&mut *self.side_effects_mut())
466    }
467
468    pub(crate) fn root(&self) -> Option<NodeId> {
469        self.core.root.get()
470    }
471
472    pub(crate) fn set_root(&self, node: Option<NodeId>) {
473        self.core.root.set(node);
474    }
475}