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