Skip to main content

cranpose_core/
composer.rs

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