Skip to main content

cranpose_core/
composer.rs

1use std::{
2    any::Any,
3    cell::{Cell, RefCell, RefMut},
4    hash::Hash,
5    marker::PhantomData,
6    rc::{Rc, Weak},
7};
8
9use smallvec::SmallVec;
10
11use crate::{
12    AnchorId, Applier, ApplierHost, COMMAND_FLUSH_THRESHOLD, ChildList, Command, CommandQueue,
13    CompositionLocal, DirtyBubble, Key, LocalKey, LocalStackSnapshot, LocalStateEntry,
14    MutableState, Node, NodeError, NodeId, Owned, ProvidedValue, RecomposeOptions, RecomposeScope,
15    RecomposeScopeInner, RecycledNode, RetentionMode, RetentionPolicy, RuntimeHandle, ScopeId,
16    SlotId, SlotPassOutcome, SlotTable, SlotsHost, SnapshotStateList, SnapshotStateMap,
17    SnapshotStateObserver, StaticCompositionLocal, StaticLocalEntry, SubcomposeState,
18    collections::map::{HashMap, HashSet},
19    composer_context, explicit_group_key_seed,
20    retention::{RetainKey, RetentionManager},
21    runtime,
22    slot::{FinishGroupResult, GroupStart, GroupStartKind, PayloadKind, RootNodeIds, ValueSlotId},
23};
24
25pub struct ValueSlotHandle<'pass, T: 'static> {
26    slot: ValueSlotId,
27    _pass: PhantomData<&'pass Composer>,
28    _value: PhantomData<fn() -> T>,
29}
30
31impl<T: 'static> Copy for ValueSlotHandle<'_, T> {}
32
33impl<T: 'static> Clone for ValueSlotHandle<'_, T> {
34    fn clone(&self) -> Self {
35        *self
36    }
37}
38
39impl<T: 'static> ValueSlotHandle<'_, T> {
40    pub(crate) fn new(slot: ValueSlotId) -> Self {
41        Self {
42            slot,
43            _pass: PhantomData,
44            _value: PhantomData,
45        }
46    }
47
48    pub(crate) fn slot(self) -> ValueSlotId {
49        self.slot
50    }
51}
52
53fn slots_storage_key(host: &Rc<SlotsHost>) -> usize {
54    host.storage_key()
55}
56
57fn bind_slots_host_to_runtime_state(
58    state: &Rc<ComposerRuntimeState>,
59    host: &Rc<SlotsHost>,
60) -> Rc<SlotsHost> {
61    if let Some(bound_state) = host.runtime_state() {
62        if Rc::ptr_eq(&bound_state, state) {
63            state.bind_slots_host(host);
64            return Rc::clone(host);
65        }
66        drop(bound_state);
67        if host.rebind_orphaned_runtime_state(state) {
68            state.bind_slots_host(host);
69            return Rc::clone(host);
70        }
71        log::error!(
72            "slot host already belongs to a different composer runtime state; using a fresh slot host"
73        );
74        let replacement = Rc::new(SlotsHost::new(SlotTable::new()));
75        state.bind_slots_host(&replacement);
76        return replacement;
77    }
78    state.bind_slots_host(host);
79    Rc::clone(host)
80}
81
82struct GroupEntry {
83    key: crate::slot::GroupKey,
84    restored: Option<crate::slot::DetachedSubtree>,
85    placeholder_for: Option<crate::slot::GroupKey>,
86}
87
88struct GroupScopeEntry<'a> {
89    options: RecomposeOptions,
90    start_kind: GroupStartKind,
91    host: &'a SlotsHost,
92    group: crate::slot::ActiveGroupId,
93}
94
95struct SlotHostPassGuard {
96    core: Rc<ComposerCore>,
97    host: Rc<SlotsHost>,
98    active: bool,
99}
100
101impl SlotHostPassGuard {
102    fn close(&mut self) {
103        if !self.active {
104            return;
105        }
106        if self.host.has_active_pass() {
107            self.host.abandon_active_pass();
108        }
109        match self.core.slot_hosts.borrow_mut().pop() {
110            Some(host) if Rc::ptr_eq(&host, &self.host) => {}
111            Some(_) => {
112                log::error!("slot host stack mismatch while closing slot host pass");
113            }
114            None => {
115                log::error!("slot host stack underflow while closing slot host pass");
116            }
117        }
118        self.active = false;
119    }
120}
121
122impl Drop for SlotHostPassGuard {
123    fn drop(&mut self) {
124        self.close();
125    }
126}
127
128pub(crate) struct PendingMovable {
129    pub(crate) key: crate::slot::GroupKey,
130    pub(crate) placeholder: AnchorId,
131    pub(crate) parent_scope: Option<Weak<RecomposeScopeInner>>,
132}
133
134fn reparent_restored_scopes(
135    host: &SlotsHost,
136    group: crate::slot::ActiveGroupId,
137    old_hint: Option<NodeId>,
138    parent_hint: Option<NodeId>,
139) {
140    host.with_write_session(|slots| {
141        slots.for_each_subtree_scope(group, |scope| {
142            if scope.parent_hint() == old_hint {
143                scope.set_parent_hint(parent_hint);
144            }
145            scope.reactivate();
146        });
147    });
148}
149
150fn movable_retain_key(id: Key) -> RetainKey {
151    RetainKey::for_group(
152        None,
153        crate::slot::GroupKey::new(crate::slot::MOVABLE_STATIC_KEY, Some(id), 0),
154    )
155}
156
157pub(crate) struct ComposerRuntimeState {
158    retention_by_host: RefCell<HashMap<usize, RetentionManager>>,
159    pending_movables_by_host: RefCell<HashMap<usize, Vec<PendingMovable>>>,
160    retention_policy: Cell<RetentionPolicy>,
161    live_hosts: RefCell<HashMap<usize, std::rc::Weak<SlotsHost>>>,
162    applier_host: RefCell<Option<std::rc::Weak<dyn ApplierHost>>>,
163}
164
165impl Default for ComposerRuntimeState {
166    fn default() -> Self {
167        Self {
168            retention_by_host: RefCell::new(HashMap::default()),
169            pending_movables_by_host: RefCell::new(HashMap::default()),
170            retention_policy: Cell::new(RetentionPolicy::default()),
171            live_hosts: RefCell::new(HashMap::default()),
172            applier_host: RefCell::new(None),
173        }
174    }
175}
176
177impl ComposerRuntimeState {
178    pub(crate) fn clear_host_storage_key(&self, host_key: usize, table: &mut SlotTable) {
179        self.retention_by_host.borrow_mut().remove(&host_key);
180        self.pending_movables_by_host.borrow_mut().remove(&host_key);
181        self.live_hosts.borrow_mut().remove(&host_key);
182        table.release_scopes();
183    }
184
185    pub(crate) fn force_recompose_retained_scopes(&self, host_key: usize) {
186        if let Some(retention) = self.retention_by_host.borrow().get(&host_key) {
187            for scope in retention
188                .subtrees()
189                .flat_map(crate::slot::DetachedSubtree::scopes)
190            {
191                scope.force_recompose();
192            }
193        }
194    }
195
196    pub(crate) fn bind_applier_host(&self, applier: &Rc<dyn ApplierHost>) {
197        *self.applier_host.borrow_mut() = Some(Rc::downgrade(applier));
198    }
199
200    pub(crate) fn has_live_applier_host(&self) -> bool {
201        self.applier_host
202            .borrow()
203            .as_ref()
204            .and_then(std::rc::Weak::upgrade)
205            .is_some()
206    }
207
208    pub(crate) fn bind_slots_host(self: &Rc<Self>, host: &Rc<SlotsHost>) {
209        host.bind_runtime_state(self);
210        self.live_hosts
211            .borrow_mut()
212            .insert(host.storage_key(), Rc::downgrade(host));
213    }
214
215    pub(crate) fn set_retention_policy(&self, policy: RetentionPolicy) {
216        self.retention_policy.set(policy);
217    }
218
219    pub(crate) fn retention_policy(&self) -> RetentionPolicy {
220        self.retention_policy.get()
221    }
222
223    pub(crate) fn take_retained(
224        &self,
225        host: &Rc<SlotsHost>,
226        key: RetainKey,
227        preflight: impl FnOnce(&mut crate::slot::DetachedSubtree) -> bool,
228    ) -> Option<crate::slot::DetachedSubtree> {
229        let host_key = slots_storage_key(host);
230        let mut retention = self.retention_by_host.borrow_mut();
231        let subtree = retention
232            .get_mut(&host_key)?
233            .take_after_restore_preflight(key, preflight);
234        if retention
235            .get(&host_key)
236            .is_some_and(|manager| manager.is_empty() && manager.evictions_total() == 0)
237        {
238            retention.remove(&host_key);
239        }
240        subtree
241    }
242
243    pub(crate) fn insert_retained(
244        &self,
245        host: &Rc<SlotsHost>,
246        key: RetainKey,
247        subtree: crate::slot::DetachedSubtree,
248        pinned: bool,
249    ) -> Vec<crate::slot::DetachedSubtree> {
250        let policy = self.retention_policy();
251        let mut retention_by_host = self.retention_by_host.borrow_mut();
252        let manager = retention_by_host
253            .entry(slots_storage_key(host))
254            .or_insert_with(|| RetentionManager::new(policy));
255        manager.set_policy(policy);
256        if pinned {
257            manager.insert_pinned(key, subtree)
258        } else {
259            manager.insert(key, subtree)
260        }
261    }
262
263    pub(crate) fn take_retained_movable(
264        &self,
265        id: Key,
266    ) -> Option<(Rc<SlotsHost>, crate::slot::DetachedSubtree)> {
267        self.take_retained_movable_by_key(movable_retain_key(id))
268    }
269
270    fn take_retained_movable_by_key(
271        &self,
272        key: RetainKey,
273    ) -> Option<(Rc<SlotsHost>, crate::slot::DetachedSubtree)> {
274        let mut retention_by_host = self.retention_by_host.borrow_mut();
275        let (host_key, manager) = retention_by_host
276            .iter_mut()
277            .find(|(_, manager)| manager.contains(key))?;
278        let host = self.host_for_storage_key(*host_key)?;
279        let subtree = manager.take(key)?;
280        Some((host, subtree))
281    }
282
283    /// Takes a movable's retained content out of a slot table other than the
284    /// one composing, which is how it crosses a subcomposition.
285    pub(crate) fn take_retained_movable_elsewhere(
286        &self,
287        besides: &Rc<SlotsHost>,
288        key: RetainKey,
289    ) -> Option<(Rc<SlotsHost>, crate::slot::DetachedSubtree)> {
290        let besides_key = slots_storage_key(besides);
291        let mut retention_by_host = self.retention_by_host.borrow_mut();
292        let (host_key, manager) = retention_by_host
293            .iter_mut()
294            .find(|(host_key, manager)| **host_key != besides_key && manager.contains(key))?;
295        let host = self.host_for_storage_key(*host_key)?;
296        let subtree = manager.take(key)?;
297        Some((host, subtree))
298    }
299
300    /// Whether a movable's content is retained in any slot table, which is
301    /// what a site waiting for it has to know: the content may have been let
302    /// go by a parent composed in another host, such as a subcomposition.
303    pub(crate) fn movable_retained_anywhere(&self, key: RetainKey) -> bool {
304        self.retention_by_host
305            .borrow()
306            .values()
307            .any(|manager| manager.contains(key))
308    }
309
310    /// The slot table a movable's content is attached to right now, if any.
311    pub(crate) fn host_holding_movable(&self, id: Key) -> Option<Rc<SlotsHost>> {
312        self.live_hosts
313            .borrow()
314            .values()
315            .filter_map(std::rc::Weak::upgrade)
316            .find(|host| host.borrow().movable_is_attached(id))
317    }
318
319    pub(crate) fn record_pending_movable(&self, host: &Rc<SlotsHost>, pending: PendingMovable) {
320        let mut by_host = self.pending_movables_by_host.borrow_mut();
321        let sites = by_host.entry(slots_storage_key(host)).or_default();
322        if sites
323            .iter()
324            .all(|site| site.placeholder != pending.placeholder)
325        {
326            sites.push(pending);
327        }
328    }
329
330    pub(crate) fn take_pending_movables(&self, host: &Rc<SlotsHost>) -> Vec<PendingMovable> {
331        self.pending_movables_by_host
332            .borrow_mut()
333            .remove(&slots_storage_key(host))
334            .unwrap_or_default()
335    }
336
337    /// Every slot table with a site still waiting for movable content. The
338    /// content may be let go by a parent composed in another table, so a pass
339    /// that ends has to look wider than itself.
340    pub(crate) fn hosts_awaiting_movables(&self) -> Vec<Rc<SlotsHost>> {
341        let waiting = self
342            .pending_movables_by_host
343            .borrow()
344            .iter()
345            .filter(|(_, sites)| !sites.is_empty())
346            .map(|(host_key, _)| *host_key)
347            .collect::<Vec<_>>();
348        waiting
349            .into_iter()
350            .filter_map(|host_key| self.host_for_storage_key(host_key))
351            .collect()
352    }
353
354    pub(crate) fn keep_pending_movables(&self, host: &Rc<SlotsHost>, sites: Vec<PendingMovable>) {
355        if sites.is_empty() {
356            return;
357        }
358        self.pending_movables_by_host
359            .borrow_mut()
360            .insert(slots_storage_key(host), sites);
361    }
362
363    pub(crate) fn advance_retention_pass(
364        &self,
365        host: &Rc<SlotsHost>,
366    ) -> Vec<crate::slot::DetachedSubtree> {
367        let host_key = slots_storage_key(host);
368        let policy = self.retention_policy();
369        let mut retention_by_host = self.retention_by_host.borrow_mut();
370        let Some(manager) = retention_by_host.get_mut(&host_key) else {
371            return Vec::new();
372        };
373        manager.set_policy(policy);
374        manager.advance_pass()
375    }
376
377    pub(crate) fn fill_slot_debug_snapshot(
378        &self,
379        host: &SlotsHost,
380        snapshot: &mut crate::SlotDebugSnapshot,
381    ) {
382        let retention = self.retention_debug_stats(host.storage_key());
383        snapshot.retained_subtree_count = retention.subtree_count;
384        snapshot.retained_group_count = retention.group_count;
385        snapshot.retained_payload_count = retention.payload_count;
386        snapshot.retained_node_count = retention.node_count;
387        snapshot.retained_scope_count = retention.scope_count;
388    }
389
390    pub(crate) fn slot_retention_debug_stats(
391        &self,
392        host: &SlotsHost,
393    ) -> crate::slot::SlotRetentionDebugStats {
394        let retention = self.retention_debug_stats(host.storage_key());
395        crate::slot::SlotRetentionDebugStats {
396            retained_subtree_count: retention.subtree_count,
397            retained_group_count: retention.group_count,
398            retained_payload_count: retention.payload_count,
399            retained_node_count: retention.node_count,
400            retained_scope_count: retention.scope_count,
401            retained_anchor_count: retention.anchor_count,
402            retained_heap_bytes: retention.heap_bytes,
403            retained_evictions_total: retention.evictions_total,
404        }
405    }
406
407    pub(crate) fn compact_table_identity_storage_for_host(
408        &self,
409        host: &SlotsHost,
410        table: &mut SlotTable,
411        compact_anchors: bool,
412        compact_payloads: bool,
413    ) {
414        if !compact_anchors && !compact_payloads {
415            return;
416        }
417
418        let host_key = host.storage_key();
419        let mut retention = self.retention_by_host.borrow_mut();
420        if let Some(retained) = retention.get_mut(&host_key) {
421            if compact_anchors {
422                table.compact_anchor_registry_storage(Some(&mut *retained));
423            }
424            if compact_payloads {
425                table.compact_payload_anchor_registry_storage(Some(&mut *retained));
426            }
427        } else {
428            if compact_anchors {
429                table.compact_anchor_registry_storage(None);
430            }
431            if compact_payloads {
432                table.compact_payload_anchor_registry_storage(None);
433            }
434        }
435    }
436
437    pub(crate) fn clear_host(&self, host: &SlotsHost, table: &mut SlotTable) {
438        let host_key = host.storage_key();
439        debug_assert!(
440            self.host_retention_is_empty(host),
441            "host retention must be drained before clearing host ownership"
442        );
443        self.clear_host_storage_key(host_key, table);
444    }
445
446    fn deactivate_and_queue_subtrees(
447        &self,
448        retention: RetentionManager,
449        table: &mut SlotTable,
450        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
451    ) {
452        for subtree in retention.into_subtrees() {
453            for scope in subtree.scopes() {
454                scope.deactivate();
455            }
456            table.invalidate_detached_subtree_anchors(&subtree);
457            lifecycle.queue_subtree_disposal(subtree);
458        }
459    }
460
461    pub(crate) fn dispose_retained_subtrees_for_host(
462        &self,
463        host_key: usize,
464        table: &mut SlotTable,
465        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
466    ) -> Result<(), NodeError> {
467        let applier_host = self
468            .applier_host
469            .borrow()
470            .as_ref()
471            .and_then(std::rc::Weak::upgrade);
472        if let Some(applier_host) = applier_host.as_ref() {
473            let retention_by_host = self.retention_by_host.borrow();
474            let Some(retention) = retention_by_host.get(&host_key) else {
475                return Ok(());
476            };
477            let mut applier = applier_host.borrow_dyn();
478            for subtree in retention.subtrees() {
479                crate::slot::dispose_detached_subtree_now(&mut *applier, subtree)?;
480            }
481        }
482        let Some(retention) = self.retention_by_host.borrow_mut().remove(&host_key) else {
483            return Ok(());
484        };
485        self.deactivate_and_queue_subtrees(retention, table, lifecycle);
486        Ok(())
487    }
488
489    pub(crate) fn abandon_retained_subtrees_for_host(
490        &self,
491        host_key: usize,
492        table: &mut SlotTable,
493        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
494    ) {
495        let Some(retention) = self.retention_by_host.borrow_mut().remove(&host_key) else {
496            self.clear_host_storage_key(host_key, table);
497            return;
498        };
499        self.deactivate_and_queue_subtrees(retention, table, lifecycle);
500        self.clear_host_storage_key(host_key, table);
501    }
502
503    pub(crate) fn host_retention_is_empty(&self, host: &SlotsHost) -> bool {
504        self.retention_by_host
505            .borrow()
506            .get(&host.storage_key())
507            .is_none_or(RetentionManager::is_empty)
508    }
509
510    #[cfg(any(test, debug_assertions))]
511    pub(crate) fn debug_verify_host(&self, host: &SlotsHost, table: &SlotTable) {
512        if let Some(retention) = self.retention_by_host.borrow().get(&host.storage_key()) {
513            retention.debug_verify(table);
514        }
515    }
516
517    #[cfg(test)]
518    pub(crate) fn validate_host_retention(
519        &self,
520        host: &SlotsHost,
521        table: &SlotTable,
522    ) -> Result<(), crate::slot::SlotInvariantError> {
523        if let Some(retention) = self.retention_by_host.borrow().get(&host.storage_key()) {
524            retention.validate(table)?;
525        }
526        Ok(())
527    }
528
529    pub(crate) fn host_for_storage_key(&self, storage_key: usize) -> Option<Rc<SlotsHost>> {
530        self.live_hosts
531            .borrow()
532            .get(&storage_key)
533            .and_then(std::rc::Weak::upgrade)
534    }
535
536    fn retention_debug_stats(&self, host_key: usize) -> crate::retention::RetentionDebugStats {
537        self.retention_by_host
538            .borrow()
539            .get(&host_key)
540            .map(RetentionManager::debug_stats)
541            .unwrap_or_default()
542    }
543}
544
545pub(crate) struct ParentFrame {
546    pub(crate) id: NodeId,
547    pub(crate) previous: ChildList,
548    pub(crate) new_children: ChildList,
549    pub(crate) new_children_membership: Option<HashSet<NodeId>>,
550    pub(crate) attach_mode: ParentAttachMode,
551    pub(crate) synthetic_root: bool,
552}
553
554#[derive(Clone, Copy)]
555pub(crate) enum InitialParentFrame {
556    SyntheticRoot,
557    RealParent,
558}
559
560const LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD: usize = 16;
561
562#[derive(Clone, Copy, Debug, PartialEq, Eq)]
563pub(crate) enum ParentAttachMode {
564    ImmediateAppend,
565    DeferredSync,
566}
567
568#[derive(Default)]
569pub(crate) struct SubcomposeFrame {
570    pub(crate) nodes: Vec<NodeId>,
571    pub(crate) scopes: Vec<RecomposeScope>,
572}
573
574/// The values one provider supplies, over the frames of the providers around
575/// it.
576pub(crate) struct LocalFrame {
577    values: SmallVec<[(LocalKey, Rc<dyn Any>); 2]>,
578    parent: LocalStackSnapshot,
579}
580
581/// The entry the innermost provider in `stack` supplies for `key`.
582fn provided_entry(stack: &LocalStackSnapshot, key: &LocalKey) -> Option<Rc<dyn Any>> {
583    let mut frame = stack.as_deref();
584    while let Some(current) = frame {
585        if let Some((_, entry)) = current.values.iter().find(|(provided, _)| provided == key) {
586            return Some(Rc::clone(entry));
587        }
588        frame = current.parent.as_deref();
589    }
590    None
591}
592
593pub(crate) struct ComposerCore {
594    pub(crate) shared_state: Rc<ComposerRuntimeState>,
595    pub(crate) slots: Rc<SlotsHost>,
596    slot_hosts: RefCell<Vec<Rc<SlotsHost>>>,
597    pub(crate) applier: Rc<dyn ApplierHost>,
598    pub(crate) runtime: RuntimeHandle,
599    pub(crate) observer: SnapshotStateObserver,
600    pub(crate) parent_stack: RefCell<Vec<ParentFrame>>,
601    pub(crate) subcompose_stack: RefCell<Vec<SubcomposeFrame>>,
602    pub(crate) root: Cell<Option<NodeId>>,
603    pub(crate) commands: RefCell<CommandQueue>,
604    pub(crate) scope_stack: RefCell<Vec<RecomposeScope>>,
605    subcomposition_owner_scope: RefCell<Option<RecomposeScope>>,
606    pub(crate) local_stack: RefCell<LocalStackSnapshot>,
607    pub(crate) side_effects: RefCell<Vec<Box<dyn FnOnce()>>>,
608    pub(crate) pending_scope_options: RefCell<Option<RecomposeOptions>>,
609    pub(crate) phase: Cell<crate::Phase>,
610    pub(crate) last_node_reused: Cell<Option<bool>>,
611    pub(crate) recompose_parent_hint: Cell<Option<NodeId>>,
612    pub(crate) recompose_child_cursor: Cell<Option<usize>>,
613    pub(crate) root_render_requested: Cell<bool>,
614    pub(crate) _not_send: PhantomData<*const ()>,
615}
616
617/// The composition context inherited by work that is composed in another slot
618/// host. Besides composition locals, this carries the source owner scope so a
619/// secondary tree cannot outlive the composition that supplied its callbacks.
620#[derive(Clone)]
621pub struct CapturedCompositionContext {
622    locals: LocalStackSnapshot,
623    owner_scope: Option<Weak<RecomposeScopeInner>>,
624}
625
626impl CapturedCompositionContext {
627    /// Total deactivations along the capturing scope's owner chain right now;
628    /// see [`crate::RecomposeScope::owner_chain_deactivation_epoch`]. Zero
629    /// when the context has no owner scope or it is gone.
630    pub fn owner_chain_deactivation_epoch(&self) -> u64 {
631        self.owner_scope
632            .as_ref()
633            .and_then(Weak::upgrade)
634            .map_or(0, |inner| {
635                crate::RecomposeScope { inner }.owner_chain_deactivation_epoch()
636            })
637    }
638}
639
640fn take_subcompose_frame(core: &ComposerCore, operation: &str) -> SubcomposeFrame {
641    match core.subcompose_stack.borrow_mut().pop() {
642        Some(frame) => frame,
643        None => {
644            log::error!("subcompose stack underflow while finishing {operation}");
645            SubcomposeFrame::default()
646        }
647    }
648}
649
650struct SubcomposeStackGuard {
651    core: Rc<ComposerCore>,
652    leaked: bool,
653}
654
655impl Drop for SubcomposeStackGuard {
656    fn drop(&mut self) {
657        if !self.leaked {
658            self.core.subcompose_stack.borrow_mut().pop();
659        }
660    }
661}
662
663impl ComposerCore {
664    pub(crate) fn open_branch_fold(&self, key: Key) -> BranchGroupGuard {
665        let hosts = self.slot_hosts.borrow();
666        let host = hosts.last().unwrap_or(&self.slots);
667        BranchGroupGuard {
668            fold: host
669                .try_push_branch_fold(key)
670                .map(|token| (Rc::clone(host), token)),
671        }
672    }
673
674    pub(crate) fn new(
675        shared_state: Rc<ComposerRuntimeState>,
676        slots: Rc<SlotsHost>,
677        applier: Rc<dyn ApplierHost>,
678        runtime: RuntimeHandle,
679        observer: SnapshotStateObserver,
680        root: Option<NodeId>,
681        initial_parent_frame: InitialParentFrame,
682    ) -> Self {
683        let parent_stack = if let Some(root_id) = root {
684            vec![ParentFrame {
685                id: root_id,
686                previous: ChildList::new(),
687                new_children: ChildList::new(),
688                new_children_membership: None,
689                attach_mode: ParentAttachMode::DeferredSync,
690                synthetic_root: matches!(initial_parent_frame, InitialParentFrame::SyntheticRoot),
691            }]
692        } else {
693            Vec::new()
694        };
695
696        Self {
697            shared_state,
698            slots,
699            slot_hosts: RefCell::new(Vec::new()),
700            applier,
701            runtime,
702            observer,
703            parent_stack: RefCell::new(parent_stack),
704            subcompose_stack: RefCell::new(Vec::new()),
705            root: Cell::new(root),
706            commands: RefCell::new(CommandQueue::default()),
707            scope_stack: RefCell::new(Vec::new()),
708            subcomposition_owner_scope: RefCell::new(None),
709            local_stack: RefCell::new(None),
710            side_effects: RefCell::new(Vec::new()),
711            pending_scope_options: RefCell::new(None),
712            phase: Cell::new(crate::Phase::Compose),
713            last_node_reused: Cell::new(None),
714            recompose_parent_hint: Cell::new(None),
715            recompose_child_cursor: Cell::new(None),
716            root_render_requested: Cell::new(false),
717            _not_send: PhantomData,
718        }
719    }
720}
721
722#[derive(Clone)]
723pub struct Composer {
724    pub(crate) core: Rc<ComposerCore>,
725}
726
727pub struct BranchGroupGuard {
728    fold: Option<(Rc<SlotsHost>, usize)>,
729}
730
731impl Drop for BranchGroupGuard {
732    fn drop(&mut self) {
733        let Some((host, token)) = &self.fold else {
734            return;
735        };
736        if !host.try_close_branch_fold(*token) {
737            log::error!("a branch fold guard closed while its slot host was busy");
738        }
739    }
740}
741
742pub(crate) enum EmittedNode {
743    Fresh(Box<dyn Node>),
744    Recycled(RecycledNode),
745}
746
747impl Composer {
748    pub(crate) fn new_with_shared_state(
749        shared_state: Rc<ComposerRuntimeState>,
750        slots: Rc<SlotsHost>,
751        applier: Rc<dyn ApplierHost>,
752        runtime: RuntimeHandle,
753        observer: SnapshotStateObserver,
754        root: Option<NodeId>,
755    ) -> Self {
756        Self::new_with_shared_state_with_parent_frame(
757            shared_state,
758            slots,
759            applier,
760            runtime,
761            observer,
762            root,
763            InitialParentFrame::SyntheticRoot,
764        )
765    }
766
767    fn new_with_shared_state_with_parent_frame(
768        shared_state: Rc<ComposerRuntimeState>,
769        slots: Rc<SlotsHost>,
770        applier: Rc<dyn ApplierHost>,
771        runtime: RuntimeHandle,
772        observer: SnapshotStateObserver,
773        root: Option<NodeId>,
774        initial_parent_frame: InitialParentFrame,
775    ) -> Self {
776        shared_state.bind_applier_host(&applier);
777        let slots = bind_slots_host_to_runtime_state(&shared_state, &slots);
778        let core = Rc::new(ComposerCore::new(
779            shared_state,
780            slots,
781            applier,
782            runtime,
783            observer,
784            root,
785            initial_parent_frame,
786        ));
787        Self { core }
788    }
789
790    pub fn new(
791        slots: Rc<SlotsHost>,
792        applier: Rc<dyn ApplierHost>,
793        runtime: RuntimeHandle,
794        observer: SnapshotStateObserver,
795        root: Option<NodeId>,
796    ) -> Self {
797        Self::new_with_shared_state_with_parent_frame(
798            slots
799                .runtime_state()
800                .unwrap_or_else(|| Rc::new(ComposerRuntimeState::default())),
801            slots,
802            applier,
803            runtime,
804            observer,
805            root,
806            InitialParentFrame::RealParent,
807        )
808    }
809
810    pub(crate) fn from_core(core: Rc<ComposerCore>) -> Self {
811        Self { core }
812    }
813
814    pub(crate) fn clone_core(&self) -> Rc<ComposerCore> {
815        Rc::clone(&self.core)
816    }
817
818    fn observer(&self) -> SnapshotStateObserver {
819        self.core.observer.clone()
820    }
821
822    pub(crate) fn request_root_render(&self) {
823        self.core.root_render_requested.set(true);
824    }
825
826    pub(crate) fn take_root_render_request(&self) -> bool {
827        self.core.root_render_requested.replace(false)
828    }
829
830    pub(crate) fn observe_scope<R>(&self, scope: &RecomposeScope, block: impl FnOnce() -> R) -> R {
831        self.core
832            .observer
833            .observe_reads(scope.clone(), super::RecomposeScope::invalidate, block)
834    }
835
836    pub fn active_slots_host(&self) -> Rc<SlotsHost> {
837        self.core
838            .slot_hosts
839            .borrow()
840            .last()
841            .cloned()
842            .unwrap_or_else(|| Rc::clone(&self.core.slots))
843    }
844
845    fn with_active_slots_host<R>(&self, f: impl FnOnce(&SlotsHost) -> R) -> R {
846        let hosts = self.core.slot_hosts.borrow();
847        f(hosts.last().unwrap_or(&self.core.slots))
848    }
849
850    pub(crate) fn with_slots<R>(&self, f: impl FnOnce(&SlotTable) -> R) -> R {
851        self.with_active_slots_host(|host| f(&host.borrow()))
852    }
853
854    pub(crate) fn with_slots_mut<R>(&self, f: impl FnOnce(&mut SlotTable) -> R) -> R {
855        self.with_active_slots_host(|host| f(&mut host.borrow_mut()))
856    }
857
858    pub(crate) fn with_slot_session_mut<R>(
859        &self,
860        f: impl FnOnce(&mut crate::slot::SlotWriteSession<'_>) -> R,
861    ) -> R {
862        self.with_active_slots_host(|host| host.with_write_session(f))
863    }
864
865    pub(crate) fn try_with_slot_host_pass<R>(
866        &self,
867        slots: Rc<SlotsHost>,
868        mode: crate::slot::SlotPassMode,
869        f: impl FnOnce(&Composer) -> R,
870    ) -> Result<(R, SlotPassOutcome), NodeError> {
871        let mut guard = self.begin_slot_host_pass(&slots, mode);
872        let result = f(self);
873        let outcome = self.finish_slot_host_pass(&guard.host)?;
874        guard.close();
875        Ok((result, outcome))
876    }
877
878    pub(crate) fn with_slot_host_pass<R>(
879        &self,
880        slots: Rc<SlotsHost>,
881        mode: crate::slot::SlotPassMode,
882        f: impl FnOnce(&Composer) -> R,
883    ) -> (R, SlotPassOutcome) {
884        let mut guard = self.begin_slot_host_pass(&slots, mode);
885        let result = f(self);
886        let outcome = match self.finish_slot_host_pass(&guard.host) {
887            Ok(outcome) => outcome,
888            Err(err) => {
889                log::error!("slot host pass finalization failed: {err}");
890                SlotPassOutcome::default()
891            }
892        };
893        guard.close();
894        (result, outcome)
895    }
896
897    pub(crate) fn with_slot_override<R>(
898        &self,
899        slots: Rc<SlotsHost>,
900        f: impl FnOnce(&Composer) -> R,
901    ) -> (R, SlotPassOutcome) {
902        self.with_slot_host_pass(slots, crate::slot::SlotPassMode::Compose, f)
903    }
904
905    fn begin_slot_host_pass(
906        &self,
907        slots: &Rc<SlotsHost>,
908        mode: crate::slot::SlotPassMode,
909    ) -> SlotHostPassGuard {
910        let slots = bind_slots_host_to_runtime_state(&self.core.shared_state, slots);
911        slots.begin_pass(mode);
912        {
913            let mut stack = self.core.slot_hosts.borrow_mut();
914            if let Some(parent) = stack.last()
915                && !Rc::ptr_eq(parent, &slots)
916            {
917                parent.note_nested_host(&slots);
918            }
919            stack.push(Rc::clone(&slots));
920        }
921        SlotHostPassGuard {
922            core: self.clone_core(),
923            host: slots,
924            active: true,
925        }
926    }
927
928    fn finish_slot_host_pass(&self, slots: &Rc<SlotsHost>) -> Result<SlotPassOutcome, NodeError> {
929        let finished = {
930            let mut applier = self.core.applier.borrow_dyn();
931            slots.finish_pass(&mut *applier)
932        }?;
933        self.handle_detached_children_in_host(slots, None, finished.detached_root_children)?;
934        self.wake_sites_whose_movable_arrived();
935        self.evict_retained_subtrees_for_host(slots)?;
936        slots.complete_pass_cleanup(&finished.outcome);
937        Ok(finished.outcome)
938    }
939
940    fn wake_sites_whose_movable_arrived(&self) {
941        for host in self.core.shared_state.hosts_awaiting_movables() {
942            self.wake_sites_in_host(&host);
943        }
944    }
945
946    fn wake_sites_in_host(&self, slots: &Rc<SlotsHost>) {
947        let pending = self.core.shared_state.take_pending_movables(slots);
948        if pending.is_empty() {
949            return;
950        }
951        let mut waiting = Vec::new();
952        for site in pending {
953            if !slots.borrow().group_is_active(site.placeholder) {
954                continue;
955            }
956            let retain_key = RetainKey::for_group(None, site.key);
957            if !self.core.shared_state.movable_retained_anywhere(retain_key) {
958                waiting.push(site);
959                continue;
960            }
961            match site.parent_scope.as_ref().and_then(RecomposeScope::upgrade) {
962                Some(scope) => {
963                    scope.force_recompose();
964                    scope.invalidate();
965                }
966                None => log::error!(
967                    "movable content {:?} arrived for a site whose scope is gone",
968                    site.key
969                ),
970            }
971        }
972        self.core.shared_state.keep_pending_movables(slots, waiting);
973    }
974
975    pub(crate) fn forget_movables(&self, ids: &[Key]) -> Result<(), NodeError> {
976        for id in ids {
977            let Some((host, subtree)) = self.core.shared_state.take_retained_movable(*id) else {
978                continue;
979            };
980            self.dispose_detached_subtree_in_host(&host, subtree)?;
981            host.flush_pending_drops();
982        }
983        Ok(())
984    }
985
986    pub(crate) fn parent_stack(&self) -> RefMut<'_, Vec<ParentFrame>> {
987        self.core.parent_stack.borrow_mut()
988    }
989
990    pub(crate) fn current_parent_hint(&self) -> Option<NodeId> {
991        let stack = self.core.parent_stack.borrow();
992        let stack_hint = stack
993            .last()
994            .and_then(|frame| (!frame.synthetic_root).then_some(frame.id));
995        stack_hint.or_else(|| self.core.recompose_parent_hint.get())
996    }
997
998    pub(crate) fn subcompose_stack(&self) -> RefMut<'_, Vec<SubcomposeFrame>> {
999        self.core.subcompose_stack.borrow_mut()
1000    }
1001
1002    pub(crate) fn commands_mut(&self) -> RefMut<'_, CommandQueue> {
1003        self.core.commands.borrow_mut()
1004    }
1005
1006    pub(crate) fn enqueue_semantics_invalidation(&self, id: NodeId) {
1007        self.commands_mut().push(Command::BubbleDirty {
1008            node_id: id,
1009            bubble: DirtyBubble::SEMANTICS,
1010        });
1011    }
1012
1013    pub(crate) fn scope_stack(&self) -> RefMut<'_, Vec<RecomposeScope>> {
1014        self.core.scope_stack.borrow_mut()
1015    }
1016
1017    pub(crate) fn local_stack(&self) -> RefMut<'_, LocalStackSnapshot> {
1018        self.core.local_stack.borrow_mut()
1019    }
1020
1021    pub(crate) fn current_local_stack(&self) -> LocalStackSnapshot {
1022        self.core.local_stack.borrow().clone()
1023    }
1024
1025    pub(crate) fn side_effects_mut(&self) -> RefMut<'_, Vec<Box<dyn FnOnce()>>> {
1026        self.core.side_effects.borrow_mut()
1027    }
1028
1029    fn pending_scope_options(&self) -> RefMut<'_, Option<RecomposeOptions>> {
1030        self.core.pending_scope_options.borrow_mut()
1031    }
1032
1033    pub(crate) fn borrow_applier(&self) -> RefMut<'_, dyn Applier> {
1034        self.core.applier.borrow_dyn()
1035    }
1036
1037    /// Records nodes whose retained subtrees a reused subcompose slot just
1038    /// rebound to new content.
1039    ///
1040    /// The rebinding recomposes inline during measure, so every repass it
1041    /// schedules is consumed by the layout pass already running — nothing
1042    /// else survives to tell the scoped scene update these subtrees changed,
1043    /// and a translate-only update would keep their stale layers (measured:
1044    /// a 60pt lazy scroll presented the old row's text at the new row's
1045    /// position). The structural-change set is the one channel drained
1046    /// after layout, so the rebound children ride it.
1047    pub fn record_rebound_slot_children(&self, children: &[NodeId]) {
1048        let mut applier = self.borrow_applier();
1049        for &child in children {
1050            applier.record_structural_change(child);
1051        }
1052    }
1053
1054    /// Registers a virtual node in the Applier.
1055    ///
1056    /// This is used by SubcomposeLayoutNode to register virtual container nodes
1057    /// so that subsequent insert_child commands can find them and attach children.
1058    /// Without this, virtual nodes would only exist in SubcomposeLayoutNodeInner.virtual_nodes
1059    /// and applier.get_mut(virtual_node_id) would fail, breaking child attachment.
1060    pub fn register_virtual_node(
1061        &self,
1062        node_id: NodeId,
1063        node: Box<dyn Node>,
1064    ) -> Result<(), NodeError> {
1065        let mut applier = self.borrow_applier();
1066        applier.insert_with_id(node_id, node)
1067    }
1068
1069    /// Checks if a node has no parent (is a root node).
1070    /// Used by SubcomposeMeasureScope to filter subcompose results.
1071    pub fn node_has_no_parent(&self, node_id: NodeId) -> bool {
1072        let mut applier = self.borrow_applier();
1073        match applier.get_mut(node_id) {
1074            Ok(node) => node.parent().is_none(),
1075            Err(_) => true,
1076        }
1077    }
1078
1079    /// Returns a node's parent, or `None` for a root node.
1080    ///
1081    /// Returns an error when the node is no longer present in the applier.
1082    pub fn node_parent(&self, node_id: NodeId) -> Result<Option<NodeId>, NodeError> {
1083        self.borrow_applier()
1084            .get_mut(node_id)
1085            .map(|node| node.parent())
1086    }
1087
1088    /// Gets the children of a node from the Applier.
1089    ///
1090    /// This is used by SubcomposeLayoutNode to get children of virtual nodes
1091    /// directly from the Applier, where insert_child commands have been applied.
1092    pub fn get_node_children(&self, node_id: NodeId) -> SmallVec<[NodeId; 8]> {
1093        let mut applier = self.borrow_applier();
1094        match applier.get_mut(node_id) {
1095            Ok(node) => {
1096                let mut children = SmallVec::<[NodeId; 8]>::new();
1097                node.collect_children_into(&mut children);
1098                children
1099            }
1100            Err(_) => SmallVec::<[NodeId; 8]>::new(),
1101        }
1102    }
1103
1104    pub fn nodes_need_measure(&self, node_ids: &[NodeId]) -> bool {
1105        let mut applier = self.borrow_applier();
1106        node_ids.iter().any(|node_id| {
1107            applier
1108                .get_mut(*node_id)
1109                .is_ok_and(|node| node.needs_measure())
1110        })
1111    }
1112
1113    /// Whether any of `node_ids` carries a pending *layout* (placement) repass.
1114    ///
1115    /// Layout-only dirtiness is not a subset of measure dirtiness: a scroll
1116    /// offset change keeps every measured size intact and therefore bubbles
1117    /// `needs_layout` alone. Callers that gate cache reuse on
1118    /// [`Self::nodes_need_measure`] must also consult this, or a node whose
1119    /// *position* changed will replay its stale cached placement forever.
1120    pub fn nodes_need_layout(&self, node_ids: &[NodeId]) -> bool {
1121        let mut applier = self.borrow_applier();
1122        node_ids.iter().any(|node_id| {
1123            applier
1124                .get_mut(*node_id)
1125                .is_ok_and(|node| node.needs_layout())
1126        })
1127    }
1128
1129    /// Records a child node in the current parent frame's expected children list.
1130    ///
1131    /// Used by SubcomposeLayout's `perform_subcompose` to register virtual nodes
1132    /// with the outer composer's parent frame. This ensures that the `pop_parent`
1133    /// call at the end of `subcompose_slot` generates a correct `SyncChildren`
1134    /// command that preserves (rather than removes) the virtual nodes.
1135    ///
1136    /// Without this, `pop_parent` would generate `SyncChildren { expected: [] }`,
1137    /// which removes all virtual nodes and their subtrees from the applier.
1138    pub fn record_subcompose_child(&self, child_id: NodeId) {
1139        let mut parent_stack = self.parent_stack();
1140        if let Some(frame) = parent_stack.last_mut()
1141            && matches!(frame.attach_mode, ParentAttachMode::DeferredSync)
1142        {
1143            if let Some(membership) = frame.new_children_membership.as_mut() {
1144                if membership.insert(child_id) {
1145                    frame.new_children.push(child_id);
1146                }
1147            } else if frame.new_children.len() >= LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD {
1148                let mut membership = HashSet::default();
1149                membership.reserve(frame.new_children.len() + 1);
1150                membership.extend(frame.new_children.iter().copied());
1151                if membership.insert(child_id) {
1152                    frame.new_children.push(child_id);
1153                }
1154                frame.new_children_membership = Some(membership);
1155            } else if !frame.new_children.contains(&child_id) {
1156                frame.new_children.push(child_id);
1157            }
1158        }
1159    }
1160
1161    /// Clears all children of a node in the Applier.
1162    ///
1163    /// This is used by SubcomposeLayoutNode when reusing a virtual node for
1164    /// different content. Without clearing, old children remain attached,
1165    /// causing duplicate/interleaved items in lazy lists after scrolling.
1166    pub fn clear_node_children(&self, node_id: NodeId) {
1167        let mut applier = self.borrow_applier();
1168        if let Ok(node) = applier.get_mut(node_id) {
1169            node.update_children(&[]);
1170        }
1171    }
1172
1173    pub fn install<R>(&self, f: impl FnOnce(&Composer) -> R) -> R {
1174        let _composer_guard = composer_context::enter(self);
1175        runtime::push_active_runtime(&self.core.runtime);
1176        struct Guard;
1177        impl Drop for Guard {
1178            fn drop(&mut self) {
1179                runtime::pop_active_runtime();
1180            }
1181        }
1182        let guard = Guard;
1183        let result = f(self);
1184        drop(guard);
1185        result
1186    }
1187
1188    pub(crate) fn flush_pending_commands_if_large(&self) -> Result<(), NodeError> {
1189        let queued = self.core.commands.borrow().len();
1190        if queued < COMMAND_FLUSH_THRESHOLD {
1191            return Ok(());
1192        }
1193        self.apply_pending_commands()
1194    }
1195
1196    fn resolve_group_entry(
1197        &self,
1198        host: &Rc<SlotsHost>,
1199        seed: crate::slot::GroupKeySeed,
1200        parent_scope_id: Option<ScopeId>,
1201    ) -> GroupEntry {
1202        let key = host.with_write_session(|slots| slots.reserve_group_key(seed));
1203        let retain_key = RetainKey::for_group(parent_scope_id, key);
1204        let restored = self
1205            .core
1206            .shared_state
1207            .take_retained(host, retain_key, |subtree| {
1208                host.with_write_session(|slots| slots.retained_restore_ready(key, subtree))
1209            })
1210            .or_else(|| self.take_movable_from_another_table(host, retain_key, key));
1211        if restored.is_some() || !key.is_movable() {
1212            return GroupEntry {
1213                key,
1214                restored,
1215                placeholder_for: None,
1216            };
1217        }
1218        let attached_elsewhere = self.movable_attached_elsewhere(host, key);
1219        if !attached_elsewhere {
1220            return GroupEntry {
1221                key,
1222                restored: None,
1223                placeholder_for: None,
1224            };
1225        }
1226        let id = key.explicit_key.unwrap_or_default();
1227        let placeholder = host.with_write_session(|slots| {
1228            slots.reserve_group_key(crate::slot::GroupKeySeed::movable_placeholder(id))
1229        });
1230        GroupEntry {
1231            key: placeholder,
1232            restored: None,
1233            placeholder_for: Some(key),
1234        }
1235    }
1236
1237    /// Movable content that was let go by a parent composed in another slot
1238    /// table — a `SubcomposeLayout`'s, or the one a subcomposition owns — is
1239    /// retained there, not here. Bring it over: the table that held it lets
1240    /// go of its anchors, and this one issues its own.
1241    fn take_movable_from_another_table(
1242        &self,
1243        host: &Rc<SlotsHost>,
1244        retain_key: RetainKey,
1245        key: crate::slot::GroupKey,
1246    ) -> Option<crate::slot::DetachedSubtree> {
1247        if !key.is_movable() {
1248            return None;
1249        }
1250        let (source, mut subtree) = self
1251            .core
1252            .shared_state
1253            .take_retained_movable_elsewhere(host, retain_key)?;
1254        source
1255            .borrow_mut()
1256            .invalidate_detached_subtree_anchors(&subtree);
1257        if host.with_write_session(|slots| slots.retained_restore_ready(key, &mut subtree)) {
1258            return Some(subtree);
1259        }
1260        log::error!(
1261            "movable content {key:?} could not be taken over by the slot table that asked for it"
1262        );
1263        if let Err(error) = self.dispose_detached_subtree_in_host(host, subtree) {
1264            log::error!("disposing movable content that could not move failed: {error}");
1265        }
1266        None
1267    }
1268
1269    /// Whether the movable's content is attached to some parent that is not
1270    /// the one composing, in this slot table or in another.
1271    fn movable_attached_elsewhere(&self, host: &Rc<SlotsHost>, key: crate::slot::GroupKey) -> bool {
1272        if host.with_write_session(|slots| slots.movable_attached_elsewhere(key)) {
1273            return true;
1274        }
1275        let Some(id) = key.movable_id() else {
1276            return false;
1277        };
1278        self.core
1279            .shared_state
1280            .host_holding_movable(id)
1281            .is_some_and(|holder| !Rc::ptr_eq(&holder, host))
1282    }
1283
1284    fn scope_for_started_group(
1285        &self,
1286        host: &SlotsHost,
1287        group: crate::slot::ActiveGroupId,
1288        scope: Option<RecomposeScope>,
1289    ) -> RecomposeScope {
1290        if let Some(scope) = scope {
1291            return scope;
1292        }
1293        let scope = RecomposeScope::new(self.runtime_handle());
1294        host.with_write_session(|slots| slots.set_group_scope(group, scope.clone()));
1295        scope
1296    }
1297
1298    fn enter_group_scope(&self, scope_ref: &RecomposeScope, entry: GroupScopeEntry<'_>) {
1299        let GroupScopeEntry {
1300            options,
1301            start_kind,
1302            host,
1303            group,
1304        } = entry;
1305        scope_ref.reactivate();
1306        {
1307            let mut stack = self.scope_stack();
1308            let parent_scope = stack.last();
1309            scope_ref.set_parent_scope(parent_scope);
1310            if parent_scope.is_none() {
1311                scope_ref.set_lifetime_owner_scope(
1312                    self.core.subcomposition_owner_scope.borrow().as_ref(),
1313                );
1314            } else {
1315                scope_ref.set_lifetime_owner_scope(None);
1316            }
1317            stack.push(scope_ref.clone());
1318        }
1319        scope_ref.set_retention_mode(options.retention);
1320
1321        if options.force_recompose {
1322            scope_ref.force_recompose();
1323        } else if options.force_reuse {
1324            scope_ref.force_reuse();
1325        }
1326        let restored = matches!(start_kind, GroupStartKind::Restored);
1327        if restored {
1328            scope_ref.force_recompose();
1329        }
1330
1331        scope_ref.set_slots_host(host);
1332
1333        {
1334            let mut stack = self.subcompose_stack();
1335            if let Some(frame) = stack.last_mut() {
1336                frame.scopes.push(scope_ref.clone());
1337            }
1338        }
1339
1340        scope_ref.snapshot_locals(&self.core.local_stack.borrow());
1341        let parent_hint = self.current_parent_hint();
1342        if restored {
1343            reparent_restored_scopes(host, group, scope_ref.parent_hint(), parent_hint);
1344        }
1345        scope_ref.set_parent_hint(parent_hint);
1346    }
1347
1348    #[inline(never)]
1349    fn with_group_in_active_pass_dyn(
1350        &self,
1351        host: &Rc<SlotsHost>,
1352        key: crate::slot::GroupKeySeed,
1353        f: &mut dyn FnMut(&Composer),
1354    ) {
1355        struct GroupGuard<'a> {
1356            composer: &'a Composer,
1357            host: &'a Rc<SlotsHost>,
1358            scope: RecomposeScope,
1359        }
1360
1361        impl Drop for GroupGuard<'_> {
1362            fn drop(&mut self) {
1363                self.composer.close_group_body(self.host, &self.scope);
1364                self.scope.mark_recomposed();
1365                #[expect(
1366                    clippy::redundant_closure_for_method_calls,
1367                    reason = "the method path is not general over the session lifetime"
1368                )]
1369                self.host.with_write_session(|slots| slots.end_group());
1370                if let Err(err) = self.composer.flush_pending_commands_if_large() {
1371                    log::error!("mid-composition command flush failed: {err}");
1372                }
1373            }
1374        }
1375
1376        let options = self.pending_scope_options().take().unwrap_or_default();
1377        let parent_scope_id = self
1378            .core
1379            .scope_stack
1380            .borrow()
1381            .last()
1382            .map(RecomposeScope::id);
1383        let GroupEntry {
1384            key: reserved_key,
1385            restored,
1386            placeholder_for,
1387        } = self.resolve_group_entry(host, key, parent_scope_id);
1388        let parent_node = self.current_parent_hint();
1389        let GroupStart {
1390            group,
1391            anchor,
1392            scope,
1393            kind,
1394        } = host.with_write_session(|slots| slots.begin_group(reserved_key, restored, parent_node));
1395        let scope_ref = self.scope_for_started_group(host, group, scope);
1396        if let Some(movable_key) = placeholder_for {
1397            self.core.shared_state.record_pending_movable(
1398                host,
1399                PendingMovable {
1400                    key: movable_key,
1401                    placeholder: anchor,
1402                    parent_scope: self
1403                        .core
1404                        .scope_stack
1405                        .borrow()
1406                        .last()
1407                        .map(RecomposeScope::downgrade),
1408                },
1409            );
1410        }
1411        self.enter_group_scope(
1412            &scope_ref,
1413            GroupScopeEntry {
1414                options,
1415                start_kind: kind,
1416                host,
1417                group,
1418            },
1419        );
1420
1421        let guard = GroupGuard {
1422            composer: self,
1423            host,
1424            scope: scope_ref,
1425        };
1426        if placeholder_for.is_none() {
1427            self.observe_scope(&guard.scope, || f(self));
1428        }
1429        guard.scope.mark_composed_once();
1430        drop(guard);
1431    }
1432
1433    fn with_group_seed_dyn(&self, key: crate::slot::GroupKeySeed, f: &mut dyn FnMut(&Composer)) {
1434        let host = self.active_slots_host();
1435        if host.has_active_pass() {
1436            self.with_group_in_active_pass_dyn(&host, key, f);
1437            return;
1438        }
1439        self.with_slot_host_pass(host, crate::slot::SlotPassMode::Compose, |composer| {
1440            composer.with_group_in_active_pass_dyn(&composer.active_slots_host(), key, f);
1441        });
1442    }
1443
1444    pub(crate) fn with_group_seed<R>(
1445        &self,
1446        key: crate::slot::GroupKeySeed,
1447        f: impl FnOnce(&Composer) -> R,
1448    ) -> R {
1449        let mut f = Some(f);
1450        let mut result = None;
1451        self.with_group_seed_dyn(key, &mut |composer| {
1452            let f = f.take().expect("group body must run at most once");
1453            result = Some(f(composer));
1454        });
1455        result.expect("group body must run exactly once")
1456    }
1457
1458    pub(crate) fn with_movable_group(&self, id: Key, f: impl FnOnce(&Composer)) {
1459        let mut f = Some(f);
1460        self.with_group_seed_dyn(crate::slot::GroupKeySeed::movable(id), &mut |composer| {
1461            if let Some(f) = f.take() {
1462                f(composer);
1463            }
1464        });
1465    }
1466
1467    pub fn with_group<R>(&self, key: Key, f: impl FnOnce(&Composer) -> R) -> R {
1468        self.with_group_seed(crate::slot::GroupKeySeed::unkeyed(key), f)
1469    }
1470
1471    pub fn cranpose_with_reuse<R>(
1472        &self,
1473        key: Key,
1474        mut options: RecomposeOptions,
1475        f: impl FnOnce(&Composer) -> R,
1476    ) -> R {
1477        options.retention = RetentionMode::RetainWhenInactive;
1478        self.pending_scope_options().replace(options);
1479        self.with_group(key, f)
1480    }
1481
1482    #[track_caller]
1483    pub fn with_key<K: Hash, R>(&self, key: &K, f: impl FnOnce(&Composer) -> R) -> R {
1484        let seed = explicit_group_key_seed(key, std::panic::Location::caller());
1485        self.with_group_seed(seed, f)
1486    }
1487
1488    #[doc(hidden)]
1489    pub fn __branch_group_deferred(&self, key: Key) -> BranchGroupGuard {
1490        self.core.open_branch_fold(key)
1491    }
1492
1493    fn dispose_detached_nodes(&self, nodes: impl IntoIterator<Item = NodeId>) {
1494        for node_id in nodes {
1495            self.commands_mut().push(Command::callback(move |applier| {
1496                crate::slot::dispose_detached_node_now(applier, node_id)
1497            }));
1498        }
1499    }
1500
1501    fn detached_root_parent_commands(
1502        &self,
1503        subtree: &crate::slot::DetachedSubtree,
1504        context: &'static str,
1505    ) -> Result<Vec<(NodeId, Option<NodeId>)>, NodeError> {
1506        let mut root_nodes = Vec::new();
1507        subtree.collect_root_nodes_checked_into(&mut root_nodes, context);
1508        let mut roots = Vec::with_capacity(root_nodes.len());
1509        for root in root_nodes {
1510            let parent_id = {
1511                let mut applier = self.borrow_applier();
1512                applier.get_mut(root)?.parent()
1513            };
1514            roots.push((root, parent_id));
1515        }
1516        Ok(roots)
1517    }
1518
1519    fn retain_detached_subtree_in_host(
1520        &self,
1521        slots_host: &Rc<SlotsHost>,
1522        parent_scope: Option<ScopeId>,
1523        subtree: crate::slot::DetachedSubtree,
1524    ) -> Result<(), NodeError> {
1525        let Some(root_key) = subtree.root_key_checked() else {
1526            log::error!("retention rejected detached subtree without a root group");
1527            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1528            return Ok(());
1529        };
1530        let root_detaches = self.detached_root_parent_commands(&subtree, "retention")?;
1531        subtree.scopes().for_each(RecomposeScope::deactivate);
1532        for (root, parent_id) in root_detaches {
1533            if let Some(parent_id) = parent_id {
1534                self.commands_mut().push(Command::DetachChild {
1535                    parent_id,
1536                    child_id: root,
1537                });
1538            }
1539        }
1540        let evicted = self.core.shared_state.insert_retained(
1541            slots_host,
1542            RetainKey::for_group(parent_scope, root_key),
1543            subtree,
1544            root_key.is_movable(),
1545        );
1546        for subtree in evicted {
1547            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1548        }
1549        Ok(())
1550    }
1551
1552    fn evict_retained_subtrees_for_host(
1553        &self,
1554        slots_host: &Rc<SlotsHost>,
1555    ) -> Result<(), NodeError> {
1556        let evicted = self.core.shared_state.advance_retention_pass(slots_host);
1557        for subtree in evicted {
1558            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1559        }
1560        Ok(())
1561    }
1562
1563    fn dispose_detached_subtree_in_host(
1564        &self,
1565        slots_host: &Rc<SlotsHost>,
1566        subtree: crate::slot::DetachedSubtree,
1567    ) -> Result<(), NodeError> {
1568        let root_nodes = self
1569            .detached_root_parent_commands(&subtree, "disposal")?
1570            .into_iter()
1571            .map(|(root, _)| root);
1572        subtree.scopes().for_each(RecomposeScope::deactivate);
1573        self.dispose_detached_nodes(root_nodes);
1574        slots_host.with_table_and_lifecycle_mut(|table, lifecycle| {
1575            table.invalidate_detached_subtree_anchors(&subtree);
1576            lifecycle.queue_subtree_disposal(subtree);
1577        });
1578        Ok(())
1579    }
1580
1581    fn handle_detached_children_in_host(
1582        &self,
1583        slots_host: &Rc<SlotsHost>,
1584        parent_scope: Option<ScopeId>,
1585        detached: Vec<crate::slot::DetachedSubtree>,
1586    ) -> Result<(), NodeError> {
1587        for mut subtree in detached {
1588            for movable in subtree.split_off_nested_movables() {
1589                self.retain_detached_subtree_in_host(slots_host, None, movable)?;
1590            }
1591            if subtree
1592                .root_key_checked()
1593                .is_some_and(crate::slot::GroupKey::is_movable)
1594            {
1595                self.retain_detached_subtree_in_host(slots_host, None, subtree)?;
1596                continue;
1597            }
1598            let retention_mode = subtree
1599                .root_scope()
1600                .map(RecomposeScope::retention_mode)
1601                .unwrap_or_default();
1602            match retention_mode {
1603                RetentionMode::DisposeWhenInactive => {
1604                    self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1605                }
1606                RetentionMode::RetainWhenInactive => {
1607                    self.retain_detached_subtree_in_host(slots_host, parent_scope, subtree)?;
1608                }
1609            }
1610        }
1611        Ok(())
1612    }
1613
1614    fn handle_finished_group_result(
1615        &self,
1616        host: &Rc<SlotsHost>,
1617        parent_scope: Option<ScopeId>,
1618        result: FinishGroupResult,
1619    ) {
1620        let FinishGroupResult {
1621            detached_children,
1622            direct_nodes,
1623            root_nodes,
1624            was_skipped,
1625        } = result;
1626        if was_skipped {
1627            self.attach_root_nodes(root_nodes);
1628        }
1629        self.dispose_detached_nodes(direct_nodes);
1630        if detached_children.is_empty() {
1631            return;
1632        }
1633        if let Err(err) =
1634            self.handle_detached_children_in_host(host, parent_scope, detached_children)
1635        {
1636            log::error!("detached subtree handling failed while closing a group: {err}");
1637        }
1638    }
1639
1640    pub(crate) fn close_current_group_body_for_scope(&self, scope: &RecomposeScope) {
1641        self.close_group_body(&self.active_slots_host(), scope);
1642    }
1643
1644    fn close_group_body(&self, host: &Rc<SlotsHost>, scope: &RecomposeScope) {
1645        #[expect(
1646            clippy::redundant_closure_for_method_calls,
1647            reason = "the method path is not general over the session lifetime"
1648        )]
1649        let result = host.with_write_session(|slots| slots.finish_group_body());
1650        self.handle_finished_group_result(host, Some(scope.id()), result);
1651        if let Some(popped) = self.scope_stack().pop() {
1652            debug_assert_eq!(
1653                popped.id(),
1654                scope.id(),
1655                "closed scope must match the active scope stack"
1656            );
1657        } else {
1658            log::error!("scope stack underflow while closing scope {}", scope.id());
1659        }
1660    }
1661
1662    #[track_caller]
1663    pub fn remember<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1664        self.remember_at(crate::caller_location_key(), init)
1665    }
1666
1667    #[doc(hidden)]
1668    pub fn remember_at<T: 'static>(
1669        &self,
1670        source: crate::Key,
1671        init: impl FnOnce() -> T,
1672    ) -> Owned<T> {
1673        self.with_slot_session_mut(|slots| {
1674            slots.remember_with_kind(PayloadKind::Remember, source, init)
1675        })
1676    }
1677
1678    #[track_caller]
1679    pub(crate) fn remember_internal<T: 'static>(
1680        &self,
1681        source_salt: crate::Key,
1682        init: impl FnOnce() -> T,
1683    ) -> Owned<T> {
1684        let source = crate::caller_location_key() ^ source_salt;
1685        self.with_slot_session_mut(|slots| {
1686            slots.remember_with_kind(PayloadKind::Internal, source, init)
1687        })
1688    }
1689
1690    #[track_caller]
1691    pub(crate) fn remember_effect<T: Default + 'static>(&self) -> Owned<T> {
1692        let source = crate::caller_location_key();
1693        self.with_slot_session_mut(|slots| slots.remember_effect::<T>(source))
1694    }
1695
1696    #[track_caller]
1697    pub fn use_value_slot<T: 'static>(&self, init: impl FnOnce() -> T) -> ValueSlotHandle<'_, T> {
1698        let source = crate::caller_location_key();
1699        let slot = self.with_slot_session_mut(|slots| {
1700            slots.value_slot_with_kind(PayloadKind::Internal, source, init)
1701        });
1702        ValueSlotHandle::new(slot)
1703    }
1704
1705    #[doc(hidden)]
1706    #[track_caller]
1707    pub fn __use_param_slot<T: 'static>(&self, init: impl FnOnce() -> T) -> ValueSlotHandle<'_, T> {
1708        let source = crate::caller_location_key();
1709        let slot = self.with_slot_session_mut(|slots| {
1710            slots.value_slot_with_kind(PayloadKind::Param, source, init)
1711        });
1712        ValueSlotHandle::new(slot)
1713    }
1714
1715    #[doc(hidden)]
1716    #[track_caller]
1717    pub fn __use_return_slot<T: 'static>(
1718        &self,
1719        init: impl FnOnce() -> T,
1720    ) -> ValueSlotHandle<'_, T> {
1721        let source = crate::caller_location_key();
1722        let slot = self.with_slot_session_mut(|slots| {
1723            slots.value_slot_with_kind(PayloadKind::Return, source, init)
1724        });
1725        ValueSlotHandle::new(slot)
1726    }
1727
1728    #[doc(hidden)]
1729    pub fn __invalidate_return_consumer_scope(&self) {
1730        let Some(scope) = self.current_recompose_scope() else {
1731            self.request_root_render();
1732            return;
1733        };
1734
1735        if let Some(target) = scope.callback_promotion_target() {
1736            target.invalidate();
1737        } else {
1738            self.request_root_render();
1739        }
1740    }
1741
1742    pub fn with_slot_value<'pass, T: 'static, R>(
1743        &'pass self,
1744        handle: ValueSlotHandle<'pass, T>,
1745        f: impl FnOnce(&T) -> R,
1746    ) -> R {
1747        self.with_slots(|slots| f(slots.read_value(handle.slot())))
1748    }
1749
1750    pub fn with_slot_value_mut<'pass, T: 'static, R>(
1751        &'pass self,
1752        handle: ValueSlotHandle<'pass, T>,
1753        f: impl FnOnce(&mut T) -> R,
1754    ) -> R {
1755        self.with_slots_mut(|slots| f(slots.read_value_mut(handle.slot())))
1756    }
1757
1758    pub fn mutable_state_of<T: Clone + 'static>(&self, initial: T) -> MutableState<T> {
1759        MutableState::with_runtime(initial, self.runtime_handle())
1760    }
1761
1762    pub fn mutable_state_list_of<T, I>(&self, values: I) -> SnapshotStateList<T>
1763    where
1764        T: Clone + 'static,
1765        I: IntoIterator<Item = T>,
1766    {
1767        SnapshotStateList::with_runtime(values, self.runtime_handle())
1768    }
1769
1770    pub fn mutable_state_map_of<K, V, I>(&self, pairs: I) -> SnapshotStateMap<K, V>
1771    where
1772        K: Clone + Eq + Hash + 'static,
1773        V: Clone + 'static,
1774        I: IntoIterator<Item = (K, V)>,
1775    {
1776        SnapshotStateMap::with_runtime(pairs, self.runtime_handle())
1777    }
1778
1779    pub fn read_composition_local<T: Clone + 'static>(&self, local: &CompositionLocal<T>) -> T {
1780        self.provided_local::<LocalStateEntry<T>>(&local.key)
1781            .map_or_else(|| local.default_value(), |entry| entry.value())
1782    }
1783
1784    pub fn read_static_composition_local<T: Clone + 'static>(
1785        &self,
1786        local: &StaticCompositionLocal<T>,
1787    ) -> T {
1788        self.provided_local::<StaticLocalEntry<T>>(&local.key)
1789            .map_or_else(|| local.default_value(), |entry| entry.value())
1790    }
1791
1792    /// The innermost provided entry for `key`, when it is an `E`.
1793    fn provided_local<E: 'static>(&self, key: &LocalKey) -> Option<Rc<E>> {
1794        let entry = provided_entry(&self.core.local_stack.borrow(), key)?;
1795        entry
1796            .downcast::<E>()
1797            .inspect_err(|_| log::error!("composition local entry type mismatch for key {key:?}"))
1798            .ok()
1799    }
1800
1801    pub fn current_recompose_scope(&self) -> Option<RecomposeScope> {
1802        self.core.scope_stack.borrow().last().cloned()
1803    }
1804
1805    pub(crate) fn current_state_invalidation_scope(&self) -> Option<RecomposeScope> {
1806        let stack = self.core.scope_stack.borrow();
1807        stack
1808            .iter()
1809            .rev()
1810            .find(|scope| scope.has_recompose_callback())
1811            .cloned()
1812            .or_else(|| stack.last().cloned())
1813    }
1814
1815    pub fn phase(&self) -> crate::Phase {
1816        self.core.phase.get()
1817    }
1818
1819    pub(crate) fn set_phase(&self, phase: crate::Phase) {
1820        self.core.phase.set(phase);
1821    }
1822
1823    pub fn enter_phase(&self, phase: crate::Phase) {
1824        self.set_phase(phase);
1825    }
1826
1827    pub(crate) fn subcompose<R>(
1828        &self,
1829        state: &mut SubcomposeState,
1830        slot_id: SlotId,
1831        content: impl FnOnce(&Composer) -> R,
1832    ) -> (R, Vec<NodeId>) {
1833        match self.phase() {
1834            crate::Phase::Measure | crate::Phase::Layout => {}
1835            current => panic!(
1836                "subcompose() may only be called during measure or layout; current phase: {current:?}"
1837            ),
1838        }
1839
1840        self.subcompose_stack().push(SubcomposeFrame::default());
1841        let mut guard = SubcomposeStackGuard {
1842            core: self.clone_core(),
1843            leaked: false,
1844        };
1845
1846        let slot_host = state.get_or_create_slots(slot_id);
1847        let (result, _) = self.with_slot_override(slot_host, |composer| {
1848            composer.with_group(slot_id.raw(), |composer| content(composer))
1849        });
1850
1851        let frame = {
1852            let frame = take_subcompose_frame(&guard.core, "subcompose");
1853            guard.leaked = true;
1854            frame
1855        };
1856        let nodes = frame.nodes;
1857        let scopes = frame.scopes;
1858        state.register_active(slot_id, &nodes, &scopes);
1859        (result, nodes)
1860    }
1861
1862    pub fn subcompose_measurement<R>(
1863        &self,
1864        state: &mut SubcomposeState,
1865        slot_id: SlotId,
1866        content: impl FnOnce(&Composer) -> R,
1867    ) -> (R, Vec<NodeId>) {
1868        let (result, nodes) = self.subcompose(state, slot_id, content);
1869        let roots = nodes
1870            .into_iter()
1871            .filter(|&id| self.node_has_no_parent(id))
1872            .collect();
1873
1874        (result, roots)
1875    }
1876
1877    fn spin_up_subcompose_core(
1878        &self,
1879        slots: &Rc<SlotsHost>,
1880        root: Option<NodeId>,
1881        runtime_handle: &RuntimeHandle,
1882        locals: LocalStackSnapshot,
1883    ) -> Rc<ComposerCore> {
1884        let phase = self.phase();
1885        let shared_state = slots
1886            .runtime_state()
1887            .unwrap_or_else(|| Rc::clone(&self.core.shared_state));
1888        let core = Rc::new(ComposerCore::new(
1889            shared_state,
1890            Rc::clone(slots),
1891            Rc::clone(&self.core.applier),
1892            runtime_handle.clone(),
1893            self.observer(),
1894            root,
1895            InitialParentFrame::RealParent,
1896        ));
1897        core.phase.set(phase);
1898        *core.local_stack.borrow_mut() = locals;
1899        core
1900    }
1901
1902    fn flush_subcompose_pass(
1903        &self,
1904        commands: CommandQueue,
1905        runtime_handle: &RuntimeHandle,
1906        compact_applier: bool,
1907        side_effects: Vec<Box<dyn FnOnce()>>,
1908    ) -> Result<(), NodeError> {
1909        {
1910            let mut applier = self.borrow_applier();
1911            commands.apply(&mut *applier)?;
1912            for update in runtime_handle.take_updates() {
1913                update.apply(&mut *applier)?;
1914            }
1915        }
1916        if compact_applier {
1917            self.core.applier.compact();
1918            self.core.applier.borrow_dyn().clear_recycled_nodes();
1919        }
1920        composer_context::without_composer(|| {
1921            for effect in side_effects {
1922                effect();
1923            }
1924        });
1925        Ok(())
1926    }
1927
1928    pub fn subcompose_in<R>(
1929        &self,
1930        slots: &Rc<SlotsHost>,
1931        root: Option<NodeId>,
1932        f: impl FnOnce(&Composer) -> R,
1933    ) -> Result<R, NodeError> {
1934        let runtime_handle = self.runtime_handle();
1935        let locals = self.current_local_stack();
1936        let core = self.spin_up_subcompose_core(slots, root, &runtime_handle, locals);
1937        let composer = Composer::from_core(core);
1938        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
1939            let (output, outcome) = composer.try_with_slot_host_pass(
1940                Rc::clone(slots),
1941                crate::slot::SlotPassMode::Compose,
1942                |composer| f(composer),
1943            )?;
1944            let commands = composer.take_commands();
1945            let side_effects = composer.take_side_effects();
1946            Ok((output, commands, side_effects, outcome.compacted))
1947        })?;
1948        self.flush_subcompose_pass(commands, &runtime_handle, compact_applier, side_effects)?;
1949        Ok(result)
1950    }
1951
1952    /// Captures the composition context at the current point so work composed
1953    /// in another slot host inherits both locals and source ownership.
1954    ///
1955    /// A `SubcomposeLayout` captures this while it is being composed and replays
1956    /// it while subcomposing off the measure pass, so content that is
1957    /// subcomposed during layout observes the same composition locals as the
1958    /// `SubcomposeLayout` call site — matching Jetpack Compose, where a
1959    /// subcomposition inherits the composition locals of the layout that
1960    /// created it rather than whatever happens to be in scope during measure
1961    /// (which, after composition unwinds, no longer carries ancestor providers).
1962    pub fn capture_composition_context(&self) -> CapturedCompositionContext {
1963        CapturedCompositionContext {
1964            locals: self.current_local_stack(),
1965            owner_scope: self
1966                .current_recompose_scope()
1967                .map(|scope| scope.downgrade()),
1968        }
1969    }
1970
1971    /// Subcomposes content using an isolated SlotsHost without resetting it.
1972    /// Unlike `subcompose_in`, this preserves existing slot state across calls,
1973    /// allowing efficient reuse during measurement passes. This is critical for
1974    /// lazy lists where items need stable slot positions.
1975    pub fn subcompose_slot<R>(
1976        &self,
1977        slots: &Rc<SlotsHost>,
1978        root: Option<NodeId>,
1979        f: impl FnOnce(&Composer) -> R,
1980    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
1981        let context = self.capture_composition_context();
1982        self.subcompose_slot_with_context(slots, root, &context, f)
1983    }
1984
1985    /// Like [`Composer::subcompose_slot`], but uses a context captured at the
1986    /// source composition site. This is required for measure-time composition,
1987    /// where the source scope is no longer on the active stack.
1988    pub fn subcompose_slot_with_context<R>(
1989        &self,
1990        slots: &Rc<SlotsHost>,
1991        root: Option<NodeId>,
1992        context: &CapturedCompositionContext,
1993        f: impl FnOnce(&Composer) -> R,
1994    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
1995        let runtime_handle = self.runtime_handle();
1996        let locals = context.locals.clone();
1997        let core = self.spin_up_subcompose_core(slots, root, &runtime_handle, locals);
1998        *core.subcomposition_owner_scope.borrow_mut() = context
1999            .owner_scope
2000            .as_ref()
2001            .and_then(RecomposeScope::upgrade);
2002        let composer = Composer::from_core(core);
2003        composer.subcompose_stack().push(SubcomposeFrame::default());
2004        let mut guard = SubcomposeStackGuard {
2005            core: composer.clone_core(),
2006            leaked: false,
2007        };
2008        let root_group_key = crate::location_key(file!(), line!(), column!());
2009        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
2010            let (output, outcome) = composer.try_with_slot_host_pass(
2011                Rc::clone(slots),
2012                crate::slot::SlotPassMode::Compose,
2013                |composer| {
2014                    let output = composer.with_group(root_group_key, |composer| f(composer));
2015                    if root.is_some() {
2016                        composer.pop_parent();
2017                    }
2018                    output
2019                },
2020            )?;
2021            let commands = composer.take_commands();
2022            let side_effects = composer.take_side_effects();
2023            Ok((output, commands, side_effects, outcome.compacted))
2024        })?;
2025        let frame = {
2026            let frame = take_subcompose_frame(&guard.core, "subcompose_slot");
2027            guard.leaked = true;
2028            frame
2029        };
2030
2031        self.flush_subcompose_pass(commands, &runtime_handle, compact_applier, side_effects)?;
2032        Ok((result, frame.scopes))
2033    }
2034
2035    fn attach_root_nodes(&self, root_nodes: RootNodeIds) {
2036        for id in root_nodes {
2037            self.attach_to_parent(id);
2038        }
2039    }
2040
2041    pub fn skip_current_group(&self) {
2042        #[expect(
2043            clippy::redundant_closure_for_method_calls,
2044            reason = "the method path is not general over the session lifetime"
2045        )]
2046        self.with_slot_session_mut(|slots| slots.skip_group());
2047    }
2048
2049    pub fn runtime_handle(&self) -> RuntimeHandle {
2050        self.core.runtime.clone()
2051    }
2052
2053    pub fn set_recompose_callback<F>(&self, callback: F)
2054    where
2055        F: FnMut(&Composer) + 'static,
2056    {
2057        let stateless = (std::mem::size_of::<F>() == 0).then(std::any::TypeId::of::<F>);
2058        if let Some(body) = stateless
2059            && self.current_scope_reruns(body)
2060        {
2061            return;
2062        }
2063        self.set_recompose_callback_boxed(Box::new(callback), stateless);
2064    }
2065
2066    fn current_scope_reruns(&self, body: std::any::TypeId) -> bool {
2067        self.core
2068            .scope_stack
2069            .borrow()
2070            .last()
2071            .is_some_and(|scope| scope.reruns_stateless(&self.core.observer, body))
2072    }
2073
2074    #[inline(never)]
2075    fn set_recompose_callback_boxed(
2076        &self,
2077        callback: Box<dyn FnMut(&Composer)>,
2078        stateless: Option<std::any::TypeId>,
2079    ) {
2080        if let Some(scope) = self.current_recompose_scope() {
2081            scope.set_observed_recompose(self.observer(), callback, stateless);
2082        }
2083    }
2084
2085    pub fn set_recompose_fn(&self, callback: fn(&Composer)) {
2086        if let Some(scope) = self.current_recompose_scope() {
2087            scope.set_recompose_fn(callback);
2088        }
2089    }
2090
2091    /// Runs `f` with `provided` in scope over the locals around it; of values
2092    /// for one local, the last wins.
2093    pub fn with_composition_locals<R>(
2094        &self,
2095        provided: impl IntoIterator<Item = ProvidedValue>,
2096        site: crate::Key,
2097        f: impl FnOnce(&Composer) -> R,
2098    ) -> R {
2099        let provided: SmallVec<[ProvidedValue; 2]> = provided.into_iter().collect();
2100        if provided.is_empty() {
2101            return f(self);
2102        }
2103        let mut values = SmallVec::<[(LocalKey, Rc<dyn Any>); 2]>::new();
2104        for value in provided.into_iter().rev() {
2105            if values.iter().any(|(key, _)| key == value.key()) {
2106                continue;
2107            }
2108            values.push(value.into_entry(self, site));
2109        }
2110        let parent = self.current_local_stack();
2111        *self.local_stack() = Some(Rc::new(LocalFrame {
2112            values,
2113            parent: parent.clone(),
2114        }));
2115        let result = f(self);
2116        *self.local_stack() = parent;
2117        result
2118    }
2119}