Skip to main content

cranpose_core/
composer.rs

1use crate::collections::map::{HashMap, HashSet};
2use crate::retention::{RetainKey, RetentionManager};
3use crate::slot::{FinishGroupResult, PayloadKind};
4use crate::slot::{GroupStart, GroupStartKind, ValueSlotId};
5use crate::{
6    composer_context, empty_local_stack, explicit_group_key_seed, runtime, Applier, ApplierHost,
7    ChildList, Command, CommandQueue, CompositionLocal, DirtyBubble, Key, LocalKey,
8    LocalStackSnapshot, LocalStateEntry, MutableState, Node, NodeError, NodeId, Owned,
9    ProvidedValue, RecomposeOptions, RecomposeScope, RecomposeScopeInner, RecycledNode,
10    RetentionMode, RetentionPolicy, RuntimeHandle, ScopeId, SlotId, SlotPassOutcome, SlotTable,
11    SlotsHost, SnapshotStateList, SnapshotStateMap, SnapshotStateObserver, StaticCompositionLocal,
12    StaticLocalEntry, SubcomposeState, COMMAND_FLUSH_THRESHOLD,
13};
14use smallvec::SmallVec;
15use std::any::Any;
16use std::cell::{Cell, RefCell, RefMut};
17use std::hash::Hash;
18use std::marker::PhantomData;
19use std::rc::{Rc, Weak};
20
21pub struct ValueSlotHandle<'pass, T: 'static> {
22    slot: ValueSlotId,
23    _pass: PhantomData<&'pass Composer>,
24    _value: PhantomData<fn() -> T>,
25}
26
27impl<T: 'static> Copy for ValueSlotHandle<'_, T> {}
28
29impl<T: 'static> Clone for ValueSlotHandle<'_, T> {
30    fn clone(&self) -> Self {
31        *self
32    }
33}
34
35impl<T: 'static> ValueSlotHandle<'_, T> {
36    pub(crate) fn new(slot: ValueSlotId) -> Self {
37        Self {
38            slot,
39            _pass: PhantomData,
40            _value: PhantomData,
41        }
42    }
43
44    pub(crate) fn slot(self) -> ValueSlotId {
45        self.slot
46    }
47}
48
49fn slots_storage_key(host: &Rc<SlotsHost>) -> usize {
50    host.storage_key()
51}
52
53fn bind_slots_host_to_runtime_state(
54    state: &Rc<ComposerRuntimeState>,
55    host: &Rc<SlotsHost>,
56) -> Rc<SlotsHost> {
57    if let Some(bound_state) = host.runtime_state() {
58        if Rc::ptr_eq(&bound_state, state) {
59            state.bind_slots_host(host);
60            return Rc::clone(host);
61        }
62        drop(bound_state);
63        if host.rebind_orphaned_runtime_state(state) {
64            state.bind_slots_host(host);
65            return Rc::clone(host);
66        }
67        log::error!(
68            "slot host already belongs to a different composer runtime state; using a fresh slot host"
69        );
70        let replacement = Rc::new(SlotsHost::new(SlotTable::new()));
71        state.bind_slots_host(&replacement);
72        return replacement;
73    }
74    state.bind_slots_host(host);
75    Rc::clone(host)
76}
77
78struct SlotHostPassGuard {
79    core: Rc<ComposerCore>,
80    host: Rc<SlotsHost>,
81    active: bool,
82}
83
84impl SlotHostPassGuard {
85    fn close(&mut self) {
86        if !self.active {
87            return;
88        }
89        if self.host.has_active_pass() {
90            self.host.abandon_active_pass();
91        }
92        match self.core.slot_hosts.borrow_mut().pop() {
93            Some(host) if Rc::ptr_eq(&host, &self.host) => {}
94            Some(_) => {
95                log::error!("slot host stack mismatch while closing slot host pass");
96            }
97            None => {
98                log::error!("slot host stack underflow while closing slot host pass");
99            }
100        }
101        self.active = false;
102    }
103}
104
105impl Drop for SlotHostPassGuard {
106    fn drop(&mut self) {
107        self.close();
108    }
109}
110
111pub(crate) struct ComposerRuntimeState {
112    scope_registry: RefCell<HashMap<ScopeId, RecomposeScope>>,
113    retention_by_host: RefCell<HashMap<usize, RetentionManager>>,
114    retention_policy: Cell<RetentionPolicy>,
115    live_hosts: RefCell<HashMap<usize, std::rc::Weak<SlotsHost>>>,
116    applier_host: RefCell<Option<std::rc::Weak<dyn ApplierHost>>>,
117}
118
119impl Default for ComposerRuntimeState {
120    fn default() -> Self {
121        Self {
122            scope_registry: RefCell::new(HashMap::default()),
123            retention_by_host: RefCell::new(HashMap::default()),
124            retention_policy: Cell::new(RetentionPolicy::default()),
125            live_hosts: RefCell::new(HashMap::default()),
126            applier_host: RefCell::new(None),
127        }
128    }
129}
130
131impl ComposerRuntimeState {
132    pub(crate) fn clear_host_storage_key(&self, host_key: usize) {
133        self.retention_by_host.borrow_mut().remove(&host_key);
134        self.live_hosts.borrow_mut().remove(&host_key);
135        let removed_scopes = {
136            let mut removed = Vec::new();
137            self.scope_registry.borrow_mut().retain(|_, scope| {
138                if scope.slots_storage_key() == Some(host_key) {
139                    removed.push(scope.clone());
140                    false
141                } else {
142                    true
143                }
144            });
145            removed
146        };
147        for scope in removed_scopes {
148            scope.deactivate();
149        }
150    }
151
152    pub(crate) fn bind_applier_host(&self, applier: &Rc<dyn ApplierHost>) {
153        *self.applier_host.borrow_mut() = Some(Rc::downgrade(applier));
154    }
155
156    pub(crate) fn has_live_applier_host(&self) -> bool {
157        self.applier_host
158            .borrow()
159            .as_ref()
160            .and_then(std::rc::Weak::upgrade)
161            .is_some()
162    }
163
164    pub(crate) fn bind_slots_host(self: &Rc<Self>, host: &Rc<SlotsHost>) {
165        host.bind_runtime_state(self);
166        self.live_hosts
167            .borrow_mut()
168            .insert(host.storage_key(), Rc::downgrade(host));
169    }
170
171    pub(crate) fn scope_for_id(&self, scope_id: ScopeId) -> Option<RecomposeScope> {
172        self.scope_registry.borrow().get(&scope_id).cloned()
173    }
174
175    pub(crate) fn register_scope(&self, scope: &RecomposeScope) {
176        self.scope_registry
177            .borrow_mut()
178            .insert(scope.id(), scope.clone());
179    }
180
181    pub(crate) fn remove_scope(&self, scope_id: ScopeId) -> Option<RecomposeScope> {
182        self.scope_registry.borrow_mut().remove(&scope_id)
183    }
184
185    pub(crate) fn set_retention_policy(&self, policy: RetentionPolicy) {
186        self.retention_policy.set(policy);
187    }
188
189    pub(crate) fn retention_policy(&self) -> RetentionPolicy {
190        self.retention_policy.get()
191    }
192
193    pub(crate) fn scope_registry_len(&self) -> usize {
194        self.scope_registry.borrow().len()
195    }
196
197    pub(crate) fn take_retained(
198        &self,
199        host: &Rc<SlotsHost>,
200        key: RetainKey,
201        preflight: impl FnOnce(&crate::slot::DetachedSubtree) -> bool,
202    ) -> Option<crate::slot::DetachedSubtree> {
203        let host_key = slots_storage_key(host);
204        let mut retention = self.retention_by_host.borrow_mut();
205        let subtree = retention
206            .get_mut(&host_key)?
207            .take_after_restore_preflight(key, preflight);
208        if retention
209            .get(&host_key)
210            .is_some_and(|manager| manager.is_empty() && manager.evictions_total() == 0)
211        {
212            retention.remove(&host_key);
213        }
214        subtree
215    }
216
217    pub(crate) fn insert_retained(
218        &self,
219        host: &Rc<SlotsHost>,
220        key: RetainKey,
221        subtree: crate::slot::DetachedSubtree,
222    ) -> Vec<crate::slot::DetachedSubtree> {
223        let policy = self.retention_policy();
224        let mut retention_by_host = self.retention_by_host.borrow_mut();
225        let manager = retention_by_host
226            .entry(slots_storage_key(host))
227            .or_insert_with(|| RetentionManager::new(policy));
228        manager.set_policy(policy);
229        manager.insert(key, subtree)
230    }
231
232    pub(crate) fn advance_retention_pass(
233        &self,
234        host: &Rc<SlotsHost>,
235    ) -> Vec<crate::slot::DetachedSubtree> {
236        let host_key = slots_storage_key(host);
237        let policy = self.retention_policy();
238        let mut retention_by_host = self.retention_by_host.borrow_mut();
239        let Some(manager) = retention_by_host.get_mut(&host_key) else {
240            return Vec::new();
241        };
242        manager.set_policy(policy);
243        manager.advance_pass()
244    }
245
246    pub(crate) fn fill_slot_debug_snapshot(
247        &self,
248        host: &SlotsHost,
249        snapshot: &mut crate::SlotDebugSnapshot,
250    ) {
251        let retention = self.retention_debug_stats(host.storage_key());
252        snapshot.runtime_scope_registry_count = Some(self.scope_registry_len());
253        snapshot.retained_subtree_count = retention.subtree_count;
254        snapshot.retained_group_count = retention.group_count;
255        snapshot.retained_payload_count = retention.payload_count;
256        snapshot.retained_node_count = retention.node_count;
257        snapshot.retained_scope_count = retention.scope_count;
258    }
259
260    pub(crate) fn slot_retention_debug_stats(
261        &self,
262        host: &SlotsHost,
263    ) -> crate::slot::SlotRetentionDebugStats {
264        let retention = self.retention_debug_stats(host.storage_key());
265        crate::slot::SlotRetentionDebugStats {
266            retained_subtree_count: retention.subtree_count,
267            retained_group_count: retention.group_count,
268            retained_payload_count: retention.payload_count,
269            retained_node_count: retention.node_count,
270            retained_scope_count: retention.scope_count,
271            retained_anchor_count: retention.anchor_count,
272            retained_heap_bytes: retention.heap_bytes,
273            retained_evictions_total: retention.evictions_total,
274        }
275    }
276
277    pub(crate) fn compact_table_identity_storage_for_host(
278        &self,
279        host: &SlotsHost,
280        table: &mut SlotTable,
281        compact_anchors: bool,
282        compact_payloads: bool,
283    ) {
284        if !compact_anchors && !compact_payloads {
285            return;
286        }
287
288        let host_key = host.storage_key();
289        let mut retention = self.retention_by_host.borrow_mut();
290        if let Some(retained) = retention.get_mut(&host_key) {
291            if compact_anchors {
292                table.compact_anchor_registry_storage(Some(&mut *retained));
293            }
294            if compact_payloads {
295                table.compact_payload_anchor_registry_storage(Some(&mut *retained));
296            }
297        } else {
298            if compact_anchors {
299                table.compact_anchor_registry_storage(None);
300            }
301            if compact_payloads {
302                table.compact_payload_anchor_registry_storage(None);
303            }
304        }
305    }
306
307    pub(crate) fn clear_host(&self, host: &SlotsHost) {
308        let host_key = host.storage_key();
309        debug_assert!(
310            self.host_retention_is_empty(host),
311            "host retention must be drained before clearing host ownership"
312        );
313        self.clear_host_storage_key(host_key);
314    }
315
316    pub(crate) fn dispose_retained_subtrees_for_host(
317        &self,
318        host_key: usize,
319        table: &mut SlotTable,
320        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
321    ) -> Result<(), NodeError> {
322        let applier_host = self
323            .applier_host
324            .borrow()
325            .as_ref()
326            .and_then(std::rc::Weak::upgrade);
327        if let Some(applier_host) = applier_host.as_ref() {
328            let retention_by_host = self.retention_by_host.borrow();
329            let Some(retention) = retention_by_host.get(&host_key) else {
330                return Ok(());
331            };
332            let mut applier = applier_host.borrow_dyn();
333            for subtree in retention.subtrees() {
334                crate::slot::dispose_detached_subtree_now(&mut *applier, subtree)?;
335            }
336        }
337        let Some(retention) = self.retention_by_host.borrow_mut().remove(&host_key) else {
338            return Ok(());
339        };
340        for subtree in retention.into_subtrees() {
341            for scope_id in subtree.scope_ids() {
342                if let Some(scope) = self.remove_scope(scope_id) {
343                    scope.deactivate();
344                }
345            }
346            table.invalidate_detached_subtree_anchors(&subtree);
347            lifecycle.queue_subtree_disposal(subtree);
348        }
349        Ok(())
350    }
351
352    pub(crate) fn abandon_retained_subtrees_for_host(
353        &self,
354        host_key: usize,
355        table: &mut SlotTable,
356        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
357    ) {
358        let Some(retention) = self.retention_by_host.borrow_mut().remove(&host_key) else {
359            self.clear_host_storage_key(host_key);
360            return;
361        };
362        for subtree in retention.into_subtrees() {
363            for scope_id in subtree.scope_ids() {
364                if let Some(scope) = self.remove_scope(scope_id) {
365                    scope.deactivate();
366                }
367            }
368            table.invalidate_detached_subtree_anchors(&subtree);
369            lifecycle.queue_subtree_disposal(subtree);
370        }
371        self.clear_host_storage_key(host_key);
372    }
373
374    pub(crate) fn host_retention_is_empty(&self, host: &SlotsHost) -> bool {
375        self.retention_by_host
376            .borrow()
377            .get(&host.storage_key())
378            .is_none_or(RetentionManager::is_empty)
379    }
380
381    #[cfg(any(test, debug_assertions))]
382    pub(crate) fn debug_verify_host(&self, host: &SlotsHost, table: &SlotTable) {
383        if let Some(retention) = self.retention_by_host.borrow().get(&host.storage_key()) {
384            retention.debug_verify(table);
385        }
386    }
387
388    #[cfg(test)]
389    pub(crate) fn validate_host_retention(
390        &self,
391        host: &SlotsHost,
392        table: &SlotTable,
393    ) -> Result<(), crate::slot::SlotInvariantError> {
394        if let Some(retention) = self.retention_by_host.borrow().get(&host.storage_key()) {
395            retention.validate(table)?;
396        }
397        Ok(())
398    }
399
400    pub(crate) fn host_for_storage_key(&self, storage_key: usize) -> Option<Rc<SlotsHost>> {
401        self.live_hosts
402            .borrow()
403            .get(&storage_key)
404            .and_then(std::rc::Weak::upgrade)
405    }
406
407    fn retention_debug_stats(&self, host_key: usize) -> crate::retention::RetentionDebugStats {
408        self.retention_by_host
409            .borrow()
410            .get(&host_key)
411            .map(RetentionManager::debug_stats)
412            .unwrap_or_default()
413    }
414}
415
416pub(crate) struct ParentFrame {
417    pub(crate) id: NodeId,
418    pub(crate) previous: ChildList,
419    pub(crate) new_children: ChildList,
420    pub(crate) new_children_membership: Option<HashSet<NodeId>>,
421    pub(crate) attach_mode: ParentAttachMode,
422    pub(crate) synthetic_root: bool,
423}
424
425#[derive(Clone, Copy)]
426pub(crate) enum InitialParentFrame {
427    SyntheticRoot,
428    RealParent,
429}
430
431const LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD: usize = 16;
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq)]
434pub(crate) enum ParentAttachMode {
435    ImmediateAppend,
436    DeferredSync,
437}
438
439#[derive(Default)]
440pub(crate) struct SubcomposeFrame {
441    pub(crate) nodes: Vec<NodeId>,
442    pub(crate) scopes: Vec<RecomposeScope>,
443}
444
445#[derive(Default, Clone)]
446pub(crate) struct LocalContext {
447    pub(crate) values: HashMap<LocalKey, Rc<dyn Any>>,
448}
449
450pub(crate) struct ComposerCore {
451    pub(crate) shared_state: Rc<ComposerRuntimeState>,
452    pub(crate) slots: Rc<SlotsHost>,
453    slot_hosts: RefCell<Vec<Rc<SlotsHost>>>,
454    pub(crate) applier: Rc<dyn ApplierHost>,
455    pub(crate) runtime: RuntimeHandle,
456    pub(crate) observer: SnapshotStateObserver,
457    pub(crate) parent_stack: RefCell<Vec<ParentFrame>>,
458    pub(crate) subcompose_stack: RefCell<Vec<SubcomposeFrame>>,
459    pub(crate) root: Cell<Option<NodeId>>,
460    pub(crate) commands: RefCell<CommandQueue>,
461    pub(crate) scope_stack: RefCell<Vec<RecomposeScope>>,
462    subcomposition_owner_scope: RefCell<Option<RecomposeScope>>,
463    pub(crate) local_stack: RefCell<LocalStackSnapshot>,
464    pub(crate) side_effects: RefCell<Vec<Box<dyn FnOnce()>>>,
465    pub(crate) pending_scope_options: RefCell<Option<RecomposeOptions>>,
466    pub(crate) phase: Cell<crate::Phase>,
467    pub(crate) last_node_reused: Cell<Option<bool>>,
468    pub(crate) recranpose_parent_hint: Cell<Option<NodeId>>,
469    pub(crate) root_render_requested: Cell<bool>,
470    pub(crate) _not_send: PhantomData<*const ()>,
471}
472
473/// The composition context inherited by work that is composed in another slot
474/// host. Besides composition locals, this carries the source owner scope so a
475/// secondary tree cannot outlive the composition that supplied its callbacks.
476#[derive(Clone)]
477pub struct CapturedCompositionContext {
478    locals: LocalStackSnapshot,
479    owner_scope: Option<Weak<RecomposeScopeInner>>,
480}
481
482fn take_subcompose_frame(core: &ComposerCore, operation: &str) -> SubcomposeFrame {
483    match core.subcompose_stack.borrow_mut().pop() {
484        Some(frame) => frame,
485        None => {
486            log::error!("subcompose stack underflow while finishing {operation}");
487            SubcomposeFrame::default()
488        }
489    }
490}
491
492impl ComposerCore {
493    pub(crate) fn new(
494        shared_state: Rc<ComposerRuntimeState>,
495        slots: Rc<SlotsHost>,
496        applier: Rc<dyn ApplierHost>,
497        runtime: RuntimeHandle,
498        observer: SnapshotStateObserver,
499        root: Option<NodeId>,
500        initial_parent_frame: InitialParentFrame,
501    ) -> Self {
502        let parent_stack = if let Some(root_id) = root {
503            vec![ParentFrame {
504                id: root_id,
505                previous: ChildList::new(),
506                new_children: ChildList::new(),
507                new_children_membership: None,
508                attach_mode: ParentAttachMode::DeferredSync,
509                synthetic_root: matches!(initial_parent_frame, InitialParentFrame::SyntheticRoot),
510            }]
511        } else {
512            Vec::new()
513        };
514
515        Self {
516            shared_state,
517            slots,
518            slot_hosts: RefCell::new(Vec::new()),
519            applier,
520            runtime,
521            observer,
522            parent_stack: RefCell::new(parent_stack),
523            subcompose_stack: RefCell::new(Vec::new()),
524            root: Cell::new(root),
525            commands: RefCell::new(CommandQueue::default()),
526            scope_stack: RefCell::new(Vec::new()),
527            subcomposition_owner_scope: RefCell::new(None),
528            local_stack: RefCell::new(empty_local_stack()),
529            side_effects: RefCell::new(Vec::new()),
530            pending_scope_options: RefCell::new(None),
531            phase: Cell::new(crate::Phase::Compose),
532            last_node_reused: Cell::new(None),
533            recranpose_parent_hint: Cell::new(None),
534            root_render_requested: Cell::new(false),
535            _not_send: PhantomData,
536        }
537    }
538}
539
540#[derive(Clone)]
541pub struct Composer {
542    pub(crate) core: Rc<ComposerCore>,
543}
544
545pub(crate) enum EmittedNode {
546    Fresh(Box<dyn Node>),
547    Recycled(RecycledNode),
548}
549
550impl Composer {
551    pub(crate) fn new_with_shared_state(
552        shared_state: Rc<ComposerRuntimeState>,
553        slots: Rc<SlotsHost>,
554        applier: Rc<dyn ApplierHost>,
555        runtime: RuntimeHandle,
556        observer: SnapshotStateObserver,
557        root: Option<NodeId>,
558    ) -> Self {
559        Self::new_with_shared_state_with_parent_frame(
560            shared_state,
561            slots,
562            applier,
563            runtime,
564            observer,
565            root,
566            InitialParentFrame::SyntheticRoot,
567        )
568    }
569
570    fn new_with_shared_state_with_parent_frame(
571        shared_state: Rc<ComposerRuntimeState>,
572        slots: Rc<SlotsHost>,
573        applier: Rc<dyn ApplierHost>,
574        runtime: RuntimeHandle,
575        observer: SnapshotStateObserver,
576        root: Option<NodeId>,
577        initial_parent_frame: InitialParentFrame,
578    ) -> Self {
579        shared_state.bind_applier_host(&applier);
580        let slots = bind_slots_host_to_runtime_state(&shared_state, &slots);
581        let core = Rc::new(ComposerCore::new(
582            shared_state,
583            slots,
584            applier,
585            runtime,
586            observer,
587            root,
588            initial_parent_frame,
589        ));
590        Self { core }
591    }
592
593    pub fn new(
594        slots: Rc<SlotsHost>,
595        applier: Rc<dyn ApplierHost>,
596        runtime: RuntimeHandle,
597        observer: SnapshotStateObserver,
598        root: Option<NodeId>,
599    ) -> Self {
600        Self::new_with_shared_state_with_parent_frame(
601            slots
602                .runtime_state()
603                .unwrap_or_else(|| Rc::new(ComposerRuntimeState::default())),
604            slots,
605            applier,
606            runtime,
607            observer,
608            root,
609            InitialParentFrame::RealParent,
610        )
611    }
612
613    pub(crate) fn from_core(core: Rc<ComposerCore>) -> Self {
614        Self { core }
615    }
616
617    pub(crate) fn clone_core(&self) -> Rc<ComposerCore> {
618        Rc::clone(&self.core)
619    }
620
621    fn observer(&self) -> SnapshotStateObserver {
622        self.core.observer.clone()
623    }
624
625    pub(crate) fn request_root_render(&self) {
626        self.core.root_render_requested.set(true);
627    }
628
629    pub(crate) fn take_root_render_request(&self) -> bool {
630        self.core.root_render_requested.replace(false)
631    }
632
633    pub(crate) fn observe_scope<R>(&self, scope: &RecomposeScope, block: impl FnOnce() -> R) -> R {
634        let observer = self.observer();
635        let scope_clone = scope.clone();
636        observer.observe_reads(scope_clone, move |scope_ref| scope_ref.invalidate(), block)
637    }
638
639    pub(crate) fn active_slots_host(&self) -> Rc<SlotsHost> {
640        self.core
641            .slot_hosts
642            .borrow()
643            .last()
644            .cloned()
645            .unwrap_or_else(|| Rc::clone(&self.core.slots))
646    }
647
648    pub(crate) fn with_slots<R>(&self, f: impl FnOnce(&SlotTable) -> R) -> R {
649        let host = self.active_slots_host();
650        let slots = host.borrow();
651        f(&slots)
652    }
653
654    pub(crate) fn with_slots_mut<R>(&self, f: impl FnOnce(&mut SlotTable) -> R) -> R {
655        let host = self.active_slots_host();
656        let mut slots = host.borrow_mut();
657        f(&mut slots)
658    }
659
660    pub(crate) fn with_slot_session_mut<R>(
661        &self,
662        f: impl FnOnce(&mut crate::slot::SlotWriteSession<'_>) -> R,
663    ) -> R {
664        self.active_slots_host().with_write_session(f)
665    }
666
667    pub(crate) fn try_with_slot_host_pass<R>(
668        &self,
669        slots: Rc<SlotsHost>,
670        mode: crate::slot::SlotPassMode,
671        f: impl FnOnce(&Composer) -> R,
672    ) -> Result<(R, SlotPassOutcome), NodeError> {
673        let mut guard = self.begin_slot_host_pass(&slots, mode);
674        let result = f(self);
675        let outcome = self.finish_slot_host_pass(&guard.host)?;
676        guard.close();
677        Ok((result, outcome))
678    }
679
680    pub(crate) fn with_slot_host_pass<R>(
681        &self,
682        slots: Rc<SlotsHost>,
683        mode: crate::slot::SlotPassMode,
684        f: impl FnOnce(&Composer) -> R,
685    ) -> (R, SlotPassOutcome) {
686        let mut guard = self.begin_slot_host_pass(&slots, mode);
687        let result = f(self);
688        let outcome = match self.finish_slot_host_pass(&guard.host) {
689            Ok(outcome) => outcome,
690            Err(err) => {
691                log::error!("slot host pass finalization failed: {err}");
692                SlotPassOutcome::default()
693            }
694        };
695        guard.close();
696        (result, outcome)
697    }
698
699    pub(crate) fn with_slot_override<R>(
700        &self,
701        slots: Rc<SlotsHost>,
702        f: impl FnOnce(&Composer) -> R,
703    ) -> (R, SlotPassOutcome) {
704        self.with_slot_host_pass(slots, crate::slot::SlotPassMode::Compose, f)
705    }
706
707    fn begin_slot_host_pass(
708        &self,
709        slots: &Rc<SlotsHost>,
710        mode: crate::slot::SlotPassMode,
711    ) -> SlotHostPassGuard {
712        let slots = bind_slots_host_to_runtime_state(&self.core.shared_state, slots);
713        slots.begin_pass(mode);
714        self.core.slot_hosts.borrow_mut().push(Rc::clone(&slots));
715        SlotHostPassGuard {
716            core: self.clone_core(),
717            host: slots,
718            active: true,
719        }
720    }
721
722    fn finish_slot_host_pass(&self, slots: &Rc<SlotsHost>) -> Result<SlotPassOutcome, NodeError> {
723        let finished = {
724            let mut applier = self.core.applier.borrow_dyn();
725            slots.finish_pass(&mut *applier)
726        }?;
727        self.handle_detached_children_in_host(slots, None, finished.detached_root_children)?;
728        self.evict_retained_subtrees_for_host(slots)?;
729        slots.complete_pass_cleanup(&finished.outcome);
730        Ok(finished.outcome)
731    }
732
733    pub(crate) fn parent_stack(&self) -> RefMut<'_, Vec<ParentFrame>> {
734        self.core.parent_stack.borrow_mut()
735    }
736
737    fn current_parent_hint(&self) -> Option<NodeId> {
738        let stack = self.core.parent_stack.borrow();
739        let stack_hint = stack
740            .last()
741            .and_then(|frame| (!frame.synthetic_root).then_some(frame.id));
742        stack_hint.or_else(|| self.core.recranpose_parent_hint.get())
743    }
744
745    pub(crate) fn subcompose_stack(&self) -> RefMut<'_, Vec<SubcomposeFrame>> {
746        self.core.subcompose_stack.borrow_mut()
747    }
748
749    pub(crate) fn commands_mut(&self) -> RefMut<'_, CommandQueue> {
750        self.core.commands.borrow_mut()
751    }
752
753    pub(crate) fn enqueue_semantics_invalidation(&self, id: NodeId) {
754        self.commands_mut().push(Command::BubbleDirty {
755            node_id: id,
756            bubble: DirtyBubble::SEMANTICS,
757        });
758    }
759
760    pub(crate) fn scope_stack(&self) -> RefMut<'_, Vec<RecomposeScope>> {
761        self.core.scope_stack.borrow_mut()
762    }
763
764    fn scope_for_id(&self, scope_id: ScopeId) -> Option<RecomposeScope> {
765        self.core.shared_state.scope_for_id(scope_id)
766    }
767
768    fn register_scope(&self, scope: &RecomposeScope) {
769        self.core.shared_state.register_scope(scope);
770    }
771
772    fn remove_scope(&self, scope_id: ScopeId) -> Option<RecomposeScope> {
773        self.core.shared_state.remove_scope(scope_id)
774    }
775
776    pub(crate) fn local_stack(&self) -> RefMut<'_, LocalStackSnapshot> {
777        self.core.local_stack.borrow_mut()
778    }
779
780    pub(crate) fn current_local_stack(&self) -> LocalStackSnapshot {
781        self.core.local_stack.borrow().clone()
782    }
783
784    pub(crate) fn side_effects_mut(&self) -> RefMut<'_, Vec<Box<dyn FnOnce()>>> {
785        self.core.side_effects.borrow_mut()
786    }
787
788    fn pending_scope_options(&self) -> RefMut<'_, Option<RecomposeOptions>> {
789        self.core.pending_scope_options.borrow_mut()
790    }
791
792    pub(crate) fn borrow_applier(&self) -> RefMut<'_, dyn Applier> {
793        self.core.applier.borrow_dyn()
794    }
795
796    /// Registers a virtual node in the Applier.
797    ///
798    /// This is used by SubcomposeLayoutNode to register virtual container nodes
799    /// so that subsequent insert_child commands can find them and attach children.
800    /// Without this, virtual nodes would only exist in SubcomposeLayoutNodeInner.virtual_nodes
801    /// and applier.get_mut(virtual_node_id) would fail, breaking child attachment.
802    pub fn register_virtual_node(
803        &self,
804        node_id: NodeId,
805        node: Box<dyn Node>,
806    ) -> Result<(), NodeError> {
807        let mut applier = self.borrow_applier();
808        applier.insert_with_id(node_id, node)
809    }
810
811    /// Checks if a node has no parent (is a root node).
812    /// Used by SubcomposeMeasureScope to filter subcompose results.
813    pub fn node_has_no_parent(&self, node_id: NodeId) -> bool {
814        let mut applier = self.borrow_applier();
815        match applier.get_mut(node_id) {
816            Ok(node) => node.parent().is_none(),
817            Err(_) => true,
818        }
819    }
820
821    /// Gets the children of a node from the Applier.
822    ///
823    /// This is used by SubcomposeLayoutNode to get children of virtual nodes
824    /// directly from the Applier, where insert_child commands have been applied.
825    pub fn get_node_children(&self, node_id: NodeId) -> SmallVec<[NodeId; 8]> {
826        let mut applier = self.borrow_applier();
827        match applier.get_mut(node_id) {
828            Ok(node) => {
829                let mut children = SmallVec::<[NodeId; 8]>::new();
830                node.collect_children_into(&mut children);
831                children
832            }
833            Err(_) => SmallVec::<[NodeId; 8]>::new(),
834        }
835    }
836
837    pub fn nodes_need_measure(&self, node_ids: &[NodeId]) -> bool {
838        let mut applier = self.borrow_applier();
839        node_ids.iter().any(|node_id| {
840            applier
841                .get_mut(*node_id)
842                .is_ok_and(|node| node.needs_measure())
843        })
844    }
845
846    /// Whether any of `node_ids` carries a pending *layout* (placement) repass.
847    ///
848    /// Layout-only dirtiness is not a subset of measure dirtiness: a scroll
849    /// offset change keeps every measured size intact and therefore bubbles
850    /// `needs_layout` alone. Callers that gate cache reuse on
851    /// [`Self::nodes_need_measure`] must also consult this, or a node whose
852    /// *position* changed will replay its stale cached placement forever.
853    pub fn nodes_need_layout(&self, node_ids: &[NodeId]) -> bool {
854        let mut applier = self.borrow_applier();
855        node_ids.iter().any(|node_id| {
856            applier
857                .get_mut(*node_id)
858                .is_ok_and(|node| node.needs_layout())
859        })
860    }
861
862    /// Records a child node in the current parent frame's expected children list.
863    ///
864    /// Used by SubcomposeLayout's `perform_subcompose` to register virtual nodes
865    /// with the outer composer's parent frame. This ensures that the `pop_parent`
866    /// call at the end of `subcompose_slot` generates a correct `SyncChildren`
867    /// command that preserves (rather than removes) the virtual nodes.
868    ///
869    /// Without this, `pop_parent` would generate `SyncChildren { expected: [] }`,
870    /// which removes all virtual nodes and their subtrees from the applier.
871    pub fn record_subcompose_child(&self, child_id: NodeId) {
872        let mut parent_stack = self.parent_stack();
873        if let Some(frame) = parent_stack.last_mut() {
874            if matches!(frame.attach_mode, ParentAttachMode::DeferredSync) {
875                if let Some(membership) = frame.new_children_membership.as_mut() {
876                    if membership.insert(child_id) {
877                        frame.new_children.push(child_id);
878                    }
879                } else if frame.new_children.len() >= LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD {
880                    let mut membership = HashSet::default();
881                    membership.reserve(frame.new_children.len() + 1);
882                    membership.extend(frame.new_children.iter().copied());
883                    if membership.insert(child_id) {
884                        frame.new_children.push(child_id);
885                    }
886                    frame.new_children_membership = Some(membership);
887                } else if !frame.new_children.contains(&child_id) {
888                    frame.new_children.push(child_id);
889                }
890            }
891        }
892    }
893
894    /// Clears all children of a node in the Applier.
895    ///
896    /// This is used by SubcomposeLayoutNode when reusing a virtual node for
897    /// different content. Without clearing, old children remain attached,
898    /// causing duplicate/interleaved items in lazy lists after scrolling.
899    pub fn clear_node_children(&self, node_id: NodeId) {
900        let mut applier = self.borrow_applier();
901        if let Ok(node) = applier.get_mut(node_id) {
902            node.update_children(&[]);
903        }
904    }
905
906    pub fn install<R>(&self, f: impl FnOnce(&Composer) -> R) -> R {
907        let _composer_guard = composer_context::enter(self);
908        runtime::push_active_runtime(&self.core.runtime);
909        struct Guard;
910        impl Drop for Guard {
911            fn drop(&mut self) {
912                runtime::pop_active_runtime();
913            }
914        }
915        let guard = Guard;
916        let result = f(self);
917        drop(guard);
918        result
919    }
920
921    pub(crate) fn flush_pending_commands_if_large(&self) -> Result<(), NodeError> {
922        let queued = self.core.commands.borrow().len();
923        if queued < COMMAND_FLUSH_THRESHOLD {
924            return Ok(());
925        }
926        self.apply_pending_commands()
927    }
928
929    /// Thin monomorphic shim: the group machinery lives in
930    /// [`Self::with_group_in_active_pass_dyn`] so it is compiled once instead
931    /// of once per composable call site (real apps have thousands).
932    fn with_group_in_active_pass<R>(
933        &self,
934        key: crate::slot::GroupKeySeed,
935        f: impl FnOnce(&Composer) -> R,
936    ) -> R {
937        let mut f = Some(f);
938        let mut result = None;
939        self.with_group_in_active_pass_dyn(key, &mut |composer| {
940            let f = f.take().expect("group body must run at most once");
941            result = Some(f(composer));
942        });
943        result.expect("group body must run exactly once")
944    }
945
946    /// `inline(never)`: with fat LTO this single-caller body would otherwise
947    /// be inlined back into every monomorphic shim, recreating the per-call-site
948    /// code explosion this split exists to prevent.
949    #[inline(never)]
950    fn with_group_in_active_pass_dyn(
951        &self,
952        key: crate::slot::GroupKeySeed,
953        f: &mut dyn FnMut(&Composer),
954    ) {
955        struct GroupGuard {
956            composer: Composer,
957            scope: RecomposeScope,
958        }
959
960        impl Drop for GroupGuard {
961            fn drop(&mut self) {
962                self.composer
963                    .close_current_group_body_for_scope(&self.scope);
964                self.scope.mark_recomposed();
965                self.composer
966                    .with_slot_session_mut(|slots| slots.end_group());
967                if let Err(err) = self.composer.flush_pending_commands_if_large() {
968                    log::error!("mid-composition command flush failed: {err}");
969                }
970            }
971        }
972
973        let parent_scope = self.current_recranpose_scope();
974        let options = self.pending_scope_options().take().unwrap_or_default();
975        let parent_scope_id = parent_scope.as_ref().map(RecomposeScope::id);
976        let reserved_key = self.with_slot_session_mut(|slots| slots.preview_group_key(key));
977        let host = self.active_slots_host();
978        let restored = self.core.shared_state.take_retained(
979            &host,
980            RetainKey {
981                parent_scope: parent_scope_id,
982                key: reserved_key,
983            },
984            |subtree| {
985                self.with_slot_session_mut(|slots| {
986                    slots.retained_restore_ready(reserved_key, subtree)
987                })
988            },
989        );
990        let (group, start_scope_id, start_kind) = self.with_slot_session_mut(|slots| {
991            let GroupStart {
992                group,
993                scope_id,
994                kind,
995                ..
996            } = slots.begin_group(reserved_key, restored);
997            (group, scope_id, kind)
998        });
999        let scope_ref =
1000            if let Some(scope) = start_scope_id.and_then(|scope_id| self.scope_for_id(scope_id)) {
1001                scope
1002            } else {
1003                let scope = RecomposeScope::new(self.runtime_handle());
1004                self.register_scope(&scope);
1005                self.with_slot_session_mut(|slots| slots.set_group_scope(group, scope.id()));
1006                scope
1007            };
1008
1009        let lifetime_owner_scope = if parent_scope.is_none() {
1010            self.core.subcomposition_owner_scope.borrow().clone()
1011        } else {
1012            None
1013        };
1014        scope_ref.reactivate();
1015        scope_ref.set_parent_scope(parent_scope);
1016        scope_ref.set_lifetime_owner_scope(lifetime_owner_scope);
1017        scope_ref.set_retention_mode(options.retention);
1018
1019        if options.force_recompose {
1020            scope_ref.force_recompose();
1021        } else if options.force_reuse {
1022            scope_ref.force_reuse();
1023        }
1024        if matches!(start_kind, GroupStartKind::Restored) {
1025            scope_ref.force_recompose();
1026        }
1027
1028        scope_ref.set_slots_host(&host);
1029
1030        {
1031            let mut stack = self.scope_stack();
1032            stack.push(scope_ref.clone());
1033        }
1034
1035        {
1036            let mut stack = self.subcompose_stack();
1037            if let Some(frame) = stack.last_mut() {
1038                frame.scopes.push(scope_ref.clone());
1039            }
1040        }
1041
1042        scope_ref.snapshot_locals(self.current_local_stack());
1043        {
1044            let parent_hint = self.current_parent_hint();
1045            scope_ref.set_parent_hint(parent_hint);
1046        }
1047
1048        let guard = GroupGuard {
1049            composer: self.clone(),
1050            scope: scope_ref.clone(),
1051        };
1052        self.observe_scope(&scope_ref, || f(self));
1053        scope_ref.mark_composed_once();
1054        drop(guard);
1055    }
1056
1057    pub(crate) fn with_group_seed<R>(
1058        &self,
1059        key: crate::slot::GroupKeySeed,
1060        f: impl FnOnce(&Composer) -> R,
1061    ) -> R {
1062        let host = self.active_slots_host();
1063        if host.has_active_pass() {
1064            return self.with_group_in_active_pass(key, f);
1065        }
1066        let (result, _) =
1067            self.with_slot_host_pass(host, crate::slot::SlotPassMode::Compose, |composer| {
1068                composer.with_group_in_active_pass(key, f)
1069            });
1070        result
1071    }
1072
1073    pub fn with_group<R>(&self, key: Key, f: impl FnOnce(&Composer) -> R) -> R {
1074        self.with_group_seed(crate::slot::GroupKeySeed::unkeyed(key), f)
1075    }
1076
1077    pub fn cranpose_with_reuse<R>(
1078        &self,
1079        key: Key,
1080        mut options: RecomposeOptions,
1081        f: impl FnOnce(&Composer) -> R,
1082    ) -> R {
1083        options.retention = RetentionMode::RetainWhenInactive;
1084        self.pending_scope_options().replace(options);
1085        self.with_group(key, f)
1086    }
1087
1088    #[track_caller]
1089    pub fn with_key<K: Hash, R>(&self, key: &K, f: impl FnOnce(&Composer) -> R) -> R {
1090        let seed = explicit_group_key_seed(key, std::panic::Location::caller());
1091        self.with_group_seed(seed, f)
1092    }
1093
1094    fn dispose_detached_nodes(&self, nodes: impl IntoIterator<Item = NodeId>) {
1095        for node_id in nodes {
1096            self.commands_mut().push(Command::callback(move |applier| {
1097                crate::slot::dispose_detached_node_now(applier, node_id)
1098            }));
1099        }
1100    }
1101
1102    fn deactivate_scope_ids(&self, scope_ids: impl IntoIterator<Item = ScopeId>) {
1103        for scope_id in scope_ids {
1104            if let Some(scope) = self.scope_for_id(scope_id) {
1105                scope.deactivate();
1106            }
1107        }
1108    }
1109
1110    fn dispose_scope_ids(&self, scope_ids: impl IntoIterator<Item = ScopeId>) {
1111        for scope_id in scope_ids {
1112            if let Some(scope) = self.remove_scope(scope_id) {
1113                scope.deactivate();
1114            }
1115        }
1116    }
1117
1118    fn detached_root_parent_commands(
1119        &self,
1120        subtree: &crate::slot::DetachedSubtree,
1121        context: &'static str,
1122    ) -> Result<Vec<(NodeId, Option<NodeId>)>, NodeError> {
1123        let mut root_nodes = Vec::new();
1124        subtree.collect_root_nodes_checked_into(&mut root_nodes, context);
1125        let mut roots = Vec::with_capacity(root_nodes.len());
1126        for root in root_nodes {
1127            let parent_id = {
1128                let mut applier = self.borrow_applier();
1129                applier.get_mut(root)?.parent()
1130            };
1131            roots.push((root, parent_id));
1132        }
1133        Ok(roots)
1134    }
1135
1136    fn retain_detached_subtree_in_host(
1137        &self,
1138        slots_host: &Rc<SlotsHost>,
1139        parent_scope: Option<ScopeId>,
1140        subtree: crate::slot::DetachedSubtree,
1141    ) -> Result<(), NodeError> {
1142        // Retention and disposal must preserve the slot lifecycle contract in
1143        // docs/SLOT_TABLE_LIFECYCLE.md across the slot table, applier, and scope
1144        // registry.
1145        let Some(root_key) = subtree.root_key_checked() else {
1146            log::error!("retention rejected detached subtree without a root group");
1147            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1148            return Ok(());
1149        };
1150        let root_detaches = self.detached_root_parent_commands(&subtree, "retention")?;
1151        self.deactivate_scope_ids(subtree.scope_ids_iter());
1152        for (root, parent_id) in root_detaches {
1153            if let Some(parent_id) = parent_id {
1154                self.commands_mut().push(Command::DetachChild {
1155                    parent_id,
1156                    child_id: root,
1157                });
1158            }
1159        }
1160        let evicted = self.core.shared_state.insert_retained(
1161            slots_host,
1162            RetainKey {
1163                parent_scope,
1164                key: root_key,
1165            },
1166            subtree,
1167        );
1168        for subtree in evicted {
1169            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1170        }
1171        Ok(())
1172    }
1173
1174    fn evict_retained_subtrees_for_host(
1175        &self,
1176        slots_host: &Rc<SlotsHost>,
1177    ) -> Result<(), NodeError> {
1178        let evicted = self.core.shared_state.advance_retention_pass(slots_host);
1179        for subtree in evicted {
1180            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1181        }
1182        Ok(())
1183    }
1184
1185    fn dispose_detached_subtree_in_host(
1186        &self,
1187        slots_host: &Rc<SlotsHost>,
1188        subtree: crate::slot::DetachedSubtree,
1189    ) -> Result<(), NodeError> {
1190        let root_nodes = self
1191            .detached_root_parent_commands(&subtree, "disposal")?
1192            .into_iter()
1193            .map(|(root, _)| root);
1194        self.dispose_scope_ids(subtree.scope_ids_iter());
1195        self.dispose_detached_nodes(root_nodes);
1196        slots_host.with_table_and_lifecycle_mut(|table, lifecycle| {
1197            table.invalidate_detached_subtree_anchors(&subtree);
1198            lifecycle.queue_subtree_disposal(subtree);
1199        });
1200        Ok(())
1201    }
1202
1203    fn handle_detached_children_in_host(
1204        &self,
1205        slots_host: &Rc<SlotsHost>,
1206        parent_scope: Option<ScopeId>,
1207        detached: Vec<crate::slot::DetachedSubtree>,
1208    ) -> Result<(), NodeError> {
1209        for subtree in detached {
1210            let retention_mode = subtree
1211                .root_scope_id()
1212                .and_then(|scope_id| self.scope_for_id(scope_id))
1213                .map(|scope| scope.retention_mode())
1214                .unwrap_or_default();
1215            match retention_mode {
1216                RetentionMode::DisposeWhenInactive => {
1217                    self.dispose_detached_subtree_in_host(slots_host, subtree)?
1218                }
1219                RetentionMode::RetainWhenInactive => {
1220                    self.retain_detached_subtree_in_host(slots_host, parent_scope, subtree)?
1221                }
1222            }
1223        }
1224        Ok(())
1225    }
1226
1227    fn handle_detached_children(
1228        &self,
1229        parent_scope: Option<ScopeId>,
1230        detached: Vec<crate::slot::DetachedSubtree>,
1231    ) {
1232        let host = self.active_slots_host();
1233        if let Err(err) = self.handle_detached_children_in_host(&host, parent_scope, detached) {
1234            log::error!("detached subtree handling failed while closing a group: {err}");
1235        }
1236    }
1237
1238    fn handle_finished_group_result(
1239        &self,
1240        parent_scope: Option<ScopeId>,
1241        result: FinishGroupResult,
1242    ) {
1243        let FinishGroupResult {
1244            detached_children,
1245            direct_nodes,
1246            root_nodes,
1247            was_skipped,
1248        } = result;
1249        if was_skipped {
1250            self.attach_root_nodes(root_nodes);
1251        }
1252        self.dispose_detached_nodes(direct_nodes);
1253        self.handle_detached_children(parent_scope, detached_children);
1254    }
1255
1256    pub(crate) fn close_current_group_body_for_scope(&self, scope: &RecomposeScope) {
1257        let result = self.with_slot_session_mut(|slots| slots.finish_group_body());
1258        self.handle_finished_group_result(Some(scope.id()), result);
1259        if let Some(popped) = self.scope_stack().pop() {
1260            debug_assert_eq!(
1261                popped.id(),
1262                scope.id(),
1263                "closed scope must match the active scope stack"
1264            );
1265        } else {
1266            log::error!("scope stack underflow while closing scope {}", scope.id());
1267        }
1268    }
1269
1270    pub fn remember<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1271        self.remember_with_kind(PayloadKind::Remember, init)
1272    }
1273
1274    pub(crate) fn remember_internal<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1275        self.remember_with_kind(PayloadKind::Internal, init)
1276    }
1277
1278    pub(crate) fn remember_effect<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1279        self.remember_with_kind(PayloadKind::Effect, init)
1280    }
1281
1282    fn remember_with_kind<T: 'static>(
1283        &self,
1284        kind: PayloadKind,
1285        init: impl FnOnce() -> T,
1286    ) -> Owned<T> {
1287        self.with_slot_session_mut(|slots| slots.remember_with_kind(kind, init))
1288    }
1289
1290    pub fn use_value_slot<'pass, T: 'static>(
1291        &'pass self,
1292        init: impl FnOnce() -> T,
1293    ) -> ValueSlotHandle<'pass, T> {
1294        let slot = self
1295            .with_slot_session_mut(|slots| slots.value_slot_with_kind(PayloadKind::Internal, init));
1296        ValueSlotHandle::new(slot)
1297    }
1298
1299    #[doc(hidden)]
1300    pub fn __use_param_slot<'pass, T: 'static>(
1301        &'pass self,
1302        init: impl FnOnce() -> T,
1303    ) -> ValueSlotHandle<'pass, T> {
1304        let slot = self
1305            .with_slot_session_mut(|slots| slots.value_slot_with_kind(PayloadKind::Param, init));
1306        ValueSlotHandle::new(slot)
1307    }
1308
1309    #[doc(hidden)]
1310    pub fn __use_return_slot<'pass, T: 'static>(
1311        &'pass self,
1312        init: impl FnOnce() -> T,
1313    ) -> ValueSlotHandle<'pass, T> {
1314        let slot = self
1315            .with_slot_session_mut(|slots| slots.value_slot_with_kind(PayloadKind::Return, init));
1316        ValueSlotHandle::new(slot)
1317    }
1318
1319    #[doc(hidden)]
1320    pub fn __invalidate_return_consumer_scope(&self) {
1321        let Some(scope) = self.current_recranpose_scope() else {
1322            self.request_root_render();
1323            return;
1324        };
1325
1326        if let Some(target) = scope.callback_promotion_target() {
1327            target.invalidate();
1328        } else {
1329            self.request_root_render();
1330        }
1331    }
1332
1333    pub fn with_slot_value<'pass, T: 'static, R>(
1334        &'pass self,
1335        handle: ValueSlotHandle<'pass, T>,
1336        f: impl FnOnce(&T) -> R,
1337    ) -> R {
1338        self.with_slots(|slots| f(slots.read_value(handle.slot())))
1339    }
1340
1341    pub fn with_slot_value_mut<'pass, T: 'static, R>(
1342        &'pass self,
1343        handle: ValueSlotHandle<'pass, T>,
1344        f: impl FnOnce(&mut T) -> R,
1345    ) -> R {
1346        self.with_slots_mut(|slots| f(slots.read_value_mut(handle.slot())))
1347    }
1348
1349    pub fn mutable_state_of<T: Clone + 'static>(&self, initial: T) -> MutableState<T> {
1350        MutableState::with_runtime(initial, self.runtime_handle())
1351    }
1352
1353    pub fn mutable_state_list_of<T, I>(&self, values: I) -> SnapshotStateList<T>
1354    where
1355        T: Clone + 'static,
1356        I: IntoIterator<Item = T>,
1357    {
1358        SnapshotStateList::with_runtime(values, self.runtime_handle())
1359    }
1360
1361    pub fn mutable_state_map_of<K, V, I>(&self, pairs: I) -> SnapshotStateMap<K, V>
1362    where
1363        K: Clone + Eq + Hash + 'static,
1364        V: Clone + 'static,
1365        I: IntoIterator<Item = (K, V)>,
1366    {
1367        SnapshotStateMap::with_runtime(pairs, self.runtime_handle())
1368    }
1369
1370    pub fn read_composition_local<T: Clone + 'static>(&self, local: &CompositionLocal<T>) -> T {
1371        let stack = self.core.local_stack.borrow();
1372        for context in stack.iter().rev() {
1373            if let Some(entry) = context.values.get(&local.key) {
1374                match entry.clone().downcast::<LocalStateEntry<T>>() {
1375                    Ok(typed) => return typed.value(),
1376                    Err(_) => {
1377                        log::error!(
1378                            "composition local entry type mismatch for key {}",
1379                            local.key
1380                        );
1381                        return local.default_value();
1382                    }
1383                }
1384            }
1385        }
1386        local.default_value()
1387    }
1388
1389    pub fn read_static_composition_local<T: Clone + 'static>(
1390        &self,
1391        local: &StaticCompositionLocal<T>,
1392    ) -> T {
1393        let stack = self.core.local_stack.borrow();
1394        for context in stack.iter().rev() {
1395            if let Some(entry) = context.values.get(&local.key) {
1396                match entry.clone().downcast::<StaticLocalEntry<T>>() {
1397                    Ok(typed) => return typed.value(),
1398                    Err(_) => {
1399                        log::error!(
1400                            "static composition local entry type mismatch for key {}",
1401                            local.key
1402                        );
1403                        return local.default_value();
1404                    }
1405                }
1406            }
1407        }
1408        local.default_value()
1409    }
1410
1411    pub fn current_recranpose_scope(&self) -> Option<RecomposeScope> {
1412        self.core.scope_stack.borrow().last().cloned()
1413    }
1414
1415    pub(crate) fn current_state_invalidation_scope(&self) -> Option<RecomposeScope> {
1416        let stack = self.core.scope_stack.borrow();
1417        stack
1418            .iter()
1419            .rev()
1420            .find(|scope| scope.has_recompose_callback())
1421            .cloned()
1422            .or_else(|| stack.last().cloned())
1423    }
1424
1425    pub fn phase(&self) -> crate::Phase {
1426        self.core.phase.get()
1427    }
1428
1429    pub(crate) fn set_phase(&self, phase: crate::Phase) {
1430        self.core.phase.set(phase);
1431    }
1432
1433    pub fn enter_phase(&self, phase: crate::Phase) {
1434        self.set_phase(phase);
1435    }
1436
1437    pub(crate) fn subcompose<R>(
1438        &self,
1439        state: &mut SubcomposeState,
1440        slot_id: SlotId,
1441        content: impl FnOnce(&Composer) -> R,
1442    ) -> (R, Vec<NodeId>) {
1443        match self.phase() {
1444            crate::Phase::Measure | crate::Phase::Layout => {}
1445            current => panic!(
1446                "subcompose() may only be called during measure or layout; current phase: {:?}",
1447                current
1448            ),
1449        }
1450
1451        self.subcompose_stack().push(SubcomposeFrame::default());
1452        struct StackGuard {
1453            core: Rc<ComposerCore>,
1454            leaked: bool,
1455        }
1456        impl Drop for StackGuard {
1457            fn drop(&mut self) {
1458                if !self.leaked {
1459                    self.core.subcompose_stack.borrow_mut().pop();
1460                }
1461            }
1462        }
1463        let mut guard = StackGuard {
1464            core: self.clone_core(),
1465            leaked: false,
1466        };
1467
1468        let slot_host = state.get_or_create_slots(slot_id);
1469        let (result, _) = self.with_slot_override(slot_host.clone(), |composer| {
1470            composer.with_group(slot_id.raw(), |composer| content(composer))
1471        });
1472
1473        let frame = {
1474            let frame = take_subcompose_frame(&guard.core, "subcompose");
1475            guard.leaked = true;
1476            frame
1477        };
1478        let nodes = frame.nodes;
1479        let scopes = frame.scopes;
1480        state.register_active(slot_id, &nodes, &scopes);
1481        (result, nodes)
1482    }
1483
1484    pub fn subcompose_measurement<R>(
1485        &self,
1486        state: &mut SubcomposeState,
1487        slot_id: SlotId,
1488        content: impl FnOnce(&Composer) -> R,
1489    ) -> (R, Vec<NodeId>) {
1490        let (result, nodes) = self.subcompose(state, slot_id, content);
1491        let roots = nodes
1492            .into_iter()
1493            .filter(|&id| self.node_has_no_parent(id))
1494            .collect();
1495
1496        (result, roots)
1497    }
1498
1499    pub fn subcompose_in<R>(
1500        &self,
1501        slots: &Rc<SlotsHost>,
1502        root: Option<NodeId>,
1503        f: impl FnOnce(&Composer) -> R,
1504    ) -> Result<R, NodeError> {
1505        let runtime_handle = self.runtime_handle();
1506        let phase = self.phase();
1507        let locals = self.current_local_stack();
1508        let shared_state = slots
1509            .runtime_state()
1510            .unwrap_or_else(|| Rc::clone(&self.core.shared_state));
1511        let core = Rc::new(ComposerCore::new(
1512            shared_state,
1513            Rc::clone(slots),
1514            Rc::clone(&self.core.applier),
1515            runtime_handle.clone(),
1516            self.observer(),
1517            root,
1518            InitialParentFrame::RealParent,
1519        ));
1520        core.phase.set(phase);
1521        *core.local_stack.borrow_mut() = locals;
1522        let composer = Composer::from_core(core);
1523        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
1524            let (output, outcome) = composer.try_with_slot_host_pass(
1525                Rc::clone(slots),
1526                crate::slot::SlotPassMode::Compose,
1527                |composer| f(composer),
1528            )?;
1529            let commands = composer.take_commands();
1530            let side_effects = composer.take_side_effects();
1531            Ok((output, commands, side_effects, outcome.compacted))
1532        })?;
1533        {
1534            let mut applier = self.borrow_applier();
1535            commands.apply(&mut *applier)?;
1536            for update in runtime_handle.take_updates() {
1537                update.apply(&mut *applier)?;
1538            }
1539        }
1540        if compact_applier {
1541            self.core.applier.compact();
1542            self.core.applier.borrow_dyn().clear_recycled_nodes();
1543        }
1544        runtime_handle.drain_ui();
1545        for effect in side_effects {
1546            effect();
1547        }
1548        runtime_handle.drain_ui();
1549        Ok(result)
1550    }
1551
1552    /// Captures the composition context at the current point so work composed
1553    /// in another slot host inherits both locals and source ownership.
1554    ///
1555    /// A `SubcomposeLayout` captures this while it is being composed and replays
1556    /// it while subcomposing off the measure pass, so content that is
1557    /// subcomposed during layout observes the same composition locals as the
1558    /// `SubcomposeLayout` call site — matching Jetpack Compose, where a
1559    /// subcomposition inherits the composition locals of the layout that
1560    /// created it rather than whatever happens to be in scope during measure
1561    /// (which, after composition unwinds, no longer carries ancestor providers).
1562    pub fn capture_composition_context(&self) -> CapturedCompositionContext {
1563        CapturedCompositionContext {
1564            locals: self.current_local_stack(),
1565            owner_scope: self
1566                .current_recranpose_scope()
1567                .map(|scope| scope.downgrade()),
1568        }
1569    }
1570
1571    /// Subcomposes content using an isolated SlotsHost without resetting it.
1572    /// Unlike `subcompose_in`, this preserves existing slot state across calls,
1573    /// allowing efficient reuse during measurement passes. This is critical for
1574    /// lazy lists where items need stable slot positions.
1575    pub fn subcompose_slot<R>(
1576        &self,
1577        slots: &Rc<SlotsHost>,
1578        root: Option<NodeId>,
1579        f: impl FnOnce(&Composer) -> R,
1580    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
1581        let context = self.capture_composition_context();
1582        self.subcompose_slot_with_context(slots, root, &context, f)
1583    }
1584
1585    /// Like [`Composer::subcompose_slot`], but uses a context captured at the
1586    /// source composition site. This is required for measure-time composition,
1587    /// where the source scope is no longer on the active stack.
1588    pub fn subcompose_slot_with_context<R>(
1589        &self,
1590        slots: &Rc<SlotsHost>,
1591        root: Option<NodeId>,
1592        context: &CapturedCompositionContext,
1593        f: impl FnOnce(&Composer) -> R,
1594    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
1595        let runtime_handle = self.runtime_handle();
1596        let phase = self.phase();
1597        let locals = context.locals.clone();
1598        let shared_state = slots
1599            .runtime_state()
1600            .unwrap_or_else(|| Rc::clone(&self.core.shared_state));
1601        let core = Rc::new(ComposerCore::new(
1602            shared_state,
1603            Rc::clone(slots),
1604            Rc::clone(&self.core.applier),
1605            runtime_handle.clone(),
1606            self.observer(),
1607            root,
1608            InitialParentFrame::RealParent,
1609        ));
1610        core.phase.set(phase);
1611        *core.local_stack.borrow_mut() = locals;
1612        *core.subcomposition_owner_scope.borrow_mut() = context
1613            .owner_scope
1614            .as_ref()
1615            .and_then(Weak::upgrade)
1616            .map(|inner| RecomposeScope { inner });
1617        let composer = Composer::from_core(core);
1618        composer.subcompose_stack().push(SubcomposeFrame::default());
1619        struct StackGuard {
1620            core: Rc<ComposerCore>,
1621            leaked: bool,
1622        }
1623        impl Drop for StackGuard {
1624            fn drop(&mut self) {
1625                if !self.leaked {
1626                    self.core.subcompose_stack.borrow_mut().pop();
1627                }
1628            }
1629        }
1630        let mut guard = StackGuard {
1631            core: composer.clone_core(),
1632            leaked: false,
1633        };
1634        let root_group_key = crate::location_key(file!(), line!(), column!());
1635        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
1636            let (output, outcome) = composer.try_with_slot_host_pass(
1637                Rc::clone(slots),
1638                crate::slot::SlotPassMode::Compose,
1639                |composer| {
1640                    let output = composer.with_group(root_group_key, |composer| f(composer));
1641                    if root.is_some() {
1642                        composer.pop_parent();
1643                    }
1644                    output
1645                },
1646            )?;
1647            let commands = composer.take_commands();
1648            let side_effects = composer.take_side_effects();
1649            Ok((output, commands, side_effects, outcome.compacted))
1650        })?;
1651        let frame = {
1652            let frame = take_subcompose_frame(&guard.core, "subcompose_slot");
1653            guard.leaked = true;
1654            frame
1655        };
1656
1657        {
1658            let mut applier = self.borrow_applier();
1659            commands.apply(&mut *applier)?;
1660            for update in runtime_handle.take_updates() {
1661                update.apply(&mut *applier)?;
1662            }
1663        }
1664        if compact_applier {
1665            self.core.applier.compact();
1666            self.core.applier.borrow_dyn().clear_recycled_nodes();
1667        }
1668        runtime_handle.drain_ui();
1669        for effect in side_effects {
1670            effect();
1671        }
1672        runtime_handle.drain_ui();
1673        Ok((result, frame.scopes))
1674    }
1675
1676    fn attach_root_nodes(&self, root_nodes: Vec<NodeId>) {
1677        for id in root_nodes {
1678            self.attach_to_parent_with_mode(id, true);
1679        }
1680    }
1681
1682    pub fn skip_current_group(&self) {
1683        self.with_slot_session_mut(|slots| slots.skip_group());
1684    }
1685
1686    pub fn runtime_handle(&self) -> RuntimeHandle {
1687        self.core.runtime.clone()
1688    }
1689
1690    pub fn set_recranpose_callback<F>(&self, callback: F)
1691    where
1692        F: FnMut(&Composer) + 'static,
1693    {
1694        self.set_recranpose_callback_boxed(Box::new(callback));
1695    }
1696
1697    /// Monomorphic core (`inline(never)` so fat LTO keeps one copy): the
1698    /// observer wiring here used to be re-instantiated for every composable
1699    /// call site through the generic entry point above.
1700    #[inline(never)]
1701    fn set_recranpose_callback_boxed(&self, mut callback: Box<dyn FnMut(&Composer)>) {
1702        if let Some(scope) = self.current_recranpose_scope() {
1703            let observer = self.observer();
1704            let scope_weak = scope.downgrade();
1705            scope.set_recompose(Box::new(move |composer: &Composer| {
1706                if let Some(inner) = scope_weak.upgrade() {
1707                    let scope_instance = RecomposeScope { inner };
1708                    observer.observe_reads(
1709                        scope_instance.clone(),
1710                        move |scope_ref| scope_ref.invalidate(),
1711                        || {
1712                            callback(composer);
1713                        },
1714                    );
1715                }
1716            }));
1717        }
1718    }
1719
1720    pub fn set_recranpose_fn(&self, callback: fn(&Composer)) {
1721        if let Some(scope) = self.current_recranpose_scope() {
1722            scope.set_recompose_fn(callback);
1723        }
1724    }
1725
1726    pub fn with_composition_locals<R>(
1727        &self,
1728        provided: Vec<ProvidedValue>,
1729        f: impl FnOnce(&Composer) -> R,
1730    ) -> R {
1731        if provided.is_empty() {
1732            return f(self);
1733        }
1734        let mut context = LocalContext::default();
1735        for value in provided {
1736            let (key, entry) = value.into_entry(self);
1737            context.values.insert(key, entry);
1738        }
1739        {
1740            let mut stack = self.local_stack();
1741            Rc::make_mut(&mut *stack).push(context);
1742        }
1743        let result = f(self);
1744        {
1745            let mut stack = self.local_stack();
1746            Rc::make_mut(&mut *stack).pop();
1747        }
1748        result
1749    }
1750}