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