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