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