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        let (id, generation) = {
126            let mut applier = self.borrow_applier();
127            let emitted = make_node(&mut *applier);
128            let id = match emitted {
129                EmittedNode::Fresh(node) => applier.create(node),
130                EmittedNode::Recycled(recycled) => {
131                    let (stable_id, node, warm_origin) = recycled.into_parts();
132                    let insertion = applier.insert_recycled_node_or_create(stable_id, node);
133                    if let Some(error) = insertion.fallback_error.as_ref() {
134                        log::warn!(
135                            target: "cranpose::compose::emit",
136                            "discarding stale recycled stable id #{stable_id}: {error}",
137                        );
138                    }
139                    applier.set_recycled_node_origin(insertion.id, warm_origin);
140                    insertion.id
141                }
142            };
143            let generation = applier.node_generation(id);
144            (id, generation)
145        };
146        let scope_debug = self
147            .current_recompose_scope()
148            .map(|scope| (scope.id(), debug_scope_label(scope.id())))
149            .unwrap_or((0, None));
150        log::trace!(
151            target: "cranpose::compose::emit",
152            "creating node #{} (gen={}) as {} [scope_id={} scope_label={:?}]",
153            id,
154            generation,
155            std::any::type_name::<N>(),
156            scope_debug.0,
157            scope_debug.1,
158        );
159        self.commands_mut().push(Command::MountNode { id });
160        self.attach_to_parent(id);
161        let parent_id = self.recorded_node_parent(id);
162        let recorded = self.with_slot_session_mut(|slots| {
163            slots.record_node_with_parent(id, generation, parent_id, source)
164        });
165        match recorded {
166            NodeSlotUpdate::Inserted {
167                id: recorded_id,
168                generation: recorded_generation,
169            } => {
170                debug_assert_eq!(recorded_id, id);
171                debug_assert_eq!(recorded_generation, generation);
172            }
173            NodeSlotUpdate::Replaced {
174                old_id,
175                old_generation,
176                new_id,
177                new_generation,
178            } => {
179                debug_assert_eq!(new_id, id);
180                debug_assert_eq!(new_generation, generation);
181                self.queue_replaced_slot_node_removal(old_id, old_generation);
182            }
183            NodeSlotUpdate::Reused { .. } => {
184                log::warn!(
185                    target: "cranpose::compose::emit",
186                    "slot writer reported reuse for newly emitted node #{id}",
187                );
188            }
189        }
190        self.core.last_node_reused.set(Some(false));
191        id
192    }
193
194    #[track_caller]
195    pub fn emit_node<N: Node + 'static>(&self, init: impl FnOnce() -> N) -> NodeId {
196        let source = crate::caller_location_key();
197        self.emit_node_box::<N>(source, |_| EmittedNode::Fresh(Box::new(init())))
198    }
199
200    #[track_caller]
201    pub fn emit_recyclable_node<N: Node + 'static>(
202        &self,
203        init: impl FnOnce() -> N,
204        reset: impl FnOnce(&mut N),
205    ) -> NodeId {
206        let source = crate::caller_location_key();
207        self.emit_node_box::<N>(source, |applier| {
208            let key = TypeId::of::<N>();
209            if let Some(mut recycled) = applier.take_recycled_node(key) {
210                if let Some(typed) = recycled.node_mut().as_any_mut().downcast_mut::<N>() {
211                    reset(typed);
212                    return EmittedNode::Recycled(recycled);
213                }
214                log::warn!(
215                    target: "cranpose::compose::emit",
216                    "discarding recycled node shell with mismatched type for {}",
217                    std::any::type_name::<N>(),
218                );
219            }
220
221            let node = Box::new(init());
222            applier.record_fresh_recyclable_creation(key);
223            if let Some(shell) = node.rehouse_for_recycle() {
224                applier.seed_recycled_node_shell(key, node.recycle_pool_limit(), shell);
225            }
226            EmittedNode::Fresh(node)
227        })
228    }
229
230    fn attach_to_parent(&self, id: NodeId) {
231        self.attach_to_parent_with_mode(id, false);
232    }
233
234    fn advance_recompose_child_cursor(&self) -> Option<usize> {
235        let cursor = self.core.recompose_child_cursor.get()?;
236        self.core.recompose_child_cursor.set(Some(cursor + 1));
237        Some(cursor)
238    }
239
240    pub(crate) fn attach_to_parent_with_mode(
241        &self,
242        id: NodeId,
243        force_reparent_current_parent: bool,
244    ) {
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                {
268                    let mut applier = self.borrow_applier();
269                    if let Ok(child_node) = applier.get_mut(id) {
270                        let existing_parent = child_node.parent();
271                        let should_set = if force_reparent_current_parent {
272                            existing_parent != Some(parent_id)
273                        } else {
274                            match existing_parent {
275                                None => true,
276                                Some(existing) => {
277                                    let root_id = self.core.root.get();
278                                    parent_id != root_id.unwrap_or(0)
279                                        || existing == root_id.unwrap_or(0)
280                                }
281                            }
282                        };
283                        if should_set {
284                            child_node.set_parent_for_bubbling(parent_id);
285                        }
286                    }
287                }
288                if matches!(attach_mode, ParentAttachMode::ImmediateAppend) {
289                    self.commands_mut().push(Command::AttachChild {
290                        parent_id,
291                        child_id: id,
292                        insert_index: None,
293                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
294                    });
295                }
296                return;
297            }
298        }
299        drop(parent_stack);
300
301        let in_subcompose = !self.subcompose_stack().is_empty();
302        if in_subcompose {
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        if let Some(parent_hint) = self.core.recompose_parent_hint.get() {
321            if parent_hint == id {
322                debug_assert_ne!(
323                    parent_hint, id,
324                    "a node cannot be attached as its own parent"
325                );
326                return;
327            }
328            let parent_status = {
329                let mut applier = self.borrow_applier();
330                applier
331                    .get_mut(id)
332                    .map(|node| node.parent())
333                    .unwrap_or(None)
334            };
335            match parent_status {
336                Some(existing) if existing == parent_hint => {
337                    self.advance_recompose_child_cursor();
338                }
339                None => {
340                    let insert_index = self.advance_recompose_child_cursor();
341                    self.commands_mut().push(Command::AttachChild {
342                        parent_id: parent_hint,
343                        child_id: id,
344                        insert_index,
345                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
346                    });
347                }
348                Some(_) => {}
349            }
350            return;
351        }
352
353        let has_parent = {
354            let mut applier = self.borrow_applier();
355            applier
356                .get_mut(id)
357                .map(|node| node.parent().is_some())
358                .unwrap_or(false)
359        };
360        if has_parent {
361            return;
362        }
363
364        self.set_root(Some(id));
365    }
366
367    pub fn with_node_mut<N: Node + 'static, R>(
368        &self,
369        id: NodeId,
370        f: impl FnOnce(&mut N) -> R,
371    ) -> Result<R, NodeError> {
372        let mut applier = self.borrow_applier();
373        let node = applier.get_mut(id)?;
374        let typed = node
375            .as_any_mut()
376            .downcast_mut::<N>()
377            .ok_or(NodeError::TypeMismatch {
378                id,
379                expected: std::any::type_name::<N>(),
380            })?;
381        Ok(f(typed))
382    }
383
384    pub fn push_parent(&self, id: NodeId) {
385        let reused = self.core.last_node_reused.take().unwrap_or(true);
386        let in_subcompose = !self.core.subcompose_stack.borrow().is_empty();
387
388        let mut previous = ChildList::new();
389        if reused || in_subcompose {
390            previous.extend(self.get_node_children(id));
391        } else {
392            let existing_children = self.get_node_children(id);
393            if !existing_children.is_empty() {
394                previous.extend(existing_children);
395            }
396        }
397        let attach_mode = if in_subcompose || !previous.is_empty() {
398            ParentAttachMode::DeferredSync
399        } else {
400            ParentAttachMode::ImmediateAppend
401        };
402
403        self.parent_stack().push(ParentFrame {
404            id,
405            previous,
406            new_children: ChildList::new(),
407            new_children_membership: None,
408            attach_mode,
409            synthetic_root: false,
410        });
411    }
412
413    pub fn pop_parent(&self) {
414        let frame_opt = {
415            let mut stack = self.parent_stack();
416            stack.pop()
417        };
418        if let Some(frame) = frame_opt {
419            let ParentFrame {
420                id,
421                previous,
422                new_children,
423                new_children_membership: _new_children_membership,
424                attach_mode,
425                synthetic_root: _synthetic_root,
426            } = frame;
427
428            log::trace!(target: "cranpose::compose::parent", "pop_parent: node #{}", id);
429            log::trace!(
430                target: "cranpose::compose::parent",
431                "previous children: {:?}",
432                previous
433            );
434            log::trace!(
435                target: "cranpose::compose::parent",
436                "new children: {:?}",
437                new_children
438            );
439            if matches!(attach_mode, ParentAttachMode::DeferredSync) {
440                let _ = previous;
441                self.commands_mut().push(Command::SyncChildren {
442                    parent_id: id,
443                    expected_children: new_children,
444                });
445            }
446        }
447    }
448
449    pub(crate) fn take_commands(&self) -> CommandQueue {
450        std::mem::take(&mut *self.commands_mut())
451    }
452
453    /// Applies any pending applier commands and runtime updates.
454    ///
455    /// This is useful during measure-time subcomposition to ensure newly created
456    /// nodes are available for measurement before the full composition is committed.
457    pub fn apply_pending_commands(&self) -> Result<(), NodeError> {
458        let commands = self.take_commands();
459        let runtime_handle = self.runtime_handle();
460        let result = {
461            let mut applier = self.borrow_applier();
462            let mut result = commands.apply(&mut *applier);
463            if result.is_ok() {
464                for update in runtime_handle.take_updates() {
465                    if let Err(err) = update.apply(&mut *applier) {
466                        result = Err(err);
467                        break;
468                    }
469                }
470            }
471            result
472        };
473        if result.is_err() {
474            let host = self.active_slots_host();
475            if !host.has_active_pass() {
476                host.abandon_after_apply_failure();
477            }
478        }
479        result?;
480        runtime_handle.drain_ui();
481        Ok(())
482    }
483
484    pub fn register_side_effect(&self, effect: impl FnOnce() + 'static) {
485        self.side_effects_mut().push(Box::new(effect));
486    }
487
488    pub fn take_side_effects(&self) -> Vec<Box<dyn FnOnce()>> {
489        std::mem::take(&mut *self.side_effects_mut())
490    }
491
492    pub(crate) fn root(&self) -> Option<NodeId> {
493        self.core.root.get()
494    }
495
496    pub(crate) fn set_root(&self, node: Option<NodeId>) {
497        self.core.root.set(node);
498    }
499}