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