Skip to main content

cranpose_core/
composer.rs

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