Skip to main content

cranpose_core/
composer.rs

1use crate::collections::map::HashMap;
2use crate::{
3    composer_context, empty_local_stack, hash_key, runtime, Applier, ApplierHost, ChildList,
4    Command, CommandQueue, CompositionLocal, DirtyBubble, Key, LocalKey, LocalStackSnapshot,
5    LocalStateEntry, MutableState, Node, NodeError, NodeId, Owned, ProvidedValue, RecomposeOptions,
6    RecomposeScope, RecycledNode, RuntimeHandle, SlotId, SlotPassOutcome, SlotTable, SlotsHost,
7    SnapshotStateList, SnapshotStateMap, SnapshotStateObserver, StartScopedGroup,
8    StaticCompositionLocal, StaticLocalEntry, SubcomposeState, COMMAND_FLUSH_THRESHOLD,
9};
10use smallvec::SmallVec;
11use std::any::Any;
12use std::cell::{Cell, RefCell, RefMut};
13use std::hash::Hash;
14use std::marker::PhantomData;
15use std::rc::Rc;
16
17#[derive(Clone, Copy)]
18pub struct ValueSlotHandle<'pass> {
19    index: usize,
20    _pass: PhantomData<&'pass Composer>,
21}
22
23impl ValueSlotHandle<'_> {
24    pub(crate) fn new(index: usize) -> Self {
25        Self {
26            index,
27            _pass: PhantomData,
28        }
29    }
30
31    pub(crate) fn index(self) -> usize {
32        self.index
33    }
34}
35
36pub(crate) struct ParentFrame {
37    pub(crate) id: NodeId,
38    pub(crate) previous: ChildList,
39    pub(crate) new_children: ChildList,
40    pub(crate) attach_mode: ParentAttachMode,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub(crate) enum ParentAttachMode {
45    ImmediateAppend,
46    DeferredSync,
47}
48
49#[derive(Default)]
50pub(crate) struct SubcomposeFrame {
51    pub(crate) nodes: Vec<NodeId>,
52    pub(crate) scopes: Vec<RecomposeScope>,
53}
54
55#[derive(Default, Clone)]
56pub(crate) struct LocalContext {
57    pub(crate) values: HashMap<LocalKey, Rc<dyn Any>>,
58}
59
60pub(crate) struct ComposerCore {
61    pub(crate) slots: Rc<SlotsHost>,
62    slot_hosts: RefCell<Vec<Rc<SlotsHost>>>,
63    pub(crate) applier: Rc<dyn ApplierHost>,
64    pub(crate) runtime: RuntimeHandle,
65    pub(crate) observer: SnapshotStateObserver,
66    pub(crate) parent_stack: RefCell<Vec<ParentFrame>>,
67    pub(crate) subcompose_stack: RefCell<Vec<SubcomposeFrame>>,
68    pub(crate) root: Cell<Option<NodeId>>,
69    pub(crate) commands: RefCell<CommandQueue>,
70    pub(crate) scope_stack: RefCell<Vec<RecomposeScope>>,
71    pub(crate) local_stack: RefCell<LocalStackSnapshot>,
72    pub(crate) side_effects: RefCell<Vec<Box<dyn FnOnce()>>>,
73    pub(crate) pending_scope_options: RefCell<Option<RecomposeOptions>>,
74    pub(crate) phase: Cell<crate::Phase>,
75    pub(crate) last_node_reused: Cell<Option<bool>>,
76    pub(crate) recranpose_parent_hint: Cell<Option<NodeId>>,
77    pub(crate) root_render_requested: Cell<bool>,
78    pub(crate) _not_send: PhantomData<*const ()>,
79}
80
81impl ComposerCore {
82    pub(crate) fn new(
83        slots: Rc<SlotsHost>,
84        applier: Rc<dyn ApplierHost>,
85        runtime: RuntimeHandle,
86        observer: SnapshotStateObserver,
87        root: Option<NodeId>,
88    ) -> Self {
89        let parent_stack = if let Some(root_id) = root {
90            vec![ParentFrame {
91                id: root_id,
92                previous: ChildList::new(),
93                new_children: ChildList::new(),
94                attach_mode: ParentAttachMode::DeferredSync,
95            }]
96        } else {
97            Vec::new()
98        };
99
100        Self {
101            slots,
102            slot_hosts: RefCell::new(Vec::new()),
103            applier,
104            runtime,
105            observer,
106            parent_stack: RefCell::new(parent_stack),
107            subcompose_stack: RefCell::new(Vec::new()),
108            root: Cell::new(root),
109            commands: RefCell::new(CommandQueue::default()),
110            scope_stack: RefCell::new(Vec::new()),
111            local_stack: RefCell::new(empty_local_stack()),
112            side_effects: RefCell::new(Vec::new()),
113            pending_scope_options: RefCell::new(None),
114            phase: Cell::new(crate::Phase::Compose),
115            last_node_reused: Cell::new(None),
116            recranpose_parent_hint: Cell::new(None),
117            root_render_requested: Cell::new(false),
118            _not_send: PhantomData,
119        }
120    }
121}
122
123#[derive(Clone)]
124pub struct Composer {
125    pub(crate) core: Rc<ComposerCore>,
126}
127
128pub(crate) enum EmittedNode {
129    Fresh(Box<dyn Node>),
130    Recycled(RecycledNode),
131}
132
133impl Composer {
134    pub fn new(
135        slots: Rc<SlotsHost>,
136        applier: Rc<dyn ApplierHost>,
137        runtime: RuntimeHandle,
138        observer: SnapshotStateObserver,
139        root: Option<NodeId>,
140    ) -> Self {
141        let core = Rc::new(ComposerCore::new(slots, applier, runtime, observer, root));
142        Self { core }
143    }
144
145    pub(crate) fn from_core(core: Rc<ComposerCore>) -> Self {
146        Self { core }
147    }
148
149    pub(crate) fn clone_core(&self) -> Rc<ComposerCore> {
150        Rc::clone(&self.core)
151    }
152
153    fn observer(&self) -> SnapshotStateObserver {
154        self.core.observer.clone()
155    }
156
157    pub(crate) fn request_root_render(&self) {
158        self.core.root_render_requested.set(true);
159    }
160
161    pub(crate) fn take_root_render_request(&self) -> bool {
162        self.core.root_render_requested.replace(false)
163    }
164
165    pub(crate) fn observe_scope<R>(&self, scope: &RecomposeScope, block: impl FnOnce() -> R) -> R {
166        let observer = self.observer();
167        let scope_clone = scope.clone();
168        observer.observe_reads(scope_clone, move |scope_ref| scope_ref.invalidate(), block)
169    }
170
171    fn active_slots_host(&self) -> Rc<SlotsHost> {
172        self.core
173            .slot_hosts
174            .borrow()
175            .last()
176            .cloned()
177            .unwrap_or_else(|| Rc::clone(&self.core.slots))
178    }
179
180    pub(crate) fn with_slots<R>(&self, f: impl FnOnce(&SlotTable) -> R) -> R {
181        let host = self.active_slots_host();
182        let slots = host.borrow();
183        f(&slots)
184    }
185
186    pub(crate) fn with_slots_mut<R>(&self, f: impl FnOnce(&mut SlotTable) -> R) -> R {
187        let host = self.active_slots_host();
188        let mut slots = host.borrow_mut();
189        f(&mut slots)
190    }
191
192    pub(crate) fn with_slot_session_mut<R>(
193        &self,
194        f: impl FnOnce(&mut crate::slot_table::SlotWriteSession<'_>) -> R,
195    ) -> R {
196        self.active_slots_host().with_write_session(f)
197    }
198
199    pub(crate) fn with_slot_host_pass<R>(
200        &self,
201        slots: Rc<SlotsHost>,
202        mode: crate::slot_table::SlotPassMode,
203        f: impl FnOnce(&Composer) -> R,
204    ) -> (R, SlotPassOutcome) {
205        slots.begin_pass(mode);
206        self.core.slot_hosts.borrow_mut().push(Rc::clone(&slots));
207
208        struct Guard {
209            core: Rc<ComposerCore>,
210            host: Rc<SlotsHost>,
211            outcome: Rc<RefCell<Option<SlotPassOutcome>>>,
212        }
213        impl Drop for Guard {
214            fn drop(&mut self) {
215                let host = self
216                    .core
217                    .slot_hosts
218                    .borrow_mut()
219                    .pop()
220                    .expect("slot host underflow");
221                debug_assert!(Rc::ptr_eq(&host, &self.host));
222
223                let outcome = {
224                    let mut applier = self.core.applier.borrow_dyn();
225                    self.host.finish_pass(&mut *applier)
226                };
227
228                if outcome.compacted {
229                    self.core.applier.compact();
230                    self.core.applier.borrow_dyn().clear_recycled_nodes();
231                }
232
233                *self.outcome.borrow_mut() = Some(outcome);
234            }
235        }
236        let outcome = Rc::new(RefCell::new(None));
237        let guard = Guard {
238            core: self.clone_core(),
239            host: Rc::clone(&slots),
240            outcome: Rc::clone(&outcome),
241        };
242        let result = f(self);
243        drop(guard);
244        let outcome = outcome
245            .borrow_mut()
246            .take()
247            .expect("slot pass should produce an outcome");
248        (result, outcome)
249    }
250
251    pub(crate) fn with_slot_override<R>(
252        &self,
253        slots: Rc<SlotsHost>,
254        f: impl FnOnce(&Composer) -> R,
255    ) -> (R, SlotPassOutcome) {
256        self.with_slot_host_pass(slots, crate::slot_table::SlotPassMode::Compose, f)
257    }
258
259    pub(crate) fn parent_stack(&self) -> RefMut<'_, Vec<ParentFrame>> {
260        self.core.parent_stack.borrow_mut()
261    }
262
263    pub(crate) fn subcompose_stack(&self) -> RefMut<'_, Vec<SubcomposeFrame>> {
264        self.core.subcompose_stack.borrow_mut()
265    }
266
267    pub(crate) fn commands_mut(&self) -> RefMut<'_, CommandQueue> {
268        self.core.commands.borrow_mut()
269    }
270
271    pub(crate) fn enqueue_semantics_invalidation(&self, id: NodeId) {
272        self.commands_mut().push(Command::BubbleDirty {
273            node_id: id,
274            bubble: DirtyBubble::SEMANTICS,
275        });
276    }
277
278    pub(crate) fn scope_stack(&self) -> RefMut<'_, Vec<RecomposeScope>> {
279        self.core.scope_stack.borrow_mut()
280    }
281
282    pub(crate) fn local_stack(&self) -> RefMut<'_, LocalStackSnapshot> {
283        self.core.local_stack.borrow_mut()
284    }
285
286    pub(crate) fn current_local_stack(&self) -> LocalStackSnapshot {
287        self.core.local_stack.borrow().clone()
288    }
289
290    pub(crate) fn side_effects_mut(&self) -> RefMut<'_, Vec<Box<dyn FnOnce()>>> {
291        self.core.side_effects.borrow_mut()
292    }
293
294    fn pending_scope_options(&self) -> RefMut<'_, Option<RecomposeOptions>> {
295        self.core.pending_scope_options.borrow_mut()
296    }
297
298    pub(crate) fn borrow_applier(&self) -> RefMut<'_, dyn Applier> {
299        self.core.applier.borrow_dyn()
300    }
301
302    /// Registers a virtual node in the Applier.
303    ///
304    /// This is used by SubcomposeLayoutNode to register virtual container nodes
305    /// so that subsequent insert_child commands can find them and attach children.
306    /// Without this, virtual nodes would only exist in SubcomposeLayoutNodeInner.virtual_nodes
307    /// and applier.get_mut(virtual_node_id) would fail, breaking child attachment.
308    pub fn register_virtual_node(
309        &self,
310        node_id: NodeId,
311        node: Box<dyn Node>,
312    ) -> Result<(), NodeError> {
313        let mut applier = self.borrow_applier();
314        applier.insert_with_id(node_id, node)
315    }
316
317    /// Checks if a node has no parent (is a root node).
318    /// Used by SubcomposeMeasureScope to filter subcompose results.
319    pub fn node_has_no_parent(&self, node_id: NodeId) -> bool {
320        let mut applier = self.borrow_applier();
321        match applier.get_mut(node_id) {
322            Ok(node) => node.parent().is_none(),
323            Err(_) => true,
324        }
325    }
326
327    /// Gets the children of a node from the Applier.
328    ///
329    /// This is used by SubcomposeLayoutNode to get children of virtual nodes
330    /// directly from the Applier, where insert_child commands have been applied.
331    pub fn get_node_children(&self, node_id: NodeId) -> SmallVec<[NodeId; 8]> {
332        let mut applier = self.borrow_applier();
333        match applier.get_mut(node_id) {
334            Ok(node) => {
335                let mut children = SmallVec::<[NodeId; 8]>::new();
336                node.collect_children_into(&mut children);
337                children
338            }
339            Err(_) => SmallVec::<[NodeId; 8]>::new(),
340        }
341    }
342
343    /// Records a child node in the current parent frame's expected children list.
344    ///
345    /// Used by SubcomposeLayout's `perform_subcompose` to register virtual nodes
346    /// with the outer composer's parent frame. This ensures that the `pop_parent`
347    /// call at the end of `subcompose_slot` generates a correct `SyncChildren`
348    /// command that preserves (rather than removes) the virtual nodes.
349    ///
350    /// Without this, `pop_parent` would generate `SyncChildren { expected: [] }`,
351    /// which removes all virtual nodes and their subtrees from the applier.
352    pub fn record_subcompose_child(&self, child_id: NodeId) {
353        let mut parent_stack = self.parent_stack();
354        if let Some(frame) = parent_stack.last_mut() {
355            if matches!(frame.attach_mode, ParentAttachMode::DeferredSync)
356                && !frame.new_children.contains(&child_id)
357            {
358                frame.new_children.push(child_id);
359            }
360        }
361    }
362
363    /// Clears all children of a node in the Applier.
364    ///
365    /// This is used by SubcomposeLayoutNode when reusing a virtual node for
366    /// different content. Without clearing, old children remain attached,
367    /// causing duplicate/interleaved items in lazy lists after scrolling.
368    pub fn clear_node_children(&self, node_id: NodeId) {
369        let mut applier = self.borrow_applier();
370        if let Ok(node) = applier.get_mut(node_id) {
371            node.update_children(&[]);
372        }
373    }
374
375    pub fn install<R>(&self, f: impl FnOnce(&Composer) -> R) -> R {
376        let _composer_guard = composer_context::enter(self);
377        runtime::push_active_runtime(&self.core.runtime);
378        struct Guard;
379        impl Drop for Guard {
380            fn drop(&mut self) {
381                runtime::pop_active_runtime();
382            }
383        }
384        let guard = Guard;
385        let result = f(self);
386        drop(guard);
387        result
388    }
389
390    pub(crate) fn flush_pending_commands_if_large(&self) {
391        let queued = self.core.commands.borrow().len();
392        if queued < COMMAND_FLUSH_THRESHOLD {
393            return;
394        }
395        self.apply_pending_commands()
396            .expect("mid-composition command flush failed");
397    }
398
399    fn with_group_in_active_pass<R>(&self, key: Key, f: impl FnOnce(&Composer) -> R) -> R {
400        struct GroupGuard {
401            composer: Composer,
402            scope: RecomposeScope,
403        }
404
405        impl Drop for GroupGuard {
406            fn drop(&mut self) {
407                let trimmed = self
408                    .composer
409                    .with_slot_session_mut(|slots| slots.finalize_current_group());
410                if trimmed {
411                    self.scope.force_recompose();
412                }
413                self.composer.scope_stack().pop();
414                self.scope.mark_recomposed();
415                self.composer
416                    .with_slot_session_mut(|slots| slots.end_group());
417                self.composer.flush_pending_commands_if_large();
418            }
419        }
420
421        let parent_scope = self.current_recranpose_scope();
422        let (group_anchor, scope_ref, requires_recompose) = self.with_slot_session_mut(|slots| {
423            let StartScopedGroup {
424                anchor,
425                scope,
426                requires_recompose,
427                ..
428            } = slots.begin_scoped_group(key, || RecomposeScope::new(self.runtime_handle()));
429            (anchor, scope, requires_recompose)
430        });
431
432        scope_ref.reactivate();
433        scope_ref.set_group_anchor(group_anchor);
434        scope_ref.set_parent_scope(parent_scope);
435
436        if let Some(options) = self.pending_scope_options().take() {
437            if options.force_recompose {
438                scope_ref.force_recompose();
439            } else if options.force_reuse {
440                scope_ref.force_reuse();
441            }
442        }
443        if requires_recompose {
444            scope_ref.force_recompose();
445        }
446
447        let slots_host = self.active_slots_host();
448        scope_ref.set_slots_host(Rc::downgrade(&slots_host));
449
450        {
451            let mut stack = self.scope_stack();
452            stack.push(scope_ref.clone());
453        }
454
455        {
456            let mut stack = self.subcompose_stack();
457            if let Some(frame) = stack.last_mut() {
458                frame.scopes.push(scope_ref.clone());
459            }
460        }
461
462        scope_ref.snapshot_locals(self.current_local_stack());
463        {
464            let parent_hint = self.parent_stack().last().map(|frame| frame.id);
465            scope_ref.set_parent_hint(parent_hint);
466        }
467
468        let guard = GroupGuard {
469            composer: self.clone(),
470            scope: scope_ref.clone(),
471        };
472        let result = self.observe_scope(&scope_ref, || f(self));
473        scope_ref.mark_composed_once();
474        drop(guard);
475        result
476    }
477
478    pub fn with_group<R>(&self, key: Key, f: impl FnOnce(&Composer) -> R) -> R {
479        let host = self.active_slots_host();
480        if host.has_active_pass() {
481            return self.with_group_in_active_pass(key, f);
482        }
483        let (result, _) =
484            self.with_slot_host_pass(host, crate::slot_table::SlotPassMode::Compose, |composer| {
485                composer.with_group_in_active_pass(key, f)
486            });
487        result
488    }
489
490    pub fn cranpose_with_reuse<R>(
491        &self,
492        key: Key,
493        options: RecomposeOptions,
494        f: impl FnOnce(&Composer) -> R,
495    ) -> R {
496        self.pending_scope_options().replace(options);
497        self.with_group(key, f)
498    }
499
500    pub fn with_key<K: Hash, R>(&self, key: &K, f: impl FnOnce(&Composer) -> R) -> R {
501        let hashed = hash_key(key);
502        self.with_group(hashed, f)
503    }
504
505    pub fn remember<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
506        self.with_slot_session_mut(|slots| slots.remember(init))
507    }
508
509    pub fn use_value_slot<'pass, T: 'static>(
510        &'pass self,
511        init: impl FnOnce() -> T,
512    ) -> ValueSlotHandle<'pass> {
513        let index = self.with_slot_session_mut(|slots| slots.use_value_slot(init));
514        ValueSlotHandle::new(index)
515    }
516
517    pub fn with_slot_value<'pass, T: 'static, R>(
518        &'pass self,
519        handle: ValueSlotHandle<'pass>,
520        f: impl FnOnce(&T) -> R,
521    ) -> R {
522        self.with_slots(|slots| f(slots.read_value(handle.index())))
523    }
524
525    pub fn with_slot_value_mut<'pass, T: 'static, R>(
526        &'pass self,
527        handle: ValueSlotHandle<'pass>,
528        f: impl FnOnce(&mut T) -> R,
529    ) -> R {
530        self.with_slots_mut(|slots| f(slots.read_value_mut(handle.index())))
531    }
532
533    pub fn write_slot_value<'pass, T: 'static>(
534        &'pass self,
535        handle: ValueSlotHandle<'pass>,
536        value: T,
537    ) {
538        self.with_slots_mut(|slots| slots.write_value(handle.index(), value));
539    }
540
541    pub fn mutable_state_of<T: Clone + 'static>(&self, initial: T) -> MutableState<T> {
542        MutableState::with_runtime(initial, self.runtime_handle())
543    }
544
545    pub fn mutable_state_list_of<T, I>(&self, values: I) -> SnapshotStateList<T>
546    where
547        T: Clone + 'static,
548        I: IntoIterator<Item = T>,
549    {
550        SnapshotStateList::with_runtime(values, self.runtime_handle())
551    }
552
553    pub fn mutable_state_map_of<K, V, I>(&self, pairs: I) -> SnapshotStateMap<K, V>
554    where
555        K: Clone + Eq + Hash + 'static,
556        V: Clone + 'static,
557        I: IntoIterator<Item = (K, V)>,
558    {
559        SnapshotStateMap::with_runtime(pairs, self.runtime_handle())
560    }
561
562    pub fn read_composition_local<T: Clone + 'static>(&self, local: &CompositionLocal<T>) -> T {
563        let stack = self.core.local_stack.borrow();
564        for context in stack.iter().rev() {
565            if let Some(entry) = context.values.get(&local.key) {
566                let typed = entry
567                    .clone()
568                    .downcast::<LocalStateEntry<T>>()
569                    .expect("composition local type mismatch");
570                return typed.value();
571            }
572        }
573        local.default_value()
574    }
575
576    pub fn read_static_composition_local<T: Clone + 'static>(
577        &self,
578        local: &StaticCompositionLocal<T>,
579    ) -> T {
580        let stack = self.core.local_stack.borrow();
581        for context in stack.iter().rev() {
582            if let Some(entry) = context.values.get(&local.key) {
583                let typed = entry
584                    .clone()
585                    .downcast::<StaticLocalEntry<T>>()
586                    .expect("static composition local type mismatch");
587                return typed.value();
588            }
589        }
590        local.default_value()
591    }
592
593    pub fn current_recranpose_scope(&self) -> Option<RecomposeScope> {
594        self.core.scope_stack.borrow().last().cloned()
595    }
596
597    pub fn phase(&self) -> crate::Phase {
598        self.core.phase.get()
599    }
600
601    pub(crate) fn set_phase(&self, phase: crate::Phase) {
602        self.core.phase.set(phase);
603    }
604
605    pub fn enter_phase(&self, phase: crate::Phase) {
606        self.set_phase(phase);
607    }
608
609    pub(crate) fn subcompose<R>(
610        &self,
611        state: &mut SubcomposeState,
612        slot_id: SlotId,
613        content: impl FnOnce(&Composer) -> R,
614    ) -> (R, Vec<NodeId>) {
615        match self.phase() {
616            crate::Phase::Measure | crate::Phase::Layout => {}
617            current => panic!(
618                "subcompose() may only be called during measure or layout; current phase: {:?}",
619                current
620            ),
621        }
622
623        self.subcompose_stack().push(SubcomposeFrame::default());
624        struct StackGuard {
625            core: Rc<ComposerCore>,
626            leaked: bool,
627        }
628        impl Drop for StackGuard {
629            fn drop(&mut self) {
630                if !self.leaked {
631                    self.core.subcompose_stack.borrow_mut().pop();
632                }
633            }
634        }
635        let mut guard = StackGuard {
636            core: self.clone_core(),
637            leaked: false,
638        };
639
640        let slot_host = state.get_or_create_slots(slot_id);
641        let (result, _) = self.with_slot_override(slot_host.clone(), |composer| {
642            composer.with_group(slot_id.raw(), |composer| content(composer))
643        });
644
645        let frame = {
646            let mut stack = guard.core.subcompose_stack.borrow_mut();
647            let frame = stack.pop().expect("subcompose stack underflow");
648            guard.leaked = true;
649            frame
650        };
651        let nodes = frame.nodes;
652        let scopes = frame.scopes;
653        state.register_active(slot_id, &nodes, &scopes);
654        (result, nodes)
655    }
656
657    pub fn subcompose_measurement<R>(
658        &self,
659        state: &mut SubcomposeState,
660        slot_id: SlotId,
661        content: impl FnOnce(&Composer) -> R,
662    ) -> (R, Vec<NodeId>) {
663        let (result, nodes) = self.subcompose(state, slot_id, content);
664        let roots = nodes
665            .into_iter()
666            .filter(|&id| self.node_has_no_parent(id))
667            .collect();
668
669        (result, roots)
670    }
671
672    pub fn subcompose_in<R>(
673        &self,
674        slots: &Rc<SlotsHost>,
675        root: Option<NodeId>,
676        f: impl FnOnce(&Composer) -> R,
677    ) -> Result<R, NodeError> {
678        let runtime_handle = self.runtime_handle();
679        let phase = self.phase();
680        let locals = self.current_local_stack();
681        let core = Rc::new(ComposerCore::new(
682            Rc::clone(slots),
683            Rc::clone(&self.core.applier),
684            runtime_handle.clone(),
685            self.observer(),
686            root,
687        ));
688        core.phase.set(phase);
689        *core.local_stack.borrow_mut() = locals;
690        let composer = Composer::from_core(core);
691        let (result, commands, side_effects) = composer.install(|composer| {
692            let (output, _) = composer.with_slot_host_pass(
693                Rc::clone(slots),
694                crate::slot_table::SlotPassMode::Compose,
695                |composer| f(composer),
696            );
697            let commands = composer.take_commands();
698            let side_effects = composer.take_side_effects();
699            (output, commands, side_effects)
700        });
701        {
702            let mut applier = self.borrow_applier();
703            commands.apply(&mut *applier)?;
704            for update in runtime_handle.take_updates() {
705                update.apply(&mut *applier)?;
706            }
707        }
708        runtime_handle.drain_ui();
709        for effect in side_effects {
710            effect();
711        }
712        runtime_handle.drain_ui();
713        Ok(result)
714    }
715
716    /// Subcomposes content using an isolated SlotsHost without resetting it.
717    /// Unlike `subcompose_in`, this preserves existing slot state across calls,
718    /// allowing efficient reuse during measurement passes. This is critical for
719    /// lazy lists where items need stable slot positions.
720    pub fn subcompose_slot<R>(
721        &self,
722        slots: &Rc<SlotsHost>,
723        root: Option<NodeId>,
724        f: impl FnOnce(&Composer) -> R,
725    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
726        let runtime_handle = self.runtime_handle();
727        let phase = self.phase();
728        let locals = self.current_local_stack();
729        let core = Rc::new(ComposerCore::new(
730            Rc::clone(slots),
731            Rc::clone(&self.core.applier),
732            runtime_handle.clone(),
733            self.observer(),
734            root,
735        ));
736        core.phase.set(phase);
737        *core.local_stack.borrow_mut() = locals;
738        let composer = Composer::from_core(core);
739        composer.subcompose_stack().push(SubcomposeFrame::default());
740        struct StackGuard {
741            core: Rc<ComposerCore>,
742            leaked: bool,
743        }
744        impl Drop for StackGuard {
745            fn drop(&mut self) {
746                if !self.leaked {
747                    self.core.subcompose_stack.borrow_mut().pop();
748                }
749            }
750        }
751        let mut guard = StackGuard {
752            core: composer.clone_core(),
753            leaked: false,
754        };
755        let root_group_key = crate::location_key(file!(), line!(), column!());
756        let (result, commands, side_effects) = composer.install(|composer| {
757            let (output, _) = composer.with_slot_host_pass(
758                Rc::clone(slots),
759                crate::slot_table::SlotPassMode::Compose,
760                |composer| {
761                    let output = composer.with_group(root_group_key, |composer| f(composer));
762                    if root.is_some() {
763                        composer.pop_parent();
764                    }
765                    output
766                },
767            );
768            let commands = composer.take_commands();
769            let side_effects = composer.take_side_effects();
770            (output, commands, side_effects)
771        });
772        let frame = {
773            let mut stack = guard.core.subcompose_stack.borrow_mut();
774            let frame = stack.pop().expect("subcompose stack underflow");
775            guard.leaked = true;
776            frame
777        };
778
779        {
780            let mut applier = self.borrow_applier();
781            commands.apply(&mut *applier)?;
782            for update in runtime_handle.take_updates() {
783                update.apply(&mut *applier)?;
784            }
785        }
786        runtime_handle.drain_ui();
787        for effect in side_effects {
788            effect();
789        }
790        runtime_handle.drain_ui();
791        Ok((result, frame.scopes))
792    }
793
794    pub(crate) fn skipped_group_root_nodes(&self, nodes: &[NodeId]) -> Vec<NodeId> {
795        let node_set: std::collections::HashSet<NodeId> = nodes.iter().copied().collect();
796        let mut applier = self.borrow_applier();
797        nodes
798            .iter()
799            .copied()
800            .filter(|id| {
801                let parent = applier.get_mut(*id).ok().and_then(|node| node.parent());
802                parent.is_none_or(|parent_id| !node_set.contains(&parent_id))
803            })
804            .collect()
805    }
806
807    pub fn skip_current_group(&self) {
808        let nodes = self.with_slot_session_mut(|slots| slots.nodes_in_current_group());
809        let root_nodes = self.skipped_group_root_nodes(&nodes);
810        self.with_slot_session_mut(|slots| slots.skip_current_group());
811        for id in root_nodes {
812            self.attach_to_parent_with_mode(id, true);
813        }
814    }
815
816    pub fn runtime_handle(&self) -> RuntimeHandle {
817        self.core.runtime.clone()
818    }
819
820    pub fn set_recranpose_callback<F>(&self, callback: F)
821    where
822        F: FnMut(&Composer) + 'static,
823    {
824        if let Some(scope) = self.current_recranpose_scope() {
825            let observer = self.observer();
826            let scope_weak = scope.downgrade();
827            let mut callback = callback;
828            scope.set_recompose(Box::new(move |composer: &Composer| {
829                if let Some(inner) = scope_weak.upgrade() {
830                    let scope_instance = RecomposeScope { inner };
831                    observer.observe_reads(
832                        scope_instance.clone(),
833                        move |scope_ref| scope_ref.invalidate(),
834                        || {
835                            callback(composer);
836                        },
837                    );
838                }
839            }));
840        }
841    }
842
843    pub fn set_recranpose_fn(&self, callback: fn(&Composer)) {
844        if let Some(scope) = self.current_recranpose_scope() {
845            scope.set_recompose_fn(callback);
846        }
847    }
848
849    pub fn with_composition_locals<R>(
850        &self,
851        provided: Vec<ProvidedValue>,
852        f: impl FnOnce(&Composer) -> R,
853    ) -> R {
854        if provided.is_empty() {
855            return f(self);
856        }
857        let mut context = LocalContext::default();
858        for value in provided {
859            let (key, entry) = value.into_entry(self);
860            context.values.insert(key, entry);
861        }
862        {
863            let mut stack = self.local_stack();
864            Rc::make_mut(&mut *stack).push(context);
865        }
866        let result = f(self);
867        {
868            let mut stack = self.local_stack();
869            Rc::make_mut(&mut *stack).pop();
870        }
871        result
872    }
873}