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 planned_node_parent(&self, id: NodeId) -> Option<NodeId> {
11        if let Some(parent) = self.current_parent_hint().filter(|parent| *parent != id) {
12            return Some(parent);
13        }
14        let mut applier = self.borrow_applier();
15        applier.get_mut(id).ok().and_then(|node| node.parent())
16    }
17
18    fn queue_replaced_slot_node_removal(&self, old_id: NodeId, old_generation: u32) {
19        let current_generation = self.borrow_applier().node_generation(old_id);
20        if current_generation != old_generation {
21            log::trace!(
22                target: "cranpose::compose::emit",
23                "skipping stale replacement cleanup for node #{old_id} (slot_generation={old_generation} current_generation={current_generation})",
24            );
25            return;
26        }
27
28        log::trace!(
29            target: "cranpose::compose::emit",
30            "removing replaced node #{old_id} (generation={old_generation})",
31        );
32        self.commands_mut().push(Command::RemoveNode { id: old_id });
33    }
34
35    #[track_caller]
36    pub fn use_state<T: Clone + 'static>(&self, init: impl FnOnce() -> T) -> MutableState<T> {
37        let source = crate::caller_location_key();
38        let runtime = self.runtime_handle();
39        let state = self.with_slot_session_mut(|slots| {
40            slots.remember(source, || {
41                OwnedMutableState::with_runtime(init(), runtime.clone())
42            })
43        });
44        state.with(super::state::OwnedMutableState::handle)
45    }
46
47    fn emit_node_box<N: Node + 'static>(
48        &self,
49        source: crate::Key,
50        make_node: impl FnOnce(&mut dyn Applier) -> EmittedNode,
51    ) -> NodeId {
52        let adopted = {
53            let mut skip = 0;
54            loop {
55                let Some((id, slot_gen)) = self
56                    .with_slot_session_mut(|slots| slots.peek_node_record_by_source(source, skip))
57                else {
58                    break None;
59                };
60                let (type_ok, gen_ok) = {
61                    let mut applier = self.borrow_applier();
62                    let gen_ok = applier.node_generation(id) == slot_gen;
63                    let type_ok = match applier.get_mut(id) {
64                        Ok(node) => node.as_any_mut().downcast_ref::<N>().is_some(),
65                        Err(_) => false,
66                    };
67                    (type_ok, gen_ok)
68                };
69                if type_ok && gen_ok {
70                    let committed = self.with_slot_session_mut(|slots| {
71                        slots.adopt_node_record_by_source(source, skip)
72                    });
73                    debug_assert_eq!(committed, Some((id, slot_gen)));
74                    break Some((id, slot_gen));
75                }
76                skip += 1;
77            }
78        };
79
80        if let Some((id, slot_gen)) = adopted {
81            let scope_debug = self.current_recompose_scope().map_or((0, None), |scope| {
82                (scope.id(), debug_scope_label(scope.id()))
83            });
84            log::trace!(
85                target: "cranpose::compose::emit",
86                "reusing node #{id} as {} [scope_id={} scope_label={:?}]",
87                std::any::type_name::<N>(),
88                scope_debug.0,
89                scope_debug.1,
90            );
91            self.commands_mut().push(Command::update_node::<N>(id));
92            self.attach_to_parent(id);
93            let parent_id = self.planned_node_parent(id);
94            let recorded = self.with_slot_session_mut(|slots| {
95                slots.record_node_with_parent(id, slot_gen, parent_id, source)
96            });
97            match recorded {
98                NodeSlotUpdate::Reused {
99                    id: recorded_id,
100                    generation,
101                } => {
102                    debug_assert_eq!(recorded_id, id);
103                    debug_assert_eq!(generation, slot_gen);
104                }
105                NodeSlotUpdate::Inserted { .. } => {
106                    log::warn!(
107                        target: "cranpose::compose::emit",
108                        "slot writer inserted node #{id} while reusing the same node identity",
109                    );
110                }
111                NodeSlotUpdate::Replaced {
112                    old_id,
113                    old_generation,
114                    ..
115                } => {
116                    log::warn!(
117                        target: "cranpose::compose::emit",
118                        "slot writer replaced node #{old_id} while reusing node #{id}",
119                    );
120                    self.queue_replaced_slot_node_removal(old_id, old_generation);
121                }
122            }
123            self.core.last_node_reused.set(Some(true));
124            return id;
125        }
126
127        let (id, generation) = {
128            let mut applier = self.borrow_applier();
129            let emitted = make_node(&mut *applier);
130            let id = match emitted {
131                EmittedNode::Fresh(node) => applier.create(node),
132                EmittedNode::Recycled(recycled) => {
133                    let (stable_id, node, warm_origin) = recycled.into_parts();
134                    let insertion = applier.insert_recycled_node_or_create(stable_id, node);
135                    if let Some(error) = insertion.fallback_error.as_ref() {
136                        log::warn!(
137                            target: "cranpose::compose::emit",
138                            "discarding stale recycled stable id #{stable_id}: {error}",
139                        );
140                    }
141                    applier.set_recycled_node_origin(insertion.id, warm_origin);
142                    insertion.id
143                }
144            };
145            let generation = applier.node_generation(id);
146            (id, generation)
147        };
148        let scope_debug = self.current_recompose_scope().map_or((0, None), |scope| {
149            (scope.id(), debug_scope_label(scope.id()))
150        });
151        log::trace!(
152            target: "cranpose::compose::emit",
153            "creating node #{} (gen={}) as {} [scope_id={} scope_label={:?}]",
154            id,
155            generation,
156            std::any::type_name::<N>(),
157            scope_debug.0,
158            scope_debug.1,
159        );
160        self.commands_mut().push(Command::MountNode { id });
161        self.attach_to_parent(id);
162        let parent_id = self.planned_node_parent(id);
163        let recorded = self.with_slot_session_mut(|slots| {
164            slots.record_node_with_parent(id, generation, parent_id, source)
165        });
166        match recorded {
167            NodeSlotUpdate::Inserted {
168                id: recorded_id,
169                generation: recorded_generation,
170            } => {
171                debug_assert_eq!(recorded_id, id);
172                debug_assert_eq!(recorded_generation, generation);
173            }
174            NodeSlotUpdate::Replaced {
175                old_id,
176                old_generation,
177                new_id,
178                new_generation,
179            } => {
180                debug_assert_eq!(new_id, id);
181                debug_assert_eq!(new_generation, generation);
182                self.queue_replaced_slot_node_removal(old_id, old_generation);
183            }
184            NodeSlotUpdate::Reused { .. } => {
185                log::warn!(
186                    target: "cranpose::compose::emit",
187                    "slot writer reported reuse for newly emitted node #{id}",
188                );
189            }
190        }
191        self.core.last_node_reused.set(Some(false));
192        id
193    }
194
195    #[track_caller]
196    pub fn emit_node<N: Node + 'static>(&self, init: impl FnOnce() -> N) -> NodeId {
197        let source = crate::caller_location_key();
198        self.emit_node_box::<N>(source, |_| EmittedNode::Fresh(Box::new(init())))
199    }
200
201    #[track_caller]
202    pub fn emit_recyclable_node<N: Node + 'static>(
203        &self,
204        init: impl FnOnce() -> N,
205        reset: impl FnOnce(&mut N),
206    ) -> NodeId {
207        let source = crate::caller_location_key();
208        self.emit_node_box::<N>(source, |applier| {
209            let key = TypeId::of::<N>();
210            if let Some(mut recycled) = applier.take_recycled_node(key) {
211                if let Some(typed) = recycled.node_mut().as_any_mut().downcast_mut::<N>() {
212                    reset(typed);
213                    return EmittedNode::Recycled(recycled);
214                }
215                log::warn!(
216                    target: "cranpose::compose::emit",
217                    "discarding recycled node shell with mismatched type for {}",
218                    std::any::type_name::<N>(),
219                );
220            }
221
222            let node = Box::new(init());
223            applier.record_fresh_recyclable_creation(key);
224            if let Some(shell) = node.rehouse_for_recycle() {
225                applier.seed_recycled_node_shell(key, node.recycle_pool_limit(), shell);
226            }
227            EmittedNode::Fresh(node)
228        })
229    }
230
231    fn advance_recompose_child_cursor(&self) -> Option<usize> {
232        let cursor = self.core.recompose_child_cursor.get()?;
233        self.core.recompose_child_cursor.set(Some(cursor + 1));
234        Some(cursor)
235    }
236
237    pub(crate) fn attach_to_parent(&self, id: NodeId) {
238        if self.attach_to_current_parent(id) {
239            return;
240        }
241        self.attach_without_parent_frame(id);
242    }
243
244    fn attach_to_current_parent(&self, id: NodeId) -> bool {
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 false;
257                };
258                let attach_mode = frame.attach_mode;
259                if parent_id == id {
260                    return true;
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                        child_node.set_parent_for_bubbling(parent_id);
271                    }
272                }
273                if matches!(attach_mode, ParentAttachMode::ImmediateAppend) {
274                    self.commands_mut().push(Command::AttachChild {
275                        parent_id,
276                        child_id: id,
277                        insert_index: None,
278                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
279                    });
280                }
281                return true;
282            }
283        }
284        false
285    }
286
287    fn attach_without_parent_frame(&self, id: NodeId) {
288        let in_subcompose = !self.subcompose_stack().is_empty();
289        if in_subcompose {
290            let has_parent = {
291                let mut applier = self.borrow_applier();
292                applier
293                    .get_mut(id)
294                    .is_ok_and(|node| node.parent().is_some())
295            };
296
297            if !has_parent {
298                let mut subcompose_stack = self.subcompose_stack();
299                if let Some(frame) = subcompose_stack.last_mut() {
300                    frame.nodes.push(id);
301                }
302            }
303            return;
304        }
305
306        if let Some(parent_hint) = self.core.recompose_parent_hint.get() {
307            if parent_hint == id {
308                debug_assert_ne!(
309                    parent_hint, id,
310                    "a node cannot be attached as its own parent"
311                );
312                return;
313            }
314            let parent_status = {
315                let mut applier = self.borrow_applier();
316                applier.get_mut(id).map_or(None, |node| node.parent())
317            };
318            match parent_status {
319                Some(existing) if existing == parent_hint => {
320                    self.advance_recompose_child_cursor();
321                }
322                None => {
323                    let insert_index = self.advance_recompose_child_cursor();
324                    self.commands_mut().push(Command::AttachChild {
325                        parent_id: parent_hint,
326                        child_id: id,
327                        insert_index,
328                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
329                    });
330                }
331                Some(_) => {}
332            }
333            return;
334        }
335
336        let has_parent = {
337            let mut applier = self.borrow_applier();
338            applier
339                .get_mut(id)
340                .is_ok_and(|node| node.parent().is_some())
341        };
342        if has_parent {
343            return;
344        }
345
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 =
357            node.as_any_mut()
358                .downcast_mut::<N>()
359                .ok_or_else(|| 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        let mut previous = ChildList::new();
371        if reused || in_subcompose {
372            previous.extend(self.get_node_children(id));
373        } else {
374            let existing_children = self.get_node_children(id);
375            if !existing_children.is_empty() {
376                previous.extend(existing_children);
377            }
378        }
379        let attach_mode = if in_subcompose || !previous.is_empty() {
380            ParentAttachMode::DeferredSync
381        } else {
382            ParentAttachMode::ImmediateAppend
383        };
384
385        self.parent_stack().push(ParentFrame {
386            id,
387            previous,
388            new_children: ChildList::new(),
389            new_children_membership: None,
390            attach_mode,
391            synthetic_root: false,
392        });
393    }
394
395    pub fn pop_parent(&self) {
396        let frame_opt = {
397            let mut stack = self.parent_stack();
398            stack.pop()
399        };
400        if let Some(frame) = frame_opt {
401            let ParentFrame {
402                id,
403                previous,
404                new_children,
405                new_children_membership: _new_children_membership,
406                attach_mode,
407                synthetic_root: _synthetic_root,
408            } = frame;
409
410            log::trace!(target: "cranpose::compose::parent", "pop_parent: node #{id}");
411            log::trace!(
412                target: "cranpose::compose::parent",
413                "previous children: {previous:?}"
414            );
415            log::trace!(
416                target: "cranpose::compose::parent",
417                "new children: {new_children:?}"
418            );
419            if matches!(attach_mode, ParentAttachMode::DeferredSync) {
420                let _ = previous;
421                self.commands_mut().push(Command::SyncChildren {
422                    parent_id: id,
423                    expected_children: new_children,
424                });
425            }
426        }
427    }
428
429    pub(crate) fn take_commands(&self) -> CommandQueue {
430        std::mem::take(&mut *self.commands_mut())
431    }
432
433    /// Applies any pending applier commands and runtime updates.
434    ///
435    /// This is useful during measure-time subcomposition to ensure newly created
436    /// nodes are available for measurement before the full composition is committed.
437    pub fn apply_pending_commands(&self) -> Result<(), NodeError> {
438        let commands = self.take_commands();
439        let runtime_handle = self.runtime_handle();
440        let result = {
441            let mut applier = self.borrow_applier();
442            let mut result = commands.apply(&mut *applier);
443            if result.is_ok() {
444                for update in runtime_handle.take_updates() {
445                    if let Err(err) = update.apply(&mut *applier) {
446                        result = Err(err);
447                        break;
448                    }
449                }
450            }
451            result
452        };
453        if result.is_err() {
454            let host = self.active_slots_host();
455            if !host.has_active_pass() {
456                host.abandon_after_apply_failure();
457            }
458        }
459        result
460    }
461
462    pub fn register_side_effect(&self, effect: impl FnOnce() + 'static) {
463        self.side_effects_mut().push(Box::new(effect));
464    }
465
466    pub fn take_side_effects(&self) -> Vec<Box<dyn FnOnce()>> {
467        std::mem::take(&mut *self.side_effects_mut())
468    }
469
470    pub(crate) fn root(&self) -> Option<NodeId> {
471        self.core.root.get()
472    }
473
474    pub(crate) fn set_root(&self, node: Option<NodeId>) {
475        self.core.root.set(node);
476    }
477}