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, ApplierGuard, ApplierHost, COMMAND_FLUSH_THRESHOLD, ChildList, Command,
13    CommandQueue, CompositionLocal, DirtyBubble, Key, LocalKey, LocalStackSnapshot,
14    LocalStateEntry, MutableState, Node, NodeError, NodeId, Owned, ProvidedValue, RecomposeOptions,
15    RecomposeScope, RecomposeScopeInner, RecycledNode, RetentionMode, RetentionPolicy,
16    RuntimeHandle, ScopeId, SlotId, SlotPassOutcome, SlotTable, SlotsHost, SnapshotStateList,
17    SnapshotStateMap, 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 retention = self.retention_by_host.borrow_mut().remove(&host_key);
468        let applier_host = self
469            .applier_host
470            .borrow()
471            .as_ref()
472            .and_then(std::rc::Weak::upgrade);
473        if let Some(applier_host) = applier_host {
474            let mut disposal = crate::NodeDisposal::default();
475            if let Some(retention) = retention {
476                for subtree in retention.into_subtrees() {
477                    subtree.scopes().for_each(RecomposeScope::deactivate);
478                    table.invalidate_detached_subtree_anchors(&subtree);
479                    disposal.retain_subtree(subtree);
480                }
481            }
482            applier_host.dispose_nodes(disposal)?;
483        } else if let Some(retention) = retention {
484            self.deactivate_and_queue_subtrees(retention, table, lifecycle);
485        }
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) parent_stack: RefCell<Vec<ParentFrame>>,
600    pub(crate) subcompose_stack: RefCell<Vec<SubcomposeFrame>>,
601    pub(crate) root: Cell<Option<NodeId>>,
602    pub(crate) commands: RefCell<CommandQueue>,
603    pub(crate) scope_stack: RefCell<Vec<RecomposeScope>>,
604    subcomposition_owner_scope: RefCell<Option<RecomposeScope>>,
605    pub(crate) local_stack: RefCell<LocalStackSnapshot>,
606    pub(crate) side_effects: RefCell<Vec<Box<dyn FnOnce()>>>,
607    pub(crate) pending_scope_options: RefCell<Option<RecomposeOptions>>,
608    pub(crate) phase: Cell<crate::Phase>,
609    pub(crate) last_node_reused: Cell<Option<bool>>,
610    pub(crate) recompose_parent_hint: Cell<Option<NodeId>>,
611    pub(crate) recompose_child_cursor: Cell<Option<usize>>,
612    pub(crate) root_render_requested: Cell<bool>,
613    pub(crate) _not_send: PhantomData<*const ()>,
614}
615
616/// The composition context inherited by work that is composed in another slot
617/// host. Besides composition locals, this carries the source owner scope so a
618/// secondary tree cannot outlive the composition that supplied its callbacks.
619#[derive(Clone)]
620pub struct CapturedCompositionContext {
621    locals: LocalStackSnapshot,
622    owner_scope: Option<Weak<RecomposeScopeInner>>,
623}
624
625impl CapturedCompositionContext {
626    /// Total deactivations along the capturing scope's owner chain right now;
627    /// see [`crate::RecomposeScope::owner_chain_deactivation_epoch`]. Zero
628    /// when the context has no owner scope or it is gone.
629    pub fn owner_chain_deactivation_epoch(&self) -> u64 {
630        self.owner_scope
631            .as_ref()
632            .and_then(Weak::upgrade)
633            .map_or(0, |inner| {
634                crate::RecomposeScope { inner }.owner_chain_deactivation_epoch()
635            })
636    }
637}
638
639fn take_subcompose_frame(core: &ComposerCore, operation: &str) -> SubcomposeFrame {
640    match core.subcompose_stack.borrow_mut().pop() {
641        Some(frame) => frame,
642        None => {
643            log::error!("subcompose stack underflow while finishing {operation}");
644            SubcomposeFrame::default()
645        }
646    }
647}
648
649struct SubcomposeStackGuard {
650    core: Rc<ComposerCore>,
651    leaked: bool,
652}
653
654impl Drop for SubcomposeStackGuard {
655    fn drop(&mut self) {
656        if !self.leaked {
657            self.core.subcompose_stack.borrow_mut().pop();
658        }
659    }
660}
661
662impl ComposerCore {
663    pub(crate) fn open_branch_fold(&self, key: Key) -> BranchGroupGuard {
664        let hosts = self.slot_hosts.borrow();
665        let host = hosts.last().unwrap_or(&self.slots);
666        BranchGroupGuard {
667            fold: host
668                .try_push_branch_fold(key)
669                .map(|token| (Rc::clone(host), token)),
670        }
671    }
672
673    pub(crate) fn new(
674        shared_state: Rc<ComposerRuntimeState>,
675        slots: Rc<SlotsHost>,
676        applier: Rc<dyn ApplierHost>,
677        runtime: RuntimeHandle,
678        root: Option<NodeId>,
679        initial_parent_frame: InitialParentFrame,
680    ) -> Self {
681        let parent_stack = if let Some(root_id) = root {
682            vec![ParentFrame {
683                id: root_id,
684                previous: ChildList::new(),
685                new_children: ChildList::new(),
686                new_children_membership: None,
687                attach_mode: ParentAttachMode::DeferredSync,
688                synthetic_root: matches!(initial_parent_frame, InitialParentFrame::SyntheticRoot),
689            }]
690        } else {
691            Vec::new()
692        };
693
694        Self {
695            shared_state,
696            slots,
697            slot_hosts: RefCell::new(Vec::new()),
698            applier,
699            runtime,
700            parent_stack: RefCell::new(parent_stack),
701            subcompose_stack: RefCell::new(Vec::new()),
702            root: Cell::new(root),
703            commands: RefCell::new(CommandQueue::default()),
704            scope_stack: RefCell::new(Vec::new()),
705            subcomposition_owner_scope: RefCell::new(None),
706            local_stack: RefCell::new(None),
707            side_effects: RefCell::new(Vec::new()),
708            pending_scope_options: RefCell::new(None),
709            phase: Cell::new(crate::Phase::Compose),
710            last_node_reused: Cell::new(None),
711            recompose_parent_hint: Cell::new(None),
712            recompose_child_cursor: Cell::new(None),
713            root_render_requested: Cell::new(false),
714            _not_send: PhantomData,
715        }
716    }
717}
718
719#[derive(Clone)]
720pub struct Composer {
721    pub(crate) core: Rc<ComposerCore>,
722}
723
724pub struct BranchGroupGuard {
725    fold: Option<(Rc<SlotsHost>, usize)>,
726}
727
728impl Drop for BranchGroupGuard {
729    fn drop(&mut self) {
730        let Some((host, token)) = &self.fold else {
731            return;
732        };
733        if !host.try_close_branch_fold(*token) {
734            log::error!("a branch fold guard closed while its slot host was busy");
735        }
736    }
737}
738
739pub(crate) enum EmittedNode {
740    Fresh(Box<dyn Node>),
741    Recycled(RecycledNode),
742}
743
744impl Composer {
745    pub(crate) fn new_with_shared_state(
746        shared_state: Rc<ComposerRuntimeState>,
747        slots: Rc<SlotsHost>,
748        applier: Rc<dyn ApplierHost>,
749        runtime: RuntimeHandle,
750        root: Option<NodeId>,
751    ) -> Self {
752        Self::new_with_shared_state_with_parent_frame(
753            shared_state,
754            slots,
755            applier,
756            runtime,
757            root,
758            InitialParentFrame::SyntheticRoot,
759        )
760    }
761
762    fn new_with_shared_state_with_parent_frame(
763        shared_state: Rc<ComposerRuntimeState>,
764        slots: Rc<SlotsHost>,
765        applier: Rc<dyn ApplierHost>,
766        runtime: RuntimeHandle,
767        root: Option<NodeId>,
768        initial_parent_frame: InitialParentFrame,
769    ) -> Self {
770        shared_state.bind_applier_host(&applier);
771        let slots = bind_slots_host_to_runtime_state(&shared_state, &slots);
772        let core = Rc::new(ComposerCore::new(
773            shared_state,
774            slots,
775            applier,
776            runtime,
777            root,
778            initial_parent_frame,
779        ));
780        Self { core }
781    }
782
783    pub fn new(
784        slots: Rc<SlotsHost>,
785        applier: Rc<dyn ApplierHost>,
786        runtime: RuntimeHandle,
787        root: Option<NodeId>,
788    ) -> Self {
789        Self::new_with_shared_state_with_parent_frame(
790            slots
791                .runtime_state()
792                .unwrap_or_else(|| Rc::new(ComposerRuntimeState::default())),
793            slots,
794            applier,
795            runtime,
796            root,
797            InitialParentFrame::RealParent,
798        )
799    }
800
801    pub(crate) fn from_core(core: Rc<ComposerCore>) -> Self {
802        Self { core }
803    }
804
805    pub(crate) fn clone_core(&self) -> Rc<ComposerCore> {
806        Rc::clone(&self.core)
807    }
808
809    pub(crate) fn request_root_render(&self) {
810        self.core.root_render_requested.set(true);
811    }
812
813    pub(crate) fn take_root_render_request(&self) -> bool {
814        self.core.root_render_requested.replace(false)
815    }
816
817    pub fn active_slots_host(&self) -> Rc<SlotsHost> {
818        self.core
819            .slot_hosts
820            .borrow()
821            .last()
822            .cloned()
823            .unwrap_or_else(|| Rc::clone(&self.core.slots))
824    }
825
826    fn with_active_slots_host<R>(&self, f: impl FnOnce(&SlotsHost) -> R) -> R {
827        let hosts = self.core.slot_hosts.borrow();
828        f(hosts.last().unwrap_or(&self.core.slots))
829    }
830
831    pub(crate) fn with_slots<R>(&self, f: impl FnOnce(&SlotTable) -> R) -> R {
832        self.with_active_slots_host(|host| f(&host.borrow()))
833    }
834
835    pub(crate) fn with_slots_mut<R>(&self, f: impl FnOnce(&mut SlotTable) -> R) -> R {
836        self.with_active_slots_host(|host| f(&mut host.borrow_mut()))
837    }
838
839    pub(crate) fn with_slot_session_mut<R>(
840        &self,
841        f: impl FnOnce(&mut crate::slot::SlotWriteSession<'_>) -> R,
842    ) -> R {
843        self.with_active_slots_host(|host| host.with_write_session(f))
844    }
845
846    pub(crate) fn try_with_slot_host_pass<R>(
847        &self,
848        slots: Rc<SlotsHost>,
849        mode: crate::slot::SlotPassMode,
850        f: impl FnOnce(&Composer) -> R,
851    ) -> Result<(R, SlotPassOutcome), NodeError> {
852        let mut guard = self.begin_slot_host_pass(&slots, mode);
853        let result = f(self);
854        let outcome = self.finish_slot_host_pass(&guard.host)?;
855        guard.close();
856        Ok((result, outcome))
857    }
858
859    pub(crate) fn with_slot_host_pass<R>(
860        &self,
861        slots: Rc<SlotsHost>,
862        mode: crate::slot::SlotPassMode,
863        f: impl FnOnce(&Composer) -> R,
864    ) -> (R, SlotPassOutcome) {
865        let mut guard = self.begin_slot_host_pass(&slots, mode);
866        let result = f(self);
867        let outcome = match self.finish_slot_host_pass(&guard.host) {
868            Ok(outcome) => outcome,
869            Err(err) => {
870                log::error!("slot host pass finalization failed: {err}");
871                SlotPassOutcome::default()
872            }
873        };
874        guard.close();
875        (result, outcome)
876    }
877
878    pub(crate) fn with_slot_override<R>(
879        &self,
880        slots: Rc<SlotsHost>,
881        f: impl FnOnce(&Composer) -> R,
882    ) -> (R, SlotPassOutcome) {
883        self.with_slot_host_pass(slots, crate::slot::SlotPassMode::Compose, f)
884    }
885
886    fn begin_slot_host_pass(
887        &self,
888        slots: &Rc<SlotsHost>,
889        mode: crate::slot::SlotPassMode,
890    ) -> SlotHostPassGuard {
891        let slots = bind_slots_host_to_runtime_state(&self.core.shared_state, slots);
892        slots.begin_pass(mode);
893        {
894            let mut stack = self.core.slot_hosts.borrow_mut();
895            if let Some(parent) = stack.last()
896                && !Rc::ptr_eq(parent, &slots)
897            {
898                parent.note_nested_host(&slots);
899            }
900            stack.push(Rc::clone(&slots));
901        }
902        SlotHostPassGuard {
903            core: self.clone_core(),
904            host: slots,
905            active: true,
906        }
907    }
908
909    fn finish_slot_host_pass(&self, slots: &Rc<SlotsHost>) -> Result<SlotPassOutcome, NodeError> {
910        let finished = {
911            let mut applier = self.core.applier.borrow_dyn();
912            slots.finish_pass(&mut *applier)
913        }?;
914        self.handle_detached_children_in_host(slots, None, finished.detached_root_children)?;
915        self.wake_sites_whose_movable_arrived();
916        self.evict_retained_subtrees_for_host(slots)?;
917        slots.complete_pass_cleanup(&finished.outcome);
918        Ok(finished.outcome)
919    }
920
921    fn wake_sites_whose_movable_arrived(&self) {
922        for host in self.core.shared_state.hosts_awaiting_movables() {
923            self.wake_sites_in_host(&host);
924        }
925    }
926
927    fn wake_sites_in_host(&self, slots: &Rc<SlotsHost>) {
928        let pending = self.core.shared_state.take_pending_movables(slots);
929        if pending.is_empty() {
930            return;
931        }
932        let mut waiting = Vec::new();
933        for site in pending {
934            if !slots.borrow().group_is_active(site.placeholder) {
935                continue;
936            }
937            let retain_key = RetainKey::for_group(None, site.key);
938            if !self.core.shared_state.movable_retained_anywhere(retain_key) {
939                waiting.push(site);
940                continue;
941            }
942            match site.parent_scope.as_ref().and_then(RecomposeScope::upgrade) {
943                Some(scope) => {
944                    scope.force_recompose();
945                    scope.invalidate();
946                }
947                None => log::error!(
948                    "movable content {:?} arrived for a site whose scope is gone",
949                    site.key
950                ),
951            }
952        }
953        self.core.shared_state.keep_pending_movables(slots, waiting);
954    }
955
956    pub(crate) fn forget_movables(&self, ids: &[Key]) -> Result<(), NodeError> {
957        for id in ids {
958            let Some((host, subtree)) = self.core.shared_state.take_retained_movable(*id) else {
959                continue;
960            };
961            self.dispose_detached_subtree_in_host(&host, subtree)?;
962            host.flush_pending_drops();
963        }
964        Ok(())
965    }
966
967    pub(crate) fn parent_stack(&self) -> RefMut<'_, Vec<ParentFrame>> {
968        self.core.parent_stack.borrow_mut()
969    }
970
971    pub(crate) fn current_parent_hint(&self) -> Option<NodeId> {
972        let stack = self.core.parent_stack.borrow();
973        let stack_hint = stack
974            .last()
975            .and_then(|frame| (!frame.synthetic_root).then_some(frame.id));
976        stack_hint.or_else(|| self.core.recompose_parent_hint.get())
977    }
978
979    pub(crate) fn subcompose_stack(&self) -> RefMut<'_, Vec<SubcomposeFrame>> {
980        self.core.subcompose_stack.borrow_mut()
981    }
982
983    pub(crate) fn commands_mut(&self) -> RefMut<'_, CommandQueue> {
984        self.core.commands.borrow_mut()
985    }
986
987    pub(crate) fn enqueue_semantics_invalidation(&self, id: NodeId) {
988        self.commands_mut().push(Command::BubbleDirty {
989            node_id: id,
990            bubble: DirtyBubble::SEMANTICS,
991        });
992    }
993
994    pub(crate) fn scope_stack(&self) -> RefMut<'_, Vec<RecomposeScope>> {
995        self.core.scope_stack.borrow_mut()
996    }
997
998    pub(crate) fn local_stack(&self) -> RefMut<'_, LocalStackSnapshot> {
999        self.core.local_stack.borrow_mut()
1000    }
1001
1002    pub(crate) fn current_local_stack(&self) -> LocalStackSnapshot {
1003        self.core.local_stack.borrow().clone()
1004    }
1005
1006    pub(crate) fn side_effects_mut(&self) -> RefMut<'_, Vec<Box<dyn FnOnce()>>> {
1007        self.core.side_effects.borrow_mut()
1008    }
1009
1010    fn pending_scope_options(&self) -> RefMut<'_, Option<RecomposeOptions>> {
1011        self.core.pending_scope_options.borrow_mut()
1012    }
1013
1014    pub(crate) fn borrow_applier(&self) -> ApplierGuard<'_, dyn Applier> {
1015        self.core.applier.borrow_dyn()
1016    }
1017
1018    /// Records nodes whose retained subtrees a reused subcompose slot just
1019    /// rebound to new content.
1020    ///
1021    /// The rebinding recomposes inline during measure, so every repass it
1022    /// schedules is consumed by the layout pass already running — nothing
1023    /// else survives to tell the scoped scene update these subtrees changed,
1024    /// and a translate-only update would keep their stale layers (measured:
1025    /// a 60pt lazy scroll presented the old row's text at the new row's
1026    /// position). The structural-change set is the one channel drained
1027    /// after layout, so the rebound children ride it.
1028    pub fn record_rebound_slot_children(&self, children: &[NodeId]) {
1029        let mut applier = self.borrow_applier();
1030        for &child in children {
1031            applier.record_structural_change(child);
1032        }
1033    }
1034
1035    /// Registers a virtual node in the Applier.
1036    ///
1037    /// Assigns the node's ID so its layout registry and invalidation metadata
1038    /// belong to the node stored in the applier.
1039    pub fn register_virtual_node(
1040        &self,
1041        node_id: NodeId,
1042        node: Box<dyn Node>,
1043    ) -> Result<(), NodeError> {
1044        let mut applier = self.borrow_applier();
1045        applier.insert_with_id(node_id, node)?;
1046        applier.get_mut(node_id)?.set_node_id(node_id);
1047        Ok(())
1048    }
1049
1050    /// Detaches and disposes subcomposition roots evicted from their reuse pool.
1051    /// Applies pending composition commands before releasing the roots and their descendants.
1052    ///
1053    /// ```no_run
1054    /// # use cranpose_core::{Composer, NodeError, SubcomposeState};
1055    /// # fn finish(composer: &Composer, state: &mut SubcomposeState) -> Result<(), NodeError> {
1056    /// let evicted = state.finish_pass();
1057    /// composer.dispose_subcomposed_nodes(evicted)?;
1058    /// # Ok(())
1059    /// # }
1060    /// ```
1061    pub fn dispose_subcomposed_nodes(
1062        &self,
1063        disposal: crate::subcompose::SubcomposeDisposal,
1064    ) -> Result<(), NodeError> {
1065        if disposal.nodes.is_empty() && disposal.slot_hosts.is_empty() {
1066            return Ok(());
1067        }
1068        self.apply_pending_commands()?;
1069        let disposal = crate::NodeDisposal::from_subcomposed(disposal, &*self.borrow_applier());
1070        self.core.applier.dispose_nodes(disposal)
1071    }
1072
1073    /// Checks if a node has no parent (is a root node).
1074    /// Used by SubcomposeMeasureScope to filter subcompose results.
1075    pub fn node_has_no_parent(&self, node_id: NodeId) -> bool {
1076        let mut applier = self.borrow_applier();
1077        match applier.get_mut(node_id) {
1078            Ok(node) => node.parent().is_none(),
1079            Err(_) => true,
1080        }
1081    }
1082
1083    /// Returns a node's parent, or `None` for a root node.
1084    ///
1085    /// Returns an error when the node is no longer present in the applier.
1086    pub fn node_parent(&self, node_id: NodeId) -> Result<Option<NodeId>, NodeError> {
1087        self.borrow_applier()
1088            .get_mut(node_id)
1089            .map(|node| node.parent())
1090    }
1091
1092    /// Gets the children of a node from the Applier.
1093    ///
1094    /// This is used by SubcomposeLayoutNode to get children of virtual nodes
1095    /// directly from the Applier, where insert_child commands have been applied.
1096    pub fn get_node_children(&self, node_id: NodeId) -> SmallVec<[NodeId; 8]> {
1097        let mut applier = self.borrow_applier();
1098        match applier.get_mut(node_id) {
1099            Ok(node) => {
1100                let mut children = SmallVec::<[NodeId; 8]>::new();
1101                node.collect_children_into(&mut children);
1102                children
1103            }
1104            Err(_) => SmallVec::<[NodeId; 8]>::new(),
1105        }
1106    }
1107
1108    /// Whether any node needs measurement or placement before its cached layout can be reused.
1109    ///
1110    /// ```no_run
1111    /// # use cranpose_core::{Composer, NodeId};
1112    /// # fn cache_is_current(composer: &Composer, children: &[NodeId]) -> bool {
1113    /// !composer.nodes_need_relayout(children.iter().copied())
1114    /// # }
1115    /// ```
1116    pub fn nodes_need_relayout(&self, node_ids: impl IntoIterator<Item = NodeId>) -> bool {
1117        let mut applier = self.borrow_applier();
1118        node_ids.into_iter().any(|node_id| {
1119            applier
1120                .get_mut(node_id)
1121                .is_ok_and(|node| node.needs_measure() || node.needs_layout())
1122        })
1123    }
1124
1125    /// Records a child node in the current parent frame's expected children list.
1126    ///
1127    /// Used by SubcomposeLayout's `perform_subcompose` to register virtual nodes
1128    /// with the outer composer's parent frame. This ensures that the `pop_parent`
1129    /// call at the end of `subcompose_slot` generates a correct `SyncChildren`
1130    /// command that preserves (rather than removes) the virtual nodes.
1131    ///
1132    /// Without this, `pop_parent` would generate `SyncChildren { expected: [] }`,
1133    /// which removes all virtual nodes and their subtrees from the applier.
1134    pub fn record_subcompose_child(&self, child_id: NodeId) {
1135        let mut parent_stack = self.parent_stack();
1136        if let Some(frame) = parent_stack.last_mut()
1137            && matches!(frame.attach_mode, ParentAttachMode::DeferredSync)
1138        {
1139            if let Some(membership) = frame.new_children_membership.as_mut() {
1140                if membership.insert(child_id) {
1141                    frame.new_children.push(child_id);
1142                }
1143            } else if frame.new_children.len() >= LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD {
1144                let mut membership = HashSet::default();
1145                membership.reserve(frame.new_children.len() + 1);
1146                membership.extend(frame.new_children.iter().copied());
1147                if membership.insert(child_id) {
1148                    frame.new_children.push(child_id);
1149                }
1150                frame.new_children_membership = Some(membership);
1151            } else if !frame.new_children.contains(&child_id) {
1152                frame.new_children.push(child_id);
1153            }
1154        }
1155    }
1156
1157    /// Clears all children of a node in the Applier.
1158    ///
1159    /// This is used by SubcomposeLayoutNode when reusing a virtual node for
1160    /// different content. Without clearing, old children remain attached,
1161    /// causing duplicate/interleaved items in lazy lists after scrolling.
1162    pub fn clear_node_children(&self, node_id: NodeId) {
1163        let mut applier = self.borrow_applier();
1164        if let Ok(node) = applier.get_mut(node_id) {
1165            node.update_children(&[]);
1166        }
1167    }
1168
1169    pub fn install<R>(&self, f: impl FnOnce(&Composer) -> R) -> R {
1170        let _composer_guard = composer_context::enter(self);
1171        runtime::push_active_runtime(&self.core.runtime);
1172        struct Guard;
1173        impl Drop for Guard {
1174            fn drop(&mut self) {
1175                runtime::pop_active_runtime();
1176            }
1177        }
1178        let guard = Guard;
1179        let result = f(self);
1180        drop(guard);
1181        result
1182    }
1183
1184    pub(crate) fn flush_pending_commands_if_large(&self) -> Result<(), NodeError> {
1185        let queued = self.core.commands.borrow().len();
1186        if queued < COMMAND_FLUSH_THRESHOLD {
1187            return Ok(());
1188        }
1189        self.apply_pending_commands()
1190    }
1191
1192    fn resolve_group_entry(
1193        &self,
1194        host: &Rc<SlotsHost>,
1195        seed: crate::slot::GroupKeySeed,
1196        parent_scope_id: Option<ScopeId>,
1197    ) -> GroupEntry {
1198        let key = host.with_write_session(|slots| slots.reserve_group_key(seed));
1199        let retain_key = RetainKey::for_group(parent_scope_id, key);
1200        let restored = self
1201            .core
1202            .shared_state
1203            .take_retained(host, retain_key, |subtree| {
1204                host.with_write_session(|slots| slots.retained_restore_ready(key, subtree))
1205            })
1206            .or_else(|| self.take_movable_from_another_table(host, retain_key, key));
1207        if restored.is_some() || !key.is_movable() {
1208            return GroupEntry {
1209                key,
1210                restored,
1211                placeholder_for: None,
1212            };
1213        }
1214        let attached_elsewhere = self.movable_attached_elsewhere(host, key);
1215        if !attached_elsewhere {
1216            return GroupEntry {
1217                key,
1218                restored: None,
1219                placeholder_for: None,
1220            };
1221        }
1222        let id = key.explicit_key.unwrap_or_default();
1223        let placeholder = host.with_write_session(|slots| {
1224            slots.reserve_group_key(crate::slot::GroupKeySeed::movable_placeholder(id))
1225        });
1226        GroupEntry {
1227            key: placeholder,
1228            restored: None,
1229            placeholder_for: Some(key),
1230        }
1231    }
1232
1233    /// Movable content that was let go by a parent composed in another slot
1234    /// table — a `SubcomposeLayout`'s, or the one a subcomposition owns — is
1235    /// retained there, not here. Bring it over: the table that held it lets
1236    /// go of its anchors, and this one issues its own.
1237    fn take_movable_from_another_table(
1238        &self,
1239        host: &Rc<SlotsHost>,
1240        retain_key: RetainKey,
1241        key: crate::slot::GroupKey,
1242    ) -> Option<crate::slot::DetachedSubtree> {
1243        if !key.is_movable() {
1244            return None;
1245        }
1246        let (source, mut subtree) = self
1247            .core
1248            .shared_state
1249            .take_retained_movable_elsewhere(host, retain_key)?;
1250        source
1251            .borrow_mut()
1252            .invalidate_detached_subtree_anchors(&subtree);
1253        if host.with_write_session(|slots| slots.retained_restore_ready(key, &mut subtree)) {
1254            return Some(subtree);
1255        }
1256        log::error!(
1257            "movable content {key:?} could not be taken over by the slot table that asked for it"
1258        );
1259        if let Err(error) = self.dispose_detached_subtree_in_host(host, subtree) {
1260            log::error!("disposing movable content that could not move failed: {error}");
1261        }
1262        None
1263    }
1264
1265    /// Whether the movable's content is attached to some parent that is not
1266    /// the one composing, in this slot table or in another.
1267    fn movable_attached_elsewhere(&self, host: &Rc<SlotsHost>, key: crate::slot::GroupKey) -> bool {
1268        if host.with_write_session(|slots| slots.movable_attached_elsewhere(key)) {
1269            return true;
1270        }
1271        let Some(id) = key.movable_id() else {
1272            return false;
1273        };
1274        self.core
1275            .shared_state
1276            .host_holding_movable(id)
1277            .is_some_and(|holder| !Rc::ptr_eq(&holder, host))
1278    }
1279
1280    fn scope_for_started_group(
1281        &self,
1282        host: &SlotsHost,
1283        group: crate::slot::ActiveGroupId,
1284        scope: Option<RecomposeScope>,
1285    ) -> RecomposeScope {
1286        if let Some(scope) = scope {
1287            return scope;
1288        }
1289        let scope = RecomposeScope::new(self.runtime_handle());
1290        host.with_write_session(|slots| slots.set_group_scope(group, scope.clone()));
1291        scope
1292    }
1293
1294    fn enter_group_scope(&self, scope_ref: &RecomposeScope, entry: GroupScopeEntry<'_>) {
1295        let GroupScopeEntry {
1296            options,
1297            start_kind,
1298            host,
1299            group,
1300        } = entry;
1301        // A group entered while its scope is inactive is reused content, as
1302        // when a lazy list gives a recycled row to another item: like Compose,
1303        // it composes again rather than skipping, which reactivates the
1304        // scopes beneath it.
1305        let reused = !scope_ref.is_active() && scope_ref.has_composed_once();
1306        scope_ref.reactivate();
1307        {
1308            let mut stack = self.scope_stack();
1309            let parent_scope = stack.last();
1310            scope_ref.set_parent_scope(parent_scope);
1311            if parent_scope.is_none() {
1312                scope_ref.set_lifetime_owner_scope(
1313                    self.core.subcomposition_owner_scope.borrow().as_ref(),
1314                );
1315            } else {
1316                scope_ref.set_lifetime_owner_scope(None);
1317            }
1318            stack.push(scope_ref.clone());
1319        }
1320        scope_ref.set_retention_mode(options.retention);
1321
1322        if options.force_recompose {
1323            scope_ref.force_recompose();
1324        } else if options.force_reuse {
1325            scope_ref.force_reuse();
1326        } else if reused {
1327            scope_ref.force_recompose();
1328        }
1329        let restored = matches!(start_kind, GroupStartKind::Restored);
1330        if restored {
1331            scope_ref.force_recompose();
1332        }
1333
1334        scope_ref.set_slots_host(host);
1335
1336        {
1337            let mut stack = self.subcompose_stack();
1338            if let Some(frame) = stack.last_mut() {
1339                frame.scopes.push(scope_ref.clone());
1340            }
1341        }
1342
1343        scope_ref.snapshot_locals(&self.core.local_stack.borrow());
1344        let parent_hint = self.current_parent_hint();
1345        if restored {
1346            reparent_restored_scopes(host, group, scope_ref.parent_hint(), parent_hint);
1347        }
1348        scope_ref.set_parent_hint(parent_hint);
1349    }
1350
1351    #[inline(never)]
1352    fn with_group_in_active_pass_dyn(
1353        &self,
1354        host: &Rc<SlotsHost>,
1355        key: crate::slot::GroupKeySeed,
1356        f: &mut dyn FnMut(&Composer),
1357    ) {
1358        struct GroupGuard<'a> {
1359            composer: &'a Composer,
1360            host: &'a Rc<SlotsHost>,
1361            scope: RecomposeScope,
1362        }
1363
1364        impl Drop for GroupGuard<'_> {
1365            fn drop(&mut self) {
1366                self.composer.close_group_body(self.host, &self.scope);
1367                self.scope.mark_recomposed();
1368                #[expect(
1369                    clippy::redundant_closure_for_method_calls,
1370                    reason = "the method path is not general over the session lifetime"
1371                )]
1372                self.host.with_write_session(|slots| slots.end_group());
1373                if let Err(err) = self.composer.flush_pending_commands_if_large() {
1374                    log::error!("mid-composition command flush failed: {err}");
1375                }
1376            }
1377        }
1378
1379        let options = self.pending_scope_options().take().unwrap_or_default();
1380        let parent_scope_id = self
1381            .core
1382            .scope_stack
1383            .borrow()
1384            .last()
1385            .map(RecomposeScope::id);
1386        let GroupEntry {
1387            key: reserved_key,
1388            restored,
1389            placeholder_for,
1390        } = self.resolve_group_entry(host, key, parent_scope_id);
1391        let parent_node = self.current_parent_hint();
1392        let GroupStart {
1393            group,
1394            anchor,
1395            scope,
1396            kind,
1397        } = host.with_write_session(|slots| slots.begin_group(reserved_key, restored, parent_node));
1398        let scope_ref = self.scope_for_started_group(host, group, scope);
1399        if let Some(movable_key) = placeholder_for {
1400            self.core.shared_state.record_pending_movable(
1401                host,
1402                PendingMovable {
1403                    key: movable_key,
1404                    placeholder: anchor,
1405                    parent_scope: self
1406                        .core
1407                        .scope_stack
1408                        .borrow()
1409                        .last()
1410                        .map(RecomposeScope::downgrade),
1411                },
1412            );
1413        }
1414        self.enter_group_scope(
1415            &scope_ref,
1416            GroupScopeEntry {
1417                options,
1418                start_kind: kind,
1419                host,
1420                group,
1421            },
1422        );
1423
1424        let guard = GroupGuard {
1425            composer: self,
1426            host,
1427            scope: scope_ref,
1428        };
1429        if placeholder_for.is_none() {
1430            f(self);
1431        }
1432        guard.scope.mark_composed_once();
1433        drop(guard);
1434    }
1435
1436    fn with_group_seed_dyn(&self, key: crate::slot::GroupKeySeed, f: &mut dyn FnMut(&Composer)) {
1437        let host = self.active_slots_host();
1438        if host.has_active_pass() {
1439            self.with_group_in_active_pass_dyn(&host, key, f);
1440            return;
1441        }
1442        self.with_slot_host_pass(host, crate::slot::SlotPassMode::Compose, |composer| {
1443            composer.with_group_in_active_pass_dyn(&composer.active_slots_host(), key, f);
1444        });
1445    }
1446
1447    pub(crate) fn with_group_seed<R>(
1448        &self,
1449        key: crate::slot::GroupKeySeed,
1450        f: impl FnOnce(&Composer) -> R,
1451    ) -> R {
1452        let mut f = Some(f);
1453        let mut result = None;
1454        self.with_group_seed_dyn(key, &mut |composer| {
1455            let f = f.take().expect("group body must run at most once");
1456            result = Some(f(composer));
1457        });
1458        result.expect("group body must run exactly once")
1459    }
1460
1461    pub(crate) fn with_movable_group(&self, id: Key, f: impl FnOnce(&Composer)) {
1462        let mut f = Some(f);
1463        self.with_group_seed_dyn(crate::slot::GroupKeySeed::movable(id), &mut |composer| {
1464            if let Some(f) = f.take() {
1465                f(composer);
1466            }
1467        });
1468    }
1469
1470    pub fn with_group<R>(&self, key: Key, f: impl FnOnce(&Composer) -> R) -> R {
1471        self.with_group_seed(crate::slot::GroupKeySeed::unkeyed(key), f)
1472    }
1473
1474    pub fn cranpose_with_reuse<R>(
1475        &self,
1476        key: Key,
1477        mut options: RecomposeOptions,
1478        f: impl FnOnce(&Composer) -> R,
1479    ) -> R {
1480        options.retention = RetentionMode::RetainWhenInactive;
1481        self.pending_scope_options().replace(options);
1482        self.with_group(key, f)
1483    }
1484
1485    #[track_caller]
1486    pub fn with_key<K: Hash, R>(&self, key: &K, f: impl FnOnce(&Composer) -> R) -> R {
1487        let seed = explicit_group_key_seed(key, std::panic::Location::caller());
1488        self.with_group_seed(seed, f)
1489    }
1490
1491    #[doc(hidden)]
1492    pub fn __branch_group_deferred(&self, key: Key) -> BranchGroupGuard {
1493        self.core.open_branch_fold(key)
1494    }
1495
1496    fn dispose_detached_nodes(&self, nodes: impl IntoIterator<Item = NodeId>) {
1497        for node_id in nodes {
1498            self.commands_mut().push(Command::callback(move |applier| {
1499                crate::slot::dispose_detached_node_now(applier, node_id)
1500            }));
1501        }
1502    }
1503
1504    fn detached_root_parent_commands(
1505        &self,
1506        subtree: &crate::slot::DetachedSubtree,
1507        context: &'static str,
1508    ) -> Result<Vec<(NodeId, Option<NodeId>)>, NodeError> {
1509        let mut root_nodes = Vec::new();
1510        subtree.collect_root_nodes_checked_into(&mut root_nodes, context);
1511        let mut roots = Vec::with_capacity(root_nodes.len());
1512        for root in root_nodes {
1513            let parent_id = {
1514                let mut applier = self.borrow_applier();
1515                applier.get_mut(root)?.parent()
1516            };
1517            roots.push((root, parent_id));
1518        }
1519        Ok(roots)
1520    }
1521
1522    fn retain_detached_subtree_in_host(
1523        &self,
1524        slots_host: &Rc<SlotsHost>,
1525        parent_scope: Option<ScopeId>,
1526        subtree: crate::slot::DetachedSubtree,
1527    ) -> Result<(), NodeError> {
1528        let Some(root_key) = subtree.root_key_checked() else {
1529            log::error!("retention rejected detached subtree without a root group");
1530            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1531            return Ok(());
1532        };
1533        let root_detaches = self.detached_root_parent_commands(&subtree, "retention")?;
1534        subtree.scopes().for_each(RecomposeScope::deactivate);
1535        for (root, parent_id) in root_detaches {
1536            if let Some(parent_id) = parent_id {
1537                self.commands_mut().push(Command::DetachChild {
1538                    parent_id,
1539                    child_id: root,
1540                });
1541            }
1542        }
1543        let evicted = self.core.shared_state.insert_retained(
1544            slots_host,
1545            RetainKey::for_group(parent_scope, root_key),
1546            subtree,
1547            root_key.is_movable(),
1548        );
1549        for subtree in evicted {
1550            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1551        }
1552        Ok(())
1553    }
1554
1555    fn evict_retained_subtrees_for_host(
1556        &self,
1557        slots_host: &Rc<SlotsHost>,
1558    ) -> Result<(), NodeError> {
1559        let evicted = self.core.shared_state.advance_retention_pass(slots_host);
1560        for subtree in evicted {
1561            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1562        }
1563        Ok(())
1564    }
1565
1566    fn dispose_detached_subtree_in_host(
1567        &self,
1568        slots_host: &Rc<SlotsHost>,
1569        subtree: crate::slot::DetachedSubtree,
1570    ) -> Result<(), NodeError> {
1571        let root_nodes = self
1572            .detached_root_parent_commands(&subtree, "disposal")?
1573            .into_iter()
1574            .map(|(root, _)| root);
1575        subtree.scopes().for_each(RecomposeScope::deactivate);
1576        self.dispose_detached_nodes(root_nodes);
1577        slots_host.with_table_and_lifecycle_mut(|table, lifecycle| {
1578            table.invalidate_detached_subtree_anchors(&subtree);
1579            lifecycle.queue_subtree_disposal(subtree);
1580        });
1581        Ok(())
1582    }
1583
1584    fn handle_detached_children_in_host(
1585        &self,
1586        slots_host: &Rc<SlotsHost>,
1587        parent_scope: Option<ScopeId>,
1588        detached: Vec<crate::slot::DetachedSubtree>,
1589    ) -> Result<(), NodeError> {
1590        for mut subtree in detached {
1591            for movable in subtree.split_off_nested_movables() {
1592                self.retain_detached_subtree_in_host(slots_host, None, movable)?;
1593            }
1594            if subtree
1595                .root_key_checked()
1596                .is_some_and(crate::slot::GroupKey::is_movable)
1597            {
1598                self.retain_detached_subtree_in_host(slots_host, None, subtree)?;
1599                continue;
1600            }
1601            let retention_mode = subtree
1602                .root_scope()
1603                .map(RecomposeScope::retention_mode)
1604                .unwrap_or_default();
1605            match retention_mode {
1606                RetentionMode::DisposeWhenInactive => {
1607                    self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1608                }
1609                RetentionMode::RetainWhenInactive => {
1610                    self.retain_detached_subtree_in_host(slots_host, parent_scope, subtree)?;
1611                }
1612            }
1613        }
1614        Ok(())
1615    }
1616
1617    fn handle_finished_group_result(
1618        &self,
1619        host: &Rc<SlotsHost>,
1620        parent_scope: Option<ScopeId>,
1621        result: FinishGroupResult,
1622    ) {
1623        let FinishGroupResult {
1624            detached_children,
1625            direct_nodes,
1626            root_nodes,
1627            was_skipped,
1628        } = result;
1629        if was_skipped {
1630            self.attach_root_nodes(root_nodes);
1631        }
1632        self.dispose_detached_nodes(direct_nodes);
1633        if detached_children.is_empty() {
1634            return;
1635        }
1636        if let Err(err) =
1637            self.handle_detached_children_in_host(host, parent_scope, detached_children)
1638        {
1639            log::error!("detached subtree handling failed while closing a group: {err}");
1640        }
1641    }
1642
1643    pub(crate) fn close_current_group_body_for_scope(&self, scope: &RecomposeScope) {
1644        self.close_group_body(&self.active_slots_host(), scope);
1645    }
1646
1647    fn close_group_body(&self, host: &Rc<SlotsHost>, scope: &RecomposeScope) {
1648        #[expect(
1649            clippy::redundant_closure_for_method_calls,
1650            reason = "the method path is not general over the session lifetime"
1651        )]
1652        let result = host.with_write_session(|slots| slots.finish_group_body());
1653        self.handle_finished_group_result(host, Some(scope.id()), result);
1654        if let Some(popped) = self.scope_stack().pop() {
1655            debug_assert_eq!(
1656                popped.id(),
1657                scope.id(),
1658                "closed scope must match the active scope stack"
1659            );
1660        } else {
1661            log::error!("scope stack underflow while closing scope {}", scope.id());
1662        }
1663    }
1664
1665    #[track_caller]
1666    pub fn remember<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1667        self.remember_at(crate::caller_location_key(), init)
1668    }
1669
1670    #[doc(hidden)]
1671    pub fn remember_at<T: 'static>(
1672        &self,
1673        source: crate::Key,
1674        init: impl FnOnce() -> T,
1675    ) -> Owned<T> {
1676        self.with_slot_session_mut(|slots| {
1677            slots.remember_with_kind(PayloadKind::Remember, source, init)
1678        })
1679    }
1680
1681    #[track_caller]
1682    pub(crate) fn remember_internal<T: 'static>(
1683        &self,
1684        source_salt: crate::Key,
1685        init: impl FnOnce() -> T,
1686    ) -> Owned<T> {
1687        let source = crate::caller_location_key() ^ source_salt;
1688        self.with_slot_session_mut(|slots| {
1689            slots.remember_with_kind(PayloadKind::Internal, source, init)
1690        })
1691    }
1692
1693    #[track_caller]
1694    pub(crate) fn remember_effect<T: Default + 'static>(&self) -> Owned<T> {
1695        let source = crate::caller_location_key();
1696        self.with_slot_session_mut(|slots| slots.remember_effect::<T>(source))
1697    }
1698
1699    #[track_caller]
1700    pub fn use_value_slot<T: 'static>(&self, init: impl FnOnce() -> T) -> ValueSlotHandle<'_, T> {
1701        let source = crate::caller_location_key();
1702        let slot = self.with_slot_session_mut(|slots| {
1703            slots.value_slot_with_kind(PayloadKind::Internal, source, init)
1704        });
1705        ValueSlotHandle::new(slot)
1706    }
1707
1708    #[doc(hidden)]
1709    #[track_caller]
1710    pub fn __use_param_slot<T: 'static>(&self, init: impl FnOnce() -> T) -> ValueSlotHandle<'_, T> {
1711        let source = crate::caller_location_key();
1712        let slot = self.with_slot_session_mut(|slots| {
1713            slots.value_slot_with_kind(PayloadKind::Param, source, init)
1714        });
1715        ValueSlotHandle::new(slot)
1716    }
1717
1718    #[doc(hidden)]
1719    #[track_caller]
1720    pub fn __use_return_slot<T: 'static>(
1721        &self,
1722        init: impl FnOnce() -> T,
1723    ) -> ValueSlotHandle<'_, T> {
1724        let source = crate::caller_location_key();
1725        let slot = self.with_slot_session_mut(|slots| {
1726            slots.value_slot_with_kind(PayloadKind::Return, source, init)
1727        });
1728        ValueSlotHandle::new(slot)
1729    }
1730
1731    #[doc(hidden)]
1732    pub fn __invalidate_return_consumer_scope(&self) {
1733        let Some(scope) = self.current_recompose_scope() else {
1734            self.request_root_render();
1735            return;
1736        };
1737
1738        if let Some(target) = scope.callback_promotion_target() {
1739            target.invalidate();
1740        } else {
1741            self.request_root_render();
1742        }
1743    }
1744
1745    pub fn with_slot_value<'pass, T: 'static, R>(
1746        &'pass self,
1747        handle: ValueSlotHandle<'pass, T>,
1748        f: impl FnOnce(&T) -> R,
1749    ) -> R {
1750        self.with_slots(|slots| f(slots.read_value(handle.slot())))
1751    }
1752
1753    pub fn with_slot_value_mut<'pass, T: 'static, R>(
1754        &'pass self,
1755        handle: ValueSlotHandle<'pass, T>,
1756        f: impl FnOnce(&mut T) -> R,
1757    ) -> R {
1758        self.with_slots_mut(|slots| f(slots.read_value_mut(handle.slot())))
1759    }
1760
1761    pub fn mutable_state_of<T: Clone + 'static>(&self, initial: T) -> MutableState<T> {
1762        MutableState::with_runtime(initial, self.runtime_handle())
1763    }
1764
1765    pub fn mutable_state_list_of<T, I>(&self, values: I) -> SnapshotStateList<T>
1766    where
1767        T: Clone + 'static,
1768        I: IntoIterator<Item = T>,
1769    {
1770        SnapshotStateList::with_runtime(values, self.runtime_handle())
1771    }
1772
1773    pub fn mutable_state_map_of<K, V, I>(&self, pairs: I) -> SnapshotStateMap<K, V>
1774    where
1775        K: Clone + Eq + Hash + 'static,
1776        V: Clone + 'static,
1777        I: IntoIterator<Item = (K, V)>,
1778    {
1779        SnapshotStateMap::with_runtime(pairs, self.runtime_handle())
1780    }
1781
1782    pub fn read_composition_local<T: Clone + 'static>(&self, local: &CompositionLocal<T>) -> T {
1783        self.provided_local::<LocalStateEntry<T>>(&local.key)
1784            .map_or_else(|| local.default_value(), |entry| entry.value())
1785    }
1786
1787    pub fn read_static_composition_local<T: Clone + 'static>(
1788        &self,
1789        local: &StaticCompositionLocal<T>,
1790    ) -> T {
1791        self.provided_local::<StaticLocalEntry<T>>(&local.key)
1792            .map_or_else(|| local.default_value(), |entry| entry.value())
1793    }
1794
1795    /// The innermost provided entry for `key`, when it is an `E`.
1796    fn provided_local<E: 'static>(&self, key: &LocalKey) -> Option<Rc<E>> {
1797        let entry = provided_entry(&self.core.local_stack.borrow(), key)?;
1798        entry
1799            .downcast::<E>()
1800            .inspect_err(|_| log::error!("composition local entry type mismatch for key {key:?}"))
1801            .ok()
1802    }
1803
1804    pub fn current_recompose_scope(&self) -> Option<RecomposeScope> {
1805        self.core.scope_stack.borrow().last().cloned()
1806    }
1807
1808    pub(crate) fn current_state_invalidation_scope(&self) -> Option<RecomposeScope> {
1809        let stack = self.core.scope_stack.borrow();
1810        stack
1811            .iter()
1812            .rev()
1813            .find(|scope| scope.has_recompose_callback())
1814            .cloned()
1815            .or_else(|| stack.last().cloned())
1816    }
1817
1818    pub fn phase(&self) -> crate::Phase {
1819        self.core.phase.get()
1820    }
1821
1822    pub(crate) fn set_phase(&self, phase: crate::Phase) {
1823        self.core.phase.set(phase);
1824    }
1825
1826    pub fn enter_phase(&self, phase: crate::Phase) {
1827        self.set_phase(phase);
1828    }
1829
1830    pub(crate) fn subcompose<R>(
1831        &self,
1832        state: &mut SubcomposeState,
1833        slot_id: SlotId,
1834        content: impl FnOnce(&Composer) -> R,
1835    ) -> (R, Vec<NodeId>) {
1836        match self.phase() {
1837            crate::Phase::Measure | crate::Phase::Layout => {}
1838            current => panic!(
1839                "subcompose() may only be called during measure or layout; current phase: {current:?}"
1840            ),
1841        }
1842
1843        self.subcompose_stack().push(SubcomposeFrame::default());
1844        let mut guard = SubcomposeStackGuard {
1845            core: self.clone_core(),
1846            leaked: false,
1847        };
1848
1849        let slot_host = state.get_or_create_slots(slot_id);
1850        let (result, _) = self.with_slot_override(slot_host, |composer| {
1851            composer.with_group(slot_id.raw(), |composer| content(composer))
1852        });
1853
1854        let frame = {
1855            let frame = take_subcompose_frame(&guard.core, "subcompose");
1856            guard.leaked = true;
1857            frame
1858        };
1859        let nodes = frame.nodes;
1860        let scopes = frame.scopes;
1861        state.register_active(slot_id, &nodes, &scopes);
1862        (result, nodes)
1863    }
1864
1865    pub fn subcompose_measurement<R>(
1866        &self,
1867        state: &mut SubcomposeState,
1868        slot_id: SlotId,
1869        content: impl FnOnce(&Composer) -> R,
1870    ) -> (R, Vec<NodeId>) {
1871        let (result, nodes) = self.subcompose(state, slot_id, content);
1872        let roots = nodes
1873            .into_iter()
1874            .filter(|&id| self.node_has_no_parent(id))
1875            .collect();
1876
1877        (result, roots)
1878    }
1879
1880    fn spin_up_subcompose_core(
1881        &self,
1882        slots: &Rc<SlotsHost>,
1883        root: Option<NodeId>,
1884        runtime_handle: &RuntimeHandle,
1885        locals: LocalStackSnapshot,
1886    ) -> Rc<ComposerCore> {
1887        let phase = self.phase();
1888        let shared_state = slots
1889            .runtime_state()
1890            .unwrap_or_else(|| Rc::clone(&self.core.shared_state));
1891        let core = Rc::new(ComposerCore::new(
1892            shared_state,
1893            Rc::clone(slots),
1894            Rc::clone(&self.core.applier),
1895            runtime_handle.clone(),
1896            root,
1897            InitialParentFrame::RealParent,
1898        ));
1899        core.phase.set(phase);
1900        *core.local_stack.borrow_mut() = locals;
1901        core
1902    }
1903
1904    fn flush_subcompose_pass(
1905        &self,
1906        commands: CommandQueue,
1907        runtime_handle: &RuntimeHandle,
1908        compact_applier: bool,
1909        side_effects: Vec<Box<dyn FnOnce()>>,
1910    ) -> Result<(), NodeError> {
1911        {
1912            let mut applier = self.borrow_applier();
1913            commands.apply(&mut *applier)?;
1914            for update in runtime_handle.take_updates() {
1915                update.apply(&mut *applier)?;
1916            }
1917        }
1918        if compact_applier {
1919            self.core.applier.compact();
1920            self.core.applier.borrow_dyn().clear_recycled_nodes();
1921        }
1922        composer_context::without_composer(|| {
1923            for effect in side_effects {
1924                effect();
1925            }
1926        });
1927        Ok(())
1928    }
1929
1930    pub fn subcompose_in<R>(
1931        &self,
1932        slots: &Rc<SlotsHost>,
1933        root: Option<NodeId>,
1934        f: impl FnOnce(&Composer) -> R,
1935    ) -> Result<R, NodeError> {
1936        let runtime_handle = self.runtime_handle();
1937        let locals = self.current_local_stack();
1938        let core = self.spin_up_subcompose_core(slots, root, &runtime_handle, locals);
1939        let composer = Composer::from_core(core);
1940        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
1941            let (output, outcome) = composer.try_with_slot_host_pass(
1942                Rc::clone(slots),
1943                crate::slot::SlotPassMode::Compose,
1944                |composer| f(composer),
1945            )?;
1946            let commands = composer.take_commands();
1947            let side_effects = composer.take_side_effects();
1948            Ok((output, commands, side_effects, outcome.compacted))
1949        })?;
1950        self.flush_subcompose_pass(commands, &runtime_handle, compact_applier, side_effects)?;
1951        Ok(result)
1952    }
1953
1954    /// Captures the composition context at the current point so work composed
1955    /// in another slot host inherits both locals and source ownership.
1956    ///
1957    /// A `SubcomposeLayout` captures this while it is being composed and replays
1958    /// it while subcomposing off the measure pass, so content that is
1959    /// subcomposed during layout observes the same composition locals as the
1960    /// `SubcomposeLayout` call site — matching Jetpack Compose, where a
1961    /// subcomposition inherits the composition locals of the layout that
1962    /// created it rather than whatever happens to be in scope during measure
1963    /// (which, after composition unwinds, no longer carries ancestor providers).
1964    pub fn capture_composition_context(&self) -> CapturedCompositionContext {
1965        CapturedCompositionContext {
1966            locals: self.current_local_stack(),
1967            owner_scope: self
1968                .current_recompose_scope()
1969                .map(|scope| scope.downgrade()),
1970        }
1971    }
1972
1973    /// Subcomposes content using an isolated SlotsHost without resetting it.
1974    /// Unlike `subcompose_in`, this preserves existing slot state across calls,
1975    /// allowing efficient reuse during measurement passes. This is critical for
1976    /// lazy lists where items need stable slot positions.
1977    pub fn subcompose_slot<R>(
1978        &self,
1979        slots: &Rc<SlotsHost>,
1980        root: Option<NodeId>,
1981        f: impl FnOnce(&Composer) -> R,
1982    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
1983        let context = self.capture_composition_context();
1984        self.subcompose_slot_with_context(slots, root, &context, f)
1985    }
1986
1987    /// Like [`Composer::subcompose_slot`], but uses a context captured at the
1988    /// source composition site. This is required for measure-time composition,
1989    /// where the source scope is no longer on the active stack.
1990    pub fn subcompose_slot_with_context<R>(
1991        &self,
1992        slots: &Rc<SlotsHost>,
1993        root: Option<NodeId>,
1994        context: &CapturedCompositionContext,
1995        f: impl FnOnce(&Composer) -> R,
1996    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
1997        let runtime_handle = self.runtime_handle();
1998        let locals = context.locals.clone();
1999        let core = self.spin_up_subcompose_core(slots, root, &runtime_handle, locals);
2000        *core.subcomposition_owner_scope.borrow_mut() = context
2001            .owner_scope
2002            .as_ref()
2003            .and_then(RecomposeScope::upgrade);
2004        let composer = Composer::from_core(core);
2005        composer.subcompose_stack().push(SubcomposeFrame::default());
2006        let mut guard = SubcomposeStackGuard {
2007            core: composer.clone_core(),
2008            leaked: false,
2009        };
2010        let root_group_key = crate::location_key(file!(), line!(), column!());
2011        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
2012            let (output, outcome) = composer.try_with_slot_host_pass(
2013                Rc::clone(slots),
2014                crate::slot::SlotPassMode::Compose,
2015                |composer| {
2016                    let output = composer.with_group(root_group_key, |composer| f(composer));
2017                    if root.is_some() {
2018                        composer.pop_parent();
2019                    }
2020                    output
2021                },
2022            )?;
2023            let commands = composer.take_commands();
2024            let side_effects = composer.take_side_effects();
2025            Ok((output, commands, side_effects, outcome.compacted))
2026        })?;
2027        let frame = {
2028            let frame = take_subcompose_frame(&guard.core, "subcompose_slot");
2029            guard.leaked = true;
2030            frame
2031        };
2032
2033        self.flush_subcompose_pass(commands, &runtime_handle, compact_applier, side_effects)?;
2034        Ok((result, frame.scopes))
2035    }
2036
2037    fn attach_root_nodes(&self, root_nodes: RootNodeIds) {
2038        for id in root_nodes {
2039            self.attach_to_parent(id);
2040        }
2041    }
2042
2043    pub fn skip_current_group(&self) {
2044        #[expect(
2045            clippy::redundant_closure_for_method_calls,
2046            reason = "the method path is not general over the session lifetime"
2047        )]
2048        self.with_slot_session_mut(|slots| slots.skip_group());
2049    }
2050
2051    pub fn runtime_handle(&self) -> RuntimeHandle {
2052        self.core.runtime.clone()
2053    }
2054
2055    pub fn set_recompose_callback<F>(&self, callback: F)
2056    where
2057        F: FnMut(&Composer) + 'static,
2058    {
2059        let stateless = (std::mem::size_of::<F>() == 0).then(std::any::TypeId::of::<F>);
2060        if let Some(body) = stateless
2061            && self.current_scope_reruns(body)
2062        {
2063            return;
2064        }
2065        self.set_recompose_callback_boxed(Box::new(callback), stateless);
2066    }
2067
2068    fn current_scope_reruns(&self, body: std::any::TypeId) -> bool {
2069        self.core
2070            .scope_stack
2071            .borrow()
2072            .last()
2073            .is_some_and(|scope| scope.reruns_stateless(body))
2074    }
2075
2076    #[inline(never)]
2077    fn set_recompose_callback_boxed(
2078        &self,
2079        callback: Box<dyn FnMut(&Composer)>,
2080        stateless: Option<std::any::TypeId>,
2081    ) {
2082        if let Some(scope) = self.current_recompose_scope() {
2083            scope.set_boxed_recompose(callback, stateless);
2084        }
2085    }
2086
2087    pub fn set_recompose_fn(&self, callback: fn(&Composer)) {
2088        if let Some(scope) = self.current_recompose_scope() {
2089            scope.set_recompose_fn(callback);
2090        }
2091    }
2092
2093    /// Runs `f` with `provided` in scope over the locals around it; of values
2094    /// for one local, the last wins.
2095    pub fn with_composition_locals<R>(
2096        &self,
2097        provided: impl IntoIterator<Item = ProvidedValue>,
2098        site: crate::Key,
2099        f: impl FnOnce(&Composer) -> R,
2100    ) -> R {
2101        let provided: SmallVec<[ProvidedValue; 2]> = provided.into_iter().collect();
2102        if provided.is_empty() {
2103            return f(self);
2104        }
2105        let mut values = SmallVec::<[(LocalKey, Rc<dyn Any>); 2]>::new();
2106        for value in provided.into_iter().rev() {
2107            if values.iter().any(|(key, _)| key == value.key()) {
2108                continue;
2109            }
2110            values.push(value.into_entry(self, site));
2111        }
2112        let parent = self.current_local_stack();
2113        *self.local_stack() = Some(Rc::new(LocalFrame {
2114            values,
2115            parent: parent.clone(),
2116        }));
2117        let result = f(self);
2118        *self.local_stack() = parent;
2119        result
2120    }
2121}