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