Skip to main content

cranpose_core/
composer.rs

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