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