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