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