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