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    pub(crate) fn attach_to_parent_with_mode(
235        &self,
236        id: NodeId,
237        force_reparent_current_parent: bool,
238    ) {
239        let mut parent_stack = self.parent_stack();
240        if let Some(parent_id) = parent_stack.last().map(|frame| frame.id) {
241            let stale_root_parent = self.core.root.get() == Some(parent_id) && {
242                let mut applier = self.borrow_applier();
243                applier.get_mut(parent_id).is_err()
244            };
245            if stale_root_parent {
246                parent_stack.pop();
247                self.set_root(None);
248            } else {
249                let Some(frame) = parent_stack.last_mut() else {
250                    return;
251                };
252                let attach_mode = frame.attach_mode;
253                if parent_id == id {
254                    return;
255                }
256                if matches!(attach_mode, ParentAttachMode::DeferredSync) {
257                    frame.new_children.push(id);
258                }
259                drop(parent_stack);
260
261                {
262                    let mut applier = self.borrow_applier();
263                    if let Ok(child_node) = applier.get_mut(id) {
264                        let existing_parent = child_node.parent();
265                        let should_set = if force_reparent_current_parent {
266                            existing_parent != Some(parent_id)
267                        } else {
268                            match existing_parent {
269                                None => true,
270                                Some(existing) => {
271                                    let root_id = self.core.root.get();
272                                    parent_id != root_id.unwrap_or(0)
273                                        || existing == root_id.unwrap_or(0)
274                                }
275                            }
276                        };
277                        if should_set {
278                            child_node.set_parent_for_bubbling(parent_id);
279                        }
280                    }
281                }
282                if matches!(attach_mode, ParentAttachMode::ImmediateAppend) {
283                    self.commands_mut().push(Command::AttachChild {
284                        parent_id,
285                        child_id: id,
286                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
287                    });
288                }
289                return;
290            }
291        }
292        drop(parent_stack);
293
294        let in_subcompose = !self.subcompose_stack().is_empty();
295        if in_subcompose {
296            let has_parent = {
297                let mut applier = self.borrow_applier();
298                applier
299                    .get_mut(id)
300                    .map(|node| node.parent().is_some())
301                    .unwrap_or(false)
302            };
303
304            if !has_parent {
305                let mut subcompose_stack = self.subcompose_stack();
306                if let Some(frame) = subcompose_stack.last_mut() {
307                    frame.nodes.push(id);
308                }
309            }
310            return;
311        }
312
313        if let Some(parent_hint) = self.core.recompose_parent_hint.get() {
314            if parent_hint == id {
315                debug_assert_ne!(
316                    parent_hint, id,
317                    "a node cannot be attached as its own parent"
318                );
319                return;
320            }
321            let parent_status = {
322                let mut applier = self.borrow_applier();
323                applier
324                    .get_mut(id)
325                    .map(|node| node.parent())
326                    .unwrap_or(None)
327            };
328            match parent_status {
329                Some(existing) if existing == parent_hint => {}
330                None => {
331                    self.commands_mut().push(Command::AttachChild {
332                        parent_id: parent_hint,
333                        child_id: id,
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}