Skip to main content

cranpose_ui/
subcompose_layout.rs

1use std::{
2    cell::{Cell, Ref, RefCell, RefMut},
3    collections::HashMap,
4    rc::Rc,
5};
6
7use cranpose_core::{
8    Composer, NodeError, NodeId, Phase, SlotId, SlotTable, SlotsHost, SubcomposeState,
9};
10use cranpose_foundation::{
11    InvalidationKind, ModifierInvalidation, ModifierInvalidations, NodeCapabilities,
12    SemanticsConfiguration,
13};
14pub use cranpose_ui_layout::{Constraints, MeasureResult, Placement};
15use smallvec::SmallVec;
16use web_time::Instant;
17
18use crate::{
19    layout::MeasuredNode,
20    modifier::{Modifier, ModifierChainHandle, ModifierNodeSlices, Point, ResolvedModifiers, Size},
21    widgets::nodes::{
22        DescendantDirt, LayoutNode, LayoutNodeCacheHandles, LayoutState, allocate_virtual_node_id,
23    },
24};
25
26fn subcompose_telemetry_enabled() -> bool {
27    cranpose_core::env_flag!("CRANPOSE_SUBCOMPOSE_TELEMETRY")
28}
29
30pub(crate) enum RetainedSlotChildren {
31    Clean,
32    Remeasure {
33        children: Vec<SubcomposeChild>,
34        children_match: bool,
35    },
36}
37
38#[derive(Clone, Copy, Debug)]
39pub struct SubcomposeChild {
40    node_id: NodeId,
41    measured_size: Option<Size>,
42}
43
44impl SubcomposeChild {
45    pub fn new(node_id: NodeId) -> Self {
46        Self {
47            node_id,
48            measured_size: None,
49        }
50    }
51
52    pub fn with_size(node_id: NodeId, size: Size) -> Self {
53        Self {
54            node_id,
55            measured_size: Some(size),
56        }
57    }
58
59    pub fn node_id(&self) -> NodeId {
60        self.node_id
61    }
62
63    pub fn size(&self) -> Size {
64        self.measured_size.unwrap_or(Size {
65            width: 0.0,
66            height: 0.0,
67        })
68    }
69
70    pub fn width(&self) -> f32 {
71        self.size().width
72    }
73
74    pub fn height(&self) -> f32 {
75        self.size().height
76    }
77
78    pub fn set_size(&mut self, size: Size) {
79        self.measured_size = Some(size);
80    }
81}
82
83impl PartialEq for SubcomposeChild {
84    fn eq(&self, other: &Self) -> bool {
85        self.node_id == other.node_id
86    }
87}
88
89pub type SubcomposePlaceable = cranpose_ui_layout::Placeable;
90
91type CachedMeasureBatchRegistrar<'a> =
92    Box<dyn FnMut(&[NodeId], Constraints, &mut Vec<Option<Size>>) + 'a>;
93type RetainedMeasureLookup<'a> = Box<dyn FnMut(NodeId) -> Option<Rc<MeasuredNode>> + 'a>;
94type RetainedMeasureRegistrar<'a> = Box<dyn FnMut(&[Rc<MeasuredNode>]) + 'a>;
95
96pub(crate) struct CachedBatchMeasureInputs<'a> {
97    pub(crate) measurer: Box<dyn FnMut(NodeId, Constraints) -> SubcomposePlaceable + 'a>,
98    pub(crate) cached_measure_batch_registrar: CachedMeasureBatchRegistrar<'a>,
99    pub(crate) retained_measure_lookup: RetainedMeasureLookup<'a>,
100    pub(crate) retained_measure_registrar: RetainedMeasureRegistrar<'a>,
101    pub(crate) error: &'a RefCell<Option<NodeError>>,
102}
103
104/// Base trait for measurement scopes.
105pub trait SubcomposeLayoutScope: cranpose_ui_layout::MeasureScope {
106    fn constraints(&self) -> Constraints;
107
108    fn layout<I>(&mut self, width: f32, height: f32, placements: I) -> MeasureResult
109    where
110        I: IntoIterator<Item = Placement>,
111    {
112        MeasureResult::new(Size { width, height }, placements.into_iter().collect())
113    }
114}
115
116/// Public trait exposed to measure policies for subcomposition.
117pub trait SubcomposeMeasureScope: SubcomposeLayoutScope {
118    /// Composes `content` into the slot, or reuses the retained composition
119    /// when it is provably current.
120    ///
121    /// `key` must carry every value that flows into `content` from the
122    /// measure policy itself rather than from reactive state — a scaffold's
123    /// computed padding, a box's constraints. Such values never invalidate a
124    /// recompose scope when they change, so an equal key is the caller's
125    /// promise that the retained composition is not stale on that channel.
126    /// Values read from reactive state inside `content` need no key entry:
127    /// their writes invalidate the slot's scopes and block reuse. Content
128    /// must not read a non-reactive container (`Cell`, `RefCell`) for a value
129    /// that changes between measure passes unless that value is part of
130    /// `key`; debug builds recompose skipped slots and panic when the
131    /// retained topology diverges from a fresh composition.
132    fn subcompose<K, Content>(
133        &mut self,
134        slot_id: SlotId,
135        key: K,
136        content: Content,
137    ) -> Vec<SubcomposeChild>
138    where
139        K: PartialEq + 'static,
140        Content: FnMut() + 'static;
141
142    /// Measures a subcomposed child with the given constraints.
143    fn measure(&mut self, child: SubcomposeChild, constraints: Constraints) -> SubcomposePlaceable;
144
145    /// Checks if a node has no parent (is a root node).
146    /// Used to filter subcompose results to only include true root nodes.
147    fn node_has_no_parent(&self, node_id: NodeId) -> bool;
148}
149
150/// Concrete implementation of [`SubcomposeMeasureScope`].
151pub struct SubcomposeMeasureScopeImpl<'a> {
152    composer: Composer,
153    density_scope: crate::density::DensityMeasureScope,
154    state: &'a mut SubcomposeState,
155    constraints: Constraints,
156    measurer: Box<dyn FnMut(NodeId, Constraints) -> SubcomposePlaceable + 'a>,
157    cached_measure_batch_registrar: CachedMeasureBatchRegistrar<'a>,
158    retained_measure_lookup: RetainedMeasureLookup<'a>,
159    retained_measure_registrar: RetainedMeasureRegistrar<'a>,
160    error: &'a RefCell<Option<NodeError>>,
161    parent_handle: SubcomposeLayoutNodeHandle,
162    root_id: NodeId,
163    placement_scratch: Vec<Placement>,
164    cached_measure_node_scratch: Vec<NodeId>,
165    cached_measure_size_scratch: Vec<Option<Size>>,
166    cached_measure_missing_scratch: Vec<NodeId>,
167    registered_measurement_node_ids: Vec<NodeId>,
168    pending_commands_applied: bool,
169    #[cfg(debug_assertions)]
170    shadow_stash: Option<(Vec<NodeId>, Vec<NodeId>)>,
171}
172
173thread_local! {
174    static CLEAN_SLOT_SKIPS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
175}
176
177fn record_clean_slot_skip() {
178    CLEAN_SLOT_SKIPS.with(|count| count.set(count.get() + 1));
179}
180
181/// Number of subcompose measure passes on this thread that reused a retained
182/// slot without a compose walk. Diagnostic surface for tests and telemetry;
183/// see [`SubcomposeMeasureScope::subcompose`] for when a slot may skip.
184pub fn clean_slot_skip_count() -> u64 {
185    CLEAN_SLOT_SKIPS.with(Cell::get)
186}
187
188struct SubcomposeMeasureScopeInit<'a> {
189    composer: Composer,
190    density: crate::density::Density,
191    state: &'a mut SubcomposeState,
192    constraints: Constraints,
193    measurer: Box<dyn FnMut(NodeId, Constraints) -> SubcomposePlaceable + 'a>,
194    cached_measure_batch_registrar: CachedMeasureBatchRegistrar<'a>,
195    retained_measure_lookup: RetainedMeasureLookup<'a>,
196    retained_measure_registrar: RetainedMeasureRegistrar<'a>,
197    error: &'a RefCell<Option<NodeError>>,
198    parent_handle: SubcomposeLayoutNodeHandle,
199    root_id: NodeId,
200    placement_scratch: Vec<Placement>,
201}
202
203impl<'a> SubcomposeMeasureScopeImpl<'a> {
204    pub(crate) fn root_id(&self) -> NodeId {
205        self.root_id
206    }
207
208    fn new(init: SubcomposeMeasureScopeInit<'a>) -> Self {
209        Self {
210            composer: init.composer,
211            density_scope: crate::density::DensityMeasureScope::new(init.density),
212            state: init.state,
213            constraints: init.constraints,
214            measurer: init.measurer,
215            cached_measure_batch_registrar: init.cached_measure_batch_registrar,
216            retained_measure_lookup: init.retained_measure_lookup,
217            retained_measure_registrar: init.retained_measure_registrar,
218            error: init.error,
219            parent_handle: init.parent_handle,
220            root_id: init.root_id,
221            placement_scratch: init.placement_scratch,
222            cached_measure_node_scratch: Vec::new(),
223            cached_measure_size_scratch: Vec::new(),
224            cached_measure_missing_scratch: Vec::new(),
225            registered_measurement_node_ids: Vec::new(),
226            pending_commands_applied: false,
227            #[cfg(debug_assertions)]
228            shadow_stash: None,
229        }
230    }
231
232    fn register_measurement_node_id(&mut self, node_id: NodeId) {
233        if !self.registered_measurement_node_ids.contains(&node_id) {
234            self.registered_measurement_node_ids.push(node_id);
235        }
236    }
237
238    fn into_placement_scratch(self) -> Vec<Placement> {
239        self.placement_scratch
240    }
241
242    pub(crate) fn layout_with_placement_builder(
243        &mut self,
244        width: f32,
245        height: f32,
246        build: impl FnOnce(&mut Vec<Placement>),
247    ) -> MeasureResult {
248        self.placement_scratch.clear();
249        build(&mut self.placement_scratch);
250        MeasureResult::new(
251            Size { width, height },
252            std::mem::take(&mut self.placement_scratch),
253        )
254    }
255
256    fn record_error(&self, err: NodeError) {
257        let mut slot = self.error.borrow_mut();
258        if slot.is_none() {
259            eprintln!("[SubcomposeLayout] Error suppressed: {err:?}");
260            *slot = Some(err);
261        }
262    }
263
264    fn owner_chain_deactivation_epoch(&self) -> u64 {
265        self.parent_handle
266            .inner
267            .borrow()
268            .captured_context
269            .as_ref()
270            .map_or(
271                0,
272                cranpose_core::CapturedCompositionContext::owner_chain_deactivation_epoch,
273            )
274    }
275
276    fn ensure_pending_commands_applied(&mut self) -> bool {
277        if self.pending_commands_applied {
278            return true;
279        }
280
281        let telemetry_start = subcompose_telemetry_enabled().then(Instant::now);
282        if let Err(err) = self.composer.apply_pending_commands() {
283            self.record_error(err);
284            return false;
285        }
286        if let Some(start) = telemetry_start {
287            log::warn!(
288                "[subcompose-telemetry] apply_pending_commands_ms={:.2}",
289                start.elapsed().as_secs_f64() * 1000.0
290            );
291        }
292
293        self.pending_commands_applied = true;
294        true
295    }
296
297    fn perform_subcompose<Content>(&mut self, slot_id: SlotId, content: Content) -> Vec<NodeId>
298    where
299        Content: FnMut() + 'static,
300    {
301        let telemetry_start = subcompose_telemetry_enabled().then(Instant::now);
302        let mut inner = self.parent_handle.inner.borrow_mut();
303
304        let (virtual_node_id, is_rebound) =
305            if let Some((node_id, rebound)) = self.state.take_node_from_reusables(slot_id) {
306                (node_id, rebound)
307            } else {
308                let id = allocate_virtual_node_id();
309                let node = LayoutNode::new_virtual();
310                if let Err(e) = self.composer.register_virtual_node(id, Box::new(node)) {
311                    eprintln!("[Subcompose] Failed to register virtual node {id}: {e:?}");
312                }
313                inner.children.push(id);
314                (id, false)
315            };
316
317        self.composer.record_subcompose_child(virtual_node_id);
318
319        drop(inner);
320
321        let children = self.compose_into_slot(slot_id, virtual_node_id, content);
322        if is_rebound {
323            self.composer.record_rebound_slot_children(&children);
324        }
325        if let Some(start) = telemetry_start {
326            log::warn!(
327                "[subcompose-telemetry] slot={} reused={} children={} subcompose_ms={:.2}",
328                slot_id.raw(),
329                is_rebound,
330                children.len(),
331                start.elapsed().as_secs_f64() * 1000.0
332            );
333        }
334        children
335    }
336
337    fn compose_into_slot<Content>(
338        &mut self,
339        slot_id: SlotId,
340        virtual_node_id: NodeId,
341        content: Content,
342    ) -> Vec<NodeId>
343    where
344        Content: FnMut() + 'static,
345    {
346        let content_holder = self.state.callback_holder(slot_id);
347        content_holder.update(content);
348
349        let _ = self
350            .composer
351            .with_node_mut::<LayoutNode, _>(virtual_node_id, |node| {
352                node.set_parent(self.root_id);
353            });
354
355        let slot_host = self.state.get_or_create_slots(slot_id);
356        self.parent_handle.note_slot_host(&slot_host);
357        let locals_stale = self.state.slot_locals_stale(slot_id);
358        let scopes = self
359            .composer
360            .with_locals_changed(locals_stale, |composer| {
361                composer.subcompose_slot(&slot_host, Some(virtual_node_id), move |_| {
362                    compose_subcompose_slot_content(content_holder);
363                })
364            })
365            .map(|((), scopes)| scopes)
366            .unwrap_or_default();
367        self.pending_commands_applied = false;
368
369        let owner_epoch = self.owner_chain_deactivation_epoch();
370        self.state
371            .register_active(slot_id, &[virtual_node_id], &scopes);
372        self.state.mark_slot_composed_current(slot_id, owner_epoch);
373
374        self.composer.get_node_children(virtual_node_id).to_vec()
375    }
376
377    fn activate_clean_retained_slot(&mut self, slot_id: SlotId) -> Option<Vec<NodeId>> {
378        if self.state.has_pending_precompositions(slot_id) {
379            return None;
380        }
381        if !self
382            .state
383            .slot_content_generation_current(slot_id, self.owner_chain_deactivation_epoch())
384        {
385            return None;
386        }
387        if !self.ensure_pending_commands_applied() {
388            return None;
389        }
390        let virtual_node_ids = self.state.activate_current_active_slot(slot_id)?;
391
392        for &virtual_node_id in virtual_node_ids {
393            self.composer.record_subcompose_child(virtual_node_id);
394            let _ = self
395                .composer
396                .with_node_mut::<LayoutNode, _>(virtual_node_id, |node| {
397                    node.set_parent(self.root_id);
398                });
399        }
400
401        let mut children = Vec::new();
402        for virtual_node_id in virtual_node_ids {
403            children.extend(self.composer.get_node_children(*virtual_node_id));
404        }
405        record_clean_slot_skip();
406
407        #[cfg(debug_assertions)]
408        {
409            self.shadow_stash = Some((virtual_node_ids.to_vec(), children.clone()));
410        }
411
412        Some(children)
413    }
414
415    #[cfg(debug_assertions)]
416    fn shadow_verify_clean_slot<Content>(&mut self, slot_id: SlotId, content: Content)
417    where
418        Content: FnMut() + 'static,
419    {
420        let Some((virtual_node_ids, skipped_children)) = self.shadow_stash.take() else {
421            return;
422        };
423        if virtual_node_ids.len() != 1 {
424            return;
425        }
426        let composed = self.compose_into_slot(slot_id, virtual_node_ids[0], content);
427        assert!(
428            composed == skipped_children,
429            "clean-slot skip diverged for slot {slot_id:?}: recomposing produced root \
430             children {composed:?} but the retained slot held {skipped_children:?}. The slot content \
431             read a value that changed between measure passes without any \
432             invalidation path — make that value reactive state, or part of \
433             the subcompose capture key",
434        );
435    }
436
437    pub(crate) fn activate_exact_retained_slot_with_known_children(
438        &mut self,
439        slot_id: SlotId,
440        known_children: &[u64],
441    ) -> Option<RetainedSlotChildren> {
442        for &node_id in known_children {
443            NodeId::try_from(node_id).ok()?;
444        }
445        if !self.ensure_pending_commands_applied() {
446            return None;
447        }
448
449        let activation = self.state.activate_exact_slot(slot_id)?;
450        for &virtual_node_id in activation.nodes {
451            self.composer.record_subcompose_child(virtual_node_id);
452            if activation.was_recycled {
453                let _ = self
454                    .composer
455                    .with_node_mut::<LayoutNode, _>(virtual_node_id, |node| {
456                        node.set_parent(self.root_id);
457                    });
458            }
459        }
460
461        let mut expected = known_children.iter().copied();
462        let mut children_match = true;
463        let mut needs_relayout = false;
464        for &virtual_node_id in activation.nodes {
465            let children = self.composer.get_node_children(virtual_node_id);
466            for &child in &children {
467                children_match &= expected.next() == Some(child as u64);
468            }
469            needs_relayout |= self.composer.nodes_need_relayout(children.iter().copied());
470        }
471        children_match &= expected.next().is_none();
472        if children_match && !needs_relayout {
473            return Some(RetainedSlotChildren::Clean);
474        }
475
476        let mut children = Vec::with_capacity(known_children.len());
477        for &virtual_node_id in activation.nodes {
478            children.extend(
479                self.composer
480                    .get_node_children(virtual_node_id)
481                    .into_iter()
482                    .map(SubcomposeChild::new),
483            );
484        }
485        Some(RetainedSlotChildren::Remeasure {
486            children,
487            children_match,
488        })
489    }
490}
491
492impl SubcomposeLayoutScope for SubcomposeMeasureScopeImpl<'_> {
493    fn constraints(&self) -> Constraints {
494        self.constraints
495    }
496
497    fn layout<I>(&mut self, width: f32, height: f32, placements: I) -> MeasureResult
498    where
499        I: IntoIterator<Item = Placement>,
500    {
501        self.layout_with_placement_builder(width, height, |scratch| {
502            scratch.extend(placements);
503        })
504    }
505}
506
507impl cranpose_ui_layout::MeasureScope for SubcomposeMeasureScopeImpl<'_> {
508    fn density(&self) -> f32 {
509        self.density_scope.density()
510    }
511
512    fn font_scale(&self) -> f32 {
513        self.density_scope.font_scale()
514    }
515}
516
517impl SubcomposeMeasureScope for SubcomposeMeasureScopeImpl<'_> {
518    fn subcompose<K, Content>(
519        &mut self,
520        slot_id: SlotId,
521        key: K,
522        content: Content,
523    ) -> Vec<SubcomposeChild>
524    where
525        K: PartialEq + 'static,
526        Content: FnMut() + 'static,
527    {
528        if self.state.retained_capture_key_matches(slot_id, &key)
529            && let Some(children) = self.activate_clean_retained_slot(slot_id)
530        {
531            #[cfg(debug_assertions)]
532            self.shadow_verify_clean_slot(slot_id, content);
533            return children.into_iter().map(SubcomposeChild::new).collect();
534        }
535        self.state.store_retained_capture_key(slot_id, key);
536        let nodes = self.perform_subcompose(slot_id, content);
537        nodes.into_iter().map(SubcomposeChild::new).collect()
538    }
539
540    fn measure(&mut self, child: SubcomposeChild, constraints: Constraints) -> SubcomposePlaceable {
541        if self.error.borrow().is_some() {
542            return SubcomposePlaceable::value(0.0, 0.0, child.node_id);
543        }
544
545        let telemetry_start = subcompose_telemetry_enabled().then(Instant::now);
546        if !self.ensure_pending_commands_applied() {
547            return SubcomposePlaceable::value(0.0, 0.0, child.node_id);
548        }
549
550        let placeable = (self.measurer)(child.node_id, constraints);
551        self.register_measurement_node_id(child.node_id);
552        if let Some(start) = telemetry_start {
553            log::warn!(
554                "[subcompose-telemetry] child={} measure_ms={:.2} size=({:.2},{:.2})",
555                child.node_id,
556                start.elapsed().as_secs_f64() * 1000.0,
557                placeable.width(),
558                placeable.height()
559            );
560        }
561        placeable
562    }
563
564    fn node_has_no_parent(&self, node_id: NodeId) -> bool {
565        self.composer.node_has_no_parent(node_id)
566    }
567}
568
569impl SubcomposeMeasureScopeImpl<'_> {
570    /// Returns the number of active slots in the subcompose state.
571    ///
572    /// Used by lazy layouts to report statistics about slot usage.
573    pub fn active_slots_count(&self) -> usize {
574        self.state.active_slots_count()
575    }
576
577    /// Returns the number of reusable slots in the pool.
578    ///
579    /// Used by lazy layouts to report statistics about cached slots.
580    pub fn reusable_slots_count(&self) -> usize {
581        self.state.reusable_slots_count()
582    }
583
584    /// Whether `slot_id` still has a composition: active or in the reuse pool.
585    pub(crate) fn slot_is_retained(&self, slot_id: SlotId) -> bool {
586        self.state.slot_is_retained(slot_id)
587    }
588
589    pub(crate) fn slot_is_active(&self, slot_id: SlotId) -> bool {
590        self.state.slot_is_active(slot_id)
591    }
592
593    /// Registers the content type for a slot.
594    ///
595    /// Call this before `subcompose()` to enable content-type-aware slot reuse.
596    /// If the policy supports content types (like `ContentTypeReusePolicy`),
597    /// slots with matching content types can reuse each other's nodes.
598    pub fn register_content_type(&mut self, slot_id: SlotId, content_type: u64) {
599        self.state.register_content_type(slot_id, content_type);
600    }
601
602    /// Updates the content type for a slot, handling Some→None transitions.
603    ///
604    /// If `content_type` is `Some(type)`, registers the type for the slot.
605    /// If `content_type` is `None`, removes any previously registered type.
606    /// This ensures stale types don't drive incorrect reuse after transitions.
607    pub fn update_content_type(&mut self, slot_id: SlotId, content_type: Option<u64>) {
608        self.state.update_content_type(slot_id, content_type);
609    }
610
611    pub(crate) fn set_reusable_pool_limits(&mut self, per_type: usize, untyped: usize) {
612        self.state.set_reusable_pool_limits(per_type, untyped);
613    }
614
615    pub(crate) fn focused_slot(&self) -> Option<SlotId> {
616        let mut node_id = crate::active_focus_target()?;
617        while node_id != self.root_id {
618            if let Some(slot) = self.state.active_slot_for_node(node_id) {
619                return Some(slot);
620            }
621            node_id = self.composer.node_parent(node_id).ok()??;
622        }
623        None
624    }
625
626    pub(crate) fn recycle_active_slots_where(&mut self, predicate: impl FnMut(SlotId) -> bool) {
627        let disposed = self.state.recycle_active_slots_where(predicate);
628        if let Err(error) = self.composer.dispose_subcomposed_nodes(disposed) {
629            self.record_error(error);
630        }
631    }
632
633    /// Returns whether the last subcomposed slot was reused.
634    ///
635    /// Returns `Some(true)` if the slot already existed (was reused from pool or
636    /// was recomposed), `Some(false)` if it was newly created, or `None` if no
637    /// slot has been subcomposed yet this pass.
638    ///
639    /// This is useful for tracking composition statistics in lazy layouts.
640    pub fn was_last_slot_reused(&self) -> Option<bool> {
641        self.state.was_last_slot_reused()
642    }
643
644    pub(crate) fn measure_retained(
645        &mut self,
646        child: SubcomposeChild,
647        constraints: Constraints,
648    ) -> (SubcomposePlaceable, Option<Rc<MeasuredNode>>) {
649        let placeable = self.measure(child, constraints);
650        let retained = (self.retained_measure_lookup)(child.node_id);
651        (placeable, retained)
652    }
653
654    pub(crate) fn register_retained_measurements(&mut self, measurements: &[Rc<MeasuredNode>]) {
655        if measurements.is_empty() {
656            return;
657        }
658
659        for measured in measurements {
660            self.register_measurement_node_id(measured.node_id());
661        }
662        (self.retained_measure_registrar)(measurements);
663    }
664
665    pub(crate) fn children_need_relayout(&mut self, children: &[SubcomposeChild]) -> bool {
666        if !self.ensure_pending_commands_applied() {
667            return true;
668        }
669
670        self.composer
671            .nodes_need_relayout(children.iter().map(SubcomposeChild::node_id))
672    }
673
674    pub(crate) fn ensure_cached_measurement_node_ids<I>(
675        &mut self,
676        node_ids: I,
677        constraints: Constraints,
678    ) -> usize
679    where
680        I: IntoIterator<Item = NodeId>,
681    {
682        if self.error.borrow().is_some() || !self.ensure_pending_commands_applied() {
683            return 0;
684        }
685
686        self.cached_measure_node_scratch.clear();
687        self.cached_measure_node_scratch.extend(
688            node_ids
689                .into_iter()
690                .filter(|node_id| !self.registered_measurement_node_ids.contains(node_id)),
691        );
692        if self.cached_measure_node_scratch.is_empty() {
693            return 0;
694        }
695
696        self.cached_measure_size_scratch.clear();
697        (self.cached_measure_batch_registrar)(
698            &self.cached_measure_node_scratch,
699            constraints,
700            &mut self.cached_measure_size_scratch,
701        );
702        self.cached_measure_size_scratch
703            .resize(self.cached_measure_node_scratch.len(), None);
704
705        let mut cached_count = 0;
706        self.cached_measure_missing_scratch.clear();
707        for index in 0..self.cached_measure_node_scratch.len() {
708            let node_id = self.cached_measure_node_scratch[index];
709            if self.cached_measure_size_scratch[index].is_some() {
710                cached_count += 1;
711                self.register_measurement_node_id(node_id);
712            } else {
713                self.cached_measure_missing_scratch.push(node_id);
714            }
715        }
716
717        let mut missing = std::mem::take(&mut self.cached_measure_missing_scratch);
718        for node_id in missing.drain(..) {
719            let _ = self.measure(SubcomposeChild::new(node_id), constraints);
720        }
721        self.cached_measure_missing_scratch = missing;
722
723        cached_count
724    }
725}
726
727fn compose_subcompose_slot_content(holder: cranpose_core::CallbackHolder) {
728    cranpose_core::with_current_composer(|composer| {
729        let holder_for_recompose = holder.clone();
730        composer.set_recompose_callback(move |_composer| {
731            compose_subcompose_slot_content(holder_for_recompose.clone());
732        });
733    });
734
735    let invoke = holder.clone_rc();
736    invoke();
737}
738
739pub type MeasurePolicy =
740    dyn for<'scope> Fn(&mut SubcomposeMeasureScopeImpl<'scope>, Constraints) -> MeasureResult;
741
742/// Node responsible for orchestrating measure-time subcomposition.
743pub struct SubcomposeLayoutNode {
744    inner: Rc<RefCell<SubcomposeLayoutNodeInner>>,
745    parent: Cell<Option<NodeId>>,
746    id: Cell<Option<NodeId>>,
747    needs_measure: Cell<bool>,
748    needs_layout: Cell<bool>,
749    /// A node below this one needs layout or measure.
750    descendant_dirt: Cell<DescendantDirt>,
751    needs_semantics: Cell<bool>,
752    needs_redraw: Cell<bool>,
753    needs_pointer_pass: Cell<bool>,
754    needs_focus_sync: Cell<bool>,
755    layout_state: RefCell<LayoutState>,
756    cache_handles: LayoutNodeCacheHandles,
757    modifier_slices_snapshot: crate::modifier::SlicesSnapshot,
758    /// Never written: this node lays its chain out from padding, size and
759    /// offset without coordinators, so its draws sit inside the padding
760    /// before them.
761    coordinator_geometry: Rc<crate::modifier::CoordinatorGeometry>,
762    modifier_slices_dirty: Cell<bool>,
763}
764
765impl SubcomposeLayoutNode {
766    pub fn new(modifier: Modifier, measure_policy: Rc<MeasurePolicy>) -> Self {
767        let inner = Rc::new(RefCell::new(SubcomposeLayoutNodeInner::new(measure_policy)));
768        let node = Self {
769            inner,
770            parent: Cell::new(None),
771            id: Cell::new(None),
772            needs_measure: Cell::new(true),
773            needs_layout: Cell::new(true),
774            descendant_dirt: Cell::default(),
775            needs_semantics: Cell::new(true),
776            needs_redraw: Cell::new(true),
777            needs_pointer_pass: Cell::new(false),
778            needs_focus_sync: Cell::new(false),
779            layout_state: RefCell::new(LayoutState::default()),
780            cache_handles: LayoutNodeCacheHandles::default(),
781            modifier_slices_snapshot: crate::modifier::SlicesSnapshot::default(),
782            coordinator_geometry: Rc::default(),
783            modifier_slices_dirty: Cell::new(true),
784        };
785        let (invalidations, _) = node.inner.borrow_mut().set_modifier_collect(modifier);
786        node.dispatch_modifier_invalidations(&invalidations, NodeCapabilities::empty());
787        node.note_host_to_the_composition_that_made_it();
788        node
789    }
790
791    fn note_host_to_the_composition_that_made_it(&self) {
792        let host = Rc::clone(&self.inner.borrow().slots);
793        cranpose_core::note_nested_slots_host(&host);
794    }
795
796    /// Creates a SubcomposeLayoutNode with ContentTypeReusePolicy.
797    ///
798    /// Use this for lazy lists to enable content-type-aware slot reuse.
799    /// Slots with matching content types can reuse each other's nodes,
800    /// improving efficiency when scrolling through items with different types.
801    pub fn with_content_type_policy(modifier: Modifier, measure_policy: Rc<MeasurePolicy>) -> Self {
802        let mut inner_data = SubcomposeLayoutNodeInner::new(measure_policy);
803        inner_data
804            .state
805            .set_policy(Box::new(cranpose_core::ContentTypeReusePolicy::new()));
806        let inner = Rc::new(RefCell::new(inner_data));
807        let node = Self {
808            inner,
809            parent: Cell::new(None),
810            id: Cell::new(None),
811            needs_measure: Cell::new(true),
812            needs_layout: Cell::new(true),
813            descendant_dirt: Cell::default(),
814            needs_semantics: Cell::new(true),
815            needs_redraw: Cell::new(true),
816            needs_pointer_pass: Cell::new(false),
817            needs_focus_sync: Cell::new(false),
818            layout_state: RefCell::new(LayoutState::default()),
819            cache_handles: LayoutNodeCacheHandles::default(),
820            modifier_slices_snapshot: crate::modifier::SlicesSnapshot::default(),
821            coordinator_geometry: Rc::default(),
822            modifier_slices_dirty: Cell::new(true),
823        };
824        let (invalidations, _) = node.inner.borrow_mut().set_modifier_collect(modifier);
825        node.dispatch_modifier_invalidations(&invalidations, NodeCapabilities::empty());
826        node.note_host_to_the_composition_that_made_it();
827        node
828    }
829
830    pub fn handle(&self) -> SubcomposeLayoutNodeHandle {
831        SubcomposeLayoutNodeHandle {
832            inner: Rc::clone(&self.inner),
833        }
834    }
835
836    #[doc(hidden)]
837    pub fn debug_scope_ids_by_slot(&self) -> Vec<(u64, Vec<usize>)> {
838        self.inner.borrow().state.debug_scope_ids_by_slot()
839    }
840
841    #[doc(hidden)]
842    pub fn debug_slot_table_for_slot(
843        &self,
844        slot_id: cranpose_core::SlotId,
845    ) -> Option<Vec<cranpose_core::SlotDebugEntry>> {
846        self.inner.borrow().state.debug_slot_table_for_slot(slot_id)
847    }
848
849    #[doc(hidden)]
850    pub fn debug_slot_table_groups_for_slot(
851        &self,
852        slot_id: cranpose_core::SlotId,
853    ) -> Option<Vec<cranpose_core::subcompose::DebugSlotGroup>> {
854        self.inner
855            .borrow()
856            .state
857            .debug_slot_table_groups_for_slot(slot_id)
858    }
859
860    pub fn set_measure_policy(&mut self, policy: Rc<MeasurePolicy>) {
861        let mut inner = self.inner.borrow_mut();
862        if Rc::ptr_eq(&inner.measure_policy, &policy) {
863            return;
864        }
865        inner.set_measure_policy(policy);
866        drop(inner);
867        self.invalidate_subcomposition();
868    }
869
870    /// Records the source composition context for measure-time subcomposition.
871    /// A context under a changed static composition local makes every slot
872    /// compose again, running every body it holds, as Compose forces a
873    /// `SubcomposeLayout`'s children.
874    pub fn set_captured_context(&mut self, context: cranpose_core::CapturedCompositionContext) {
875        let locals_changed = context.locals_changed();
876        self.inner.borrow_mut().captured_context = Some(context);
877        if locals_changed {
878            self.inner.borrow().state.invalidate_locals();
879            self.request_measure_recompose();
880        }
881    }
882
883    /// Records the grid the composition provided, re-measuring if it moved.
884    ///
885    /// Mirrors [`LayoutNode::set_density`](crate::widgets::nodes::LayoutNode::set_density):
886    /// `SubcomposeLayout` captures this at the same composition site that
887    /// captures the subcomposition context, so the two cannot disagree.
888    pub fn set_density(&mut self, density: crate::density::Density) {
889        let mut inner = self.inner.borrow_mut();
890        if inner.density != density {
891            inner.density = density;
892            drop(inner);
893            self.modifier_slices_dirty.set(true);
894            self.mark_needs_measure();
895        }
896    }
897
898    /// Updates relative modifier placement for this node's composition scope.
899    pub fn set_layout_direction(&mut self, direction: crate::LayoutDirection) {
900        let mut inner = self.inner.borrow_mut();
901        if inner.modifier_chain.set_layout_direction(direction) {
902            inner.resolved_modifiers = inner.modifier_chain.resolved_modifiers();
903            drop(inner);
904            self.modifier_slices_dirty.set(true);
905            self.mark_needs_measure();
906        }
907    }
908
909    /// The grid the composition provided, which the chain's lengths land on.
910    pub(crate) fn density(&self) -> crate::density::Density {
911        self.inner.borrow().density
912    }
913
914    pub fn set_modifier(&mut self, modifier: Modifier) {
915        let prev_caps = self.modifier_capabilities();
916        let (invalidations, modifier_changed) = {
917            let mut inner = self.inner.borrow_mut();
918            inner.set_modifier_collect(modifier)
919        };
920        self.dispatch_modifier_invalidations(&invalidations, prev_caps);
921        self.modifier_slices_dirty.set(true);
922        crate::modal_nodes::reach_changed(self.id.get());
923        if modifier_changed {
924            self.request_semantics_update();
925        }
926    }
927
928    fn update_modifier_slices_cache(&self) {
929        let inner = self.inner.borrow();
930        self.modifier_slices_snapshot.collect(
931            inner.modifier_chain.chain(),
932            &self.coordinator_geometry,
933            inner.density.density(),
934        );
935        self.modifier_slices_dirty.set(false);
936    }
937
938    pub(crate) fn mark_modifier_slices_dirty(&self) {
939        self.modifier_slices_dirty.set(true);
940    }
941
942    pub fn set_debug_modifiers(&mut self, enabled: bool) {
943        self.inner.borrow_mut().set_debug_modifiers(enabled);
944    }
945
946    pub fn modifier(&self) -> Modifier {
947        self.handle().modifier()
948    }
949
950    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
951        self.inner.borrow().resolved_modifiers
952    }
953
954    /// Returns a clone of the current layout state.
955    pub fn layout_state(&self) -> LayoutState {
956        self.layout_state.borrow().clone()
957    }
958
959    pub(crate) fn cache_handles(&self) -> &LayoutNodeCacheHandles {
960        &self.cache_handles
961    }
962
963    /// Updates the position of this node. Called during placement.
964    /// [`LayoutState::place`] self-reports actual moves to the scene phase.
965    pub fn set_position(&self, position: Point) {
966        self.layout_state.borrow_mut().place(position);
967    }
968
969    /// Updates the measured size of this node. Called during measurement.
970    /// [`LayoutState::set_size`] self-reports actual changes to the scene
971    /// phase.
972    pub fn set_measured_size(&self, size: Size) {
973        self.layout_state.borrow_mut().set_size(size);
974    }
975
976    /// Clears the is_placed flag. Called at the start of a layout pass.
977    pub fn clear_placed(&self) {
978        self.layout_state.borrow_mut().clear_placed();
979    }
980
981    /// Semantics reported by this node's live modifier chain.
982    pub fn semantics_configuration(&self) -> Option<SemanticsConfiguration> {
983        crate::modifier::collect_semantics_from_chain(self.inner.borrow().modifier_chain.chain())
984    }
985
986    /// Whether this node's modifiers make it modal or hidden.
987    pub fn semantics_reach(&self) -> cranpose_foundation::SemanticsReach {
988        crate::modifier::semantics_reach_of_chain(self.inner.borrow().modifier_chain.chain())
989    }
990
991    /// Returns the modifier slices snapshot for rendering.
992    pub fn modifier_slices_snapshot(&self) -> Rc<ModifierNodeSlices> {
993        if self.modifier_slices_dirty.get() {
994            self.update_modifier_slices_cache();
995        }
996        self.modifier_slices_snapshot.get()
997    }
998
999    pub fn state(&self) -> Ref<'_, SubcomposeState> {
1000        Ref::map(self.inner.borrow(), |inner| &inner.state)
1001    }
1002
1003    pub fn state_mut(&self) -> RefMut<'_, SubcomposeState> {
1004        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.state)
1005    }
1006
1007    pub fn invalidate_subcomposition(&self) {
1008        self.inner.borrow().state.invalidate_scopes();
1009        self.mark_needs_measure();
1010        if let Some(id) = self.id.get() {
1011            cranpose_core::bubble_measure_dirty_in_composer(id);
1012        }
1013    }
1014
1015    pub fn request_measure_recompose(&self) {
1016        self.mark_needs_measure();
1017        if let Some(id) = self.id.get() {
1018            cranpose_core::bubble_measure_dirty_in_composer(id);
1019        }
1020    }
1021
1022    /// Calls `f` with the children this node placed in its last layout, in
1023    /// placement order, without copying them.
1024    /// Changes through a node handle during `f` become visible to the next
1025    /// read; this read keeps its original children.
1026    pub fn with_active_children<R>(&self, f: impl FnOnce(&[NodeId]) -> R) -> R {
1027        let children = Rc::clone(&self.inner.borrow().last_placements);
1028        f(&children)
1029    }
1030
1031    /// Mark this node as needing measure. Also marks it as needing layout.
1032    pub fn mark_needs_measure(&self) {
1033        self.needs_measure.set(true);
1034        self.needs_layout.set(true);
1035    }
1036
1037    /// Mark this node as needing layout (but not necessarily measure).
1038    pub fn mark_needs_layout_flag(&self) {
1039        self.needs_layout.set(true);
1040    }
1041
1042    /// Mark this node as needing redraw without forcing measure/layout.
1043    pub fn mark_needs_redraw(&self) {
1044        self.needs_redraw.set(true);
1045        if let Some(id) = self.id.get() {
1046            crate::schedule_draw_repass(id);
1047        }
1048        crate::request_render_invalidation();
1049    }
1050
1051    /// Check if this node needs measure.
1052    pub fn needs_measure(&self) -> bool {
1053        self.needs_measure.get()
1054    }
1055
1056    pub(crate) fn clear_needs_measure(&self) {
1057        self.needs_measure.set(false);
1058    }
1059
1060    /// Clears the node's layout flag and its descendants': a layout pass
1061    /// visited them.
1062    pub(crate) fn clear_needs_layout(&self) {
1063        self.needs_layout.set(false);
1064        self.descendant_dirt.set(DescendantDirt::default());
1065    }
1066
1067    /// Mark this node as needing semantics recomputation.
1068    pub fn mark_needs_semantics(&self) {
1069        self.needs_semantics.set(true);
1070        crate::modal_nodes::reach_changed(self.id.get());
1071    }
1072
1073    /// The id this node was inserted under, once it is.
1074    pub(crate) fn id(&self) -> Option<NodeId> {
1075        self.id.get()
1076    }
1077
1078    pub(crate) fn clear_needs_semantics(&self) {
1079        self.needs_semantics.set(false);
1080    }
1081
1082    #[cfg(test)]
1083    pub(crate) fn clear_needs_semantics_for_tests(&self) {
1084        self.clear_needs_semantics();
1085    }
1086
1087    /// Returns true when this node requested a redraw since the last render pass.
1088    pub fn needs_redraw(&self) -> bool {
1089        self.needs_redraw.get()
1090    }
1091
1092    pub fn clear_needs_redraw(&self) {
1093        self.needs_redraw.set(false);
1094    }
1095
1096    /// Marks this node as needing a fresh pointer-input pass.
1097    pub fn mark_needs_pointer_pass(&self) {
1098        self.needs_pointer_pass.set(true);
1099    }
1100
1101    /// Returns true when pointer-input state needs to be recomputed.
1102    pub fn needs_pointer_pass(&self) -> bool {
1103        self.needs_pointer_pass.get()
1104    }
1105
1106    /// Clears the pointer-input dirty flag after hosts service it.
1107    pub fn clear_needs_pointer_pass(&self) {
1108        self.needs_pointer_pass.set(false);
1109    }
1110
1111    /// Marks this node as needing a focus synchronization.
1112    pub fn mark_needs_focus_sync(&self) {
1113        self.needs_focus_sync.set(true);
1114    }
1115
1116    /// Returns true when focus state needs to be synchronized.
1117    pub fn needs_focus_sync(&self) -> bool {
1118        self.needs_focus_sync.get()
1119    }
1120
1121    /// Clears the focus dirty flag after the focus manager processes it.
1122    pub fn clear_needs_focus_sync(&self) {
1123        self.needs_focus_sync.set(false);
1124    }
1125
1126    fn request_semantics_update(&self) {
1127        let already_dirty = self.needs_semantics.replace(true);
1128        if already_dirty {
1129            return;
1130        }
1131
1132        if let Some(id) = self.id.get() {
1133            cranpose_core::queue_semantics_invalidation(id);
1134        }
1135    }
1136
1137    /// Returns the modifier capabilities for this node.
1138    pub fn modifier_capabilities(&self) -> NodeCapabilities {
1139        self.inner.borrow().modifier_capabilities
1140    }
1141
1142    pub fn has_layout_modifier_nodes(&self) -> bool {
1143        self.modifier_capabilities()
1144            .contains(NodeCapabilities::LAYOUT)
1145    }
1146
1147    pub fn has_draw_modifier_nodes(&self) -> bool {
1148        self.modifier_capabilities()
1149            .contains(NodeCapabilities::DRAW)
1150    }
1151
1152    pub fn has_pointer_input_modifier_nodes(&self) -> bool {
1153        self.modifier_capabilities()
1154            .contains(NodeCapabilities::POINTER_INPUT)
1155    }
1156
1157    pub fn has_semantics_modifier_nodes(&self) -> bool {
1158        self.modifier_capabilities()
1159            .contains(NodeCapabilities::SEMANTICS)
1160    }
1161
1162    pub fn has_focus_modifier_nodes(&self) -> bool {
1163        self.modifier_capabilities()
1164            .contains(NodeCapabilities::FOCUS)
1165    }
1166
1167    fn dispatch_modifier_invalidations(
1168        &self,
1169        invalidations: &[ModifierInvalidation],
1170        prev_caps: NodeCapabilities,
1171    ) {
1172        let curr_caps = self.modifier_capabilities();
1173        for invalidation in invalidations {
1174            self.modifier_slices_dirty.set(true);
1175            let invalidation_caps = invalidation.capabilities();
1176            let has_capability = |capability| {
1177                curr_caps.contains(capability)
1178                    || prev_caps.contains(capability)
1179                    || invalidation_caps.contains(capability)
1180            };
1181            match invalidation.kind() {
1182                InvalidationKind::Layout => {
1183                    if has_capability(NodeCapabilities::LAYOUT) {
1184                        self.mark_needs_measure();
1185                    }
1186                }
1187                InvalidationKind::Draw => {
1188                    if has_capability(NodeCapabilities::DRAW) {
1189                        self.mark_needs_redraw();
1190                    }
1191                }
1192                InvalidationKind::PointerInput => {
1193                    if has_capability(NodeCapabilities::POINTER_INPUT) {
1194                        self.mark_needs_pointer_pass();
1195                        crate::request_pointer_invalidation();
1196                        if let Some(id) = self.id.get() {
1197                            crate::schedule_pointer_repass(id);
1198                        }
1199                    }
1200                }
1201                InvalidationKind::Semantics => {
1202                    self.request_semantics_update();
1203                }
1204                InvalidationKind::Focus => {
1205                    if has_capability(NodeCapabilities::FOCUS) {
1206                        self.mark_needs_focus_sync();
1207                        crate::request_focus_invalidation();
1208                        if let Some(id) = self.id.get() {
1209                            crate::schedule_focus_invalidation(id);
1210                        }
1211                    }
1212                }
1213            }
1214        }
1215    }
1216}
1217
1218impl cranpose_core::Node for SubcomposeLayoutNode {
1219    fn mount(&mut self) {
1220        let mut inner = self.inner.borrow_mut();
1221        let node_id = inner.node_id;
1222        inner.modifier_chain.remount(node_id);
1223        crate::modal_nodes::reach_changed(self.id.get());
1224    }
1225
1226    fn unmount(&mut self) {
1227        self.inner
1228            .borrow_mut()
1229            .modifier_chain
1230            .chain_mut()
1231            .detach_nodes();
1232    }
1233
1234    fn insert_child(&mut self, child: NodeId) -> bool {
1235        let mut inner = self.inner.borrow_mut();
1236        if inner.children.contains(&child) {
1237            return false;
1238        }
1239        inner.children.push(child);
1240        true
1241    }
1242
1243    fn remove_child(&mut self, child: NodeId) -> bool {
1244        let mut inner = self.inner.borrow_mut();
1245        let before = inner.children.len();
1246        inner.children.retain(|&id| id != child);
1247        inner.children.len() < before
1248    }
1249
1250    fn move_child(&mut self, from: usize, to: usize) {
1251        let mut inner = self.inner.borrow_mut();
1252        if from == to || from >= inner.children.len() {
1253            return;
1254        }
1255        let child = inner.children.remove(from);
1256        let target = to.min(inner.children.len());
1257        inner.children.insert(target, child);
1258    }
1259
1260    fn update_children(&mut self, children: &[NodeId]) {
1261        let mut inner = self.inner.borrow_mut();
1262        inner.children.clear();
1263        inner.children.extend_from_slice(children);
1264    }
1265
1266    fn collect_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1267        out.clear();
1268        self.with_active_children(|children| out.extend_from_slice(children));
1269    }
1270
1271    fn collect_owned_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1272        out.clear();
1273        out.extend_from_slice(&self.inner.borrow().children);
1274    }
1275
1276    fn owned_child_index(&self, child: NodeId) -> Option<usize> {
1277        self.inner
1278            .borrow()
1279            .children
1280            .iter()
1281            .position(|&id| id == child)
1282    }
1283
1284    fn set_node_id(&mut self, id: NodeId) {
1285        self.id.set(Some(id));
1286        self.layout_state.borrow_mut().set_node_id(id);
1287        {
1288            let mut inner = self.inner.borrow_mut();
1289            let previous = inner.node_id.replace(id);
1290            inner.modifier_chain.move_to_node_later(previous, Some(id));
1291        }
1292        // The slices carry the node's id; they are collected again when next read.
1293        self.modifier_slices_dirty.set(true);
1294    }
1295
1296    fn on_attached_to_parent(&mut self, parent: NodeId) {
1297        self.parent.set(Some(parent));
1298    }
1299
1300    fn on_removed_from_parent(&mut self) {
1301        self.parent.set(None);
1302        self.inner.borrow().state.bump_content_generation();
1303    }
1304
1305    fn parent(&self) -> Option<NodeId> {
1306        self.parent.get()
1307    }
1308
1309    fn mark_needs_layout(&self) {
1310        self.needs_layout.set(true);
1311    }
1312
1313    fn needs_layout(&self) -> bool {
1314        self.needs_layout.get()
1315    }
1316
1317    fn mark_needs_measure(&self) {
1318        self.needs_measure.set(true);
1319        self.needs_layout.set(true);
1320    }
1321
1322    fn needs_measure(&self) -> bool {
1323        self.needs_measure.get()
1324    }
1325
1326    fn mark_descendant_needs_layout(&self, measure: bool) {
1327        self.descendant_dirt
1328            .set(self.descendant_dirt.get().marked(measure));
1329        if measure {
1330            self.cache_handles.forget_intrinsics();
1331        }
1332    }
1333
1334    fn descendant_needs_layout(&self) -> bool {
1335        self.descendant_dirt.get().layout
1336    }
1337
1338    fn descendant_needs_measure(&self) -> bool {
1339        self.descendant_dirt.get().measure
1340    }
1341
1342    fn mark_needs_semantics(&self) {
1343        self.needs_semantics.set(true);
1344        crate::modal_nodes::reach_changed(self.id.get());
1345    }
1346
1347    /// One flag serves both: the tree merges a subcompose node on every
1348    /// update, and a descendant's change leaves its reach as it was.
1349    fn mark_descendant_needs_semantics(&self) {
1350        self.needs_semantics.set(true);
1351    }
1352
1353    fn needs_semantics(&self) -> bool {
1354        self.needs_semantics.get()
1355    }
1356
1357    fn set_parent_for_bubbling(&mut self, parent: NodeId) {
1358        if self.parent.get().is_none() {
1359            self.parent.set(Some(parent));
1360        }
1361    }
1362}
1363
1364#[derive(Clone)]
1365pub struct SubcomposeLayoutNodeHandle {
1366    inner: Rc<RefCell<SubcomposeLayoutNodeInner>>,
1367}
1368
1369impl SubcomposeLayoutNodeHandle {
1370    pub(crate) fn note_slot_host(&self, slot_host: &Rc<cranpose_core::SlotsHost>) {
1371        let Ok(inner) = self.inner.try_borrow() else {
1372            return;
1373        };
1374        if Rc::ptr_eq(&inner.slots, slot_host) {
1375            return;
1376        }
1377        inner.slots.note_nested_host(slot_host);
1378    }
1379
1380    pub(crate) fn measured_children_scratch(
1381        &self,
1382    ) -> Rc<RefCell<HashMap<NodeId, Rc<MeasuredNode>>>> {
1383        let scratch = {
1384            let inner = self.inner.borrow();
1385            Rc::clone(&inner.measured_children_scratch)
1386        };
1387        scratch.borrow_mut().clear();
1388        scratch
1389    }
1390
1391    pub fn modifier(&self) -> Modifier {
1392        self.inner.borrow().modifier.clone()
1393    }
1394
1395    pub fn layout_properties(&self) -> crate::modifier::LayoutProperties {
1396        self.resolved_modifiers().layout_properties()
1397    }
1398
1399    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
1400        self.inner.borrow().resolved_modifiers
1401    }
1402
1403    pub fn total_offset(&self) -> Point {
1404        self.resolved_modifiers().offset()
1405    }
1406
1407    pub fn modifier_capabilities(&self) -> NodeCapabilities {
1408        self.inner.borrow().modifier_capabilities
1409    }
1410
1411    pub fn has_layout_modifier_nodes(&self) -> bool {
1412        self.modifier_capabilities()
1413            .contains(NodeCapabilities::LAYOUT)
1414    }
1415
1416    pub fn has_draw_modifier_nodes(&self) -> bool {
1417        self.modifier_capabilities()
1418            .contains(NodeCapabilities::DRAW)
1419    }
1420
1421    pub fn has_pointer_input_modifier_nodes(&self) -> bool {
1422        self.modifier_capabilities()
1423            .contains(NodeCapabilities::POINTER_INPUT)
1424    }
1425
1426    pub fn has_semantics_modifier_nodes(&self) -> bool {
1427        self.modifier_capabilities()
1428            .contains(NodeCapabilities::SEMANTICS)
1429    }
1430
1431    pub fn has_focus_modifier_nodes(&self) -> bool {
1432        self.modifier_capabilities()
1433            .contains(NodeCapabilities::FOCUS)
1434    }
1435
1436    pub fn set_debug_modifiers(&self, enabled: bool) {
1437        self.inner.borrow_mut().set_debug_modifiers(enabled);
1438    }
1439
1440    pub fn measure<'a>(
1441        &self,
1442        composer: &Composer,
1443        node_id: NodeId,
1444        constraints: Constraints,
1445        measurer: Box<dyn FnMut(NodeId, Constraints) -> SubcomposePlaceable + 'a>,
1446        mut cached_measure_registrar: Box<dyn FnMut(NodeId, Constraints) -> Option<Size> + 'a>,
1447        error: &'a RefCell<Option<NodeError>>,
1448    ) -> Result<MeasureResult, NodeError> {
1449        self.measure_with_cached_batch(
1450            composer,
1451            node_id,
1452            constraints,
1453            CachedBatchMeasureInputs {
1454                measurer,
1455                cached_measure_batch_registrar: Box::new(
1456                    move |node_ids, child_constraints, out| {
1457                        out.clear();
1458                        out.reserve(node_ids.len());
1459                        for &child_id in node_ids {
1460                            out.push(cached_measure_registrar(child_id, child_constraints));
1461                        }
1462                    },
1463                ),
1464                retained_measure_lookup: Box::new(|_| None),
1465                retained_measure_registrar: Box::new(|_| {}),
1466                error,
1467            },
1468        )
1469    }
1470
1471    pub(crate) fn measure_with_cached_batch(
1472        &self,
1473        composer: &Composer,
1474        node_id: NodeId,
1475        constraints: Constraints,
1476        callbacks: CachedBatchMeasureInputs<'_>,
1477    ) -> Result<MeasureResult, NodeError> {
1478        let CachedBatchMeasureInputs {
1479            measurer,
1480            cached_measure_batch_registrar,
1481            retained_measure_lookup,
1482            retained_measure_registrar,
1483            error,
1484        } = callbacks;
1485        let (policy, mut state, slots_host, placement_scratch, captured_context, density) = {
1486            let mut inner = self.inner.borrow_mut();
1487            let policy = Rc::clone(&inner.measure_policy);
1488            let state = std::mem::take(&mut inner.state);
1489            let slots_host = Rc::clone(&inner.slots);
1490            let placement_scratch = std::mem::take(&mut inner.placement_scratch);
1491            let captured_context = inner.captured_context.clone();
1492            let density = inner.density;
1493            (
1494                policy,
1495                state,
1496                slots_host,
1497                placement_scratch,
1498                captured_context,
1499                density,
1500            )
1501        };
1502        state.begin_pass();
1503
1504        let previous = composer.phase();
1505        if !matches!(previous, Phase::Measure | Phase::Layout) {
1506            composer.enter_phase(Phase::Measure);
1507        }
1508
1509        let constraints_copy = constraints;
1510        let fallback_context;
1511        let context = if let Some(context) = captured_context.as_ref() {
1512            context
1513        } else {
1514            fallback_context = composer.capture_composition_context();
1515            &fallback_context
1516        };
1517        let ((result, placement_scratch), _) = composer.subcompose_slot_with_context(
1518            &slots_host,
1519            Some(node_id),
1520            context,
1521            |inner_composer| {
1522                let mut scope = SubcomposeMeasureScopeImpl::new(SubcomposeMeasureScopeInit {
1523                    composer: inner_composer.clone(),
1524                    density,
1525                    state: &mut state,
1526                    constraints: constraints_copy,
1527                    measurer,
1528                    cached_measure_batch_registrar,
1529                    retained_measure_lookup,
1530                    retained_measure_registrar,
1531                    error,
1532                    parent_handle: self.clone(),
1533                    root_id: node_id,
1534                    placement_scratch,
1535                });
1536                let result = (policy)(&mut scope, constraints_copy);
1537                (result, scope.into_placement_scratch())
1538            },
1539        )?;
1540
1541        let disposed = state.finish_pass();
1542        let disposal = composer.dispose_subcomposed_nodes(disposed);
1543
1544        if previous != composer.phase() {
1545            composer.enter_phase(previous);
1546        }
1547
1548        {
1549            let mut inner = self.inner.borrow_mut();
1550            inner.state = state;
1551            inner.placement_scratch = placement_scratch;
1552
1553            inner.replace_placed_children(
1554                result.placements.iter().map(|placement| placement.node_id),
1555            );
1556        }
1557
1558        disposal?;
1559        Ok(result)
1560    }
1561
1562    pub(crate) fn recycle_placement_scratch(&self, mut placements: Vec<Placement>) {
1563        placements.clear();
1564        let mut inner = self.inner.borrow_mut();
1565        if placements.capacity() > inner.placement_scratch.capacity() {
1566            inner.placement_scratch = placements;
1567        }
1568    }
1569
1570    pub fn set_active_children<I>(&self, children: I)
1571    where
1572        I: IntoIterator<Item = NodeId>,
1573    {
1574        self.inner.borrow_mut().replace_placed_children(children);
1575    }
1576}
1577
1578struct SubcomposeLayoutNodeInner {
1579    modifier: Modifier,
1580    modifier_chain: ModifierChainHandle,
1581    resolved_modifiers: ResolvedModifiers,
1582    modifier_capabilities: NodeCapabilities,
1583    state: SubcomposeState,
1584    measure_policy: Rc<MeasurePolicy>,
1585    children: Vec<NodeId>,
1586    slots: Rc<SlotsHost>,
1587    debug_modifiers: bool,
1588    node_id: Option<NodeId>,
1589    last_placements: Rc<Vec<NodeId>>,
1590    placement_scratch: Vec<Placement>,
1591    measured_children_scratch: Rc<RefCell<HashMap<NodeId, Rc<MeasuredNode>>>>,
1592    captured_context: Option<cranpose_core::CapturedCompositionContext>,
1593    density: crate::density::Density,
1594}
1595
1596impl SubcomposeLayoutNodeInner {
1597    /// Writes the children this node placed, self-reporting an actual change
1598    /// to the scene phase the way [`LayoutState::place`] reports a move.
1599    ///
1600    /// These children *are* the node's render-graph children, and a
1601    /// subcomposition changes them without the applier seeing an insert or a
1602    /// remove: a lazy row that leaves the content keeps its nodes parked in
1603    /// the reusable pool, still attached and still carrying the position it
1604    /// was last placed at. Nothing else in the frame then says the child set
1605    /// shrank, so a scoped scene update would keep the departed row's layer
1606    /// and paint it under the row that took its place.
1607    fn replace_placed_children<I>(&mut self, children: I)
1608    where
1609        I: IntoIterator<Item = NodeId>,
1610    {
1611        let mut changed = false;
1612        let mut count = 0usize;
1613        for child in children {
1614            match self.last_placements.get(count) {
1615                Some(&placed) if placed == child => {}
1616                Some(_) => {
1617                    Rc::make_mut(&mut self.last_placements)[count] = child;
1618                    changed = true;
1619                }
1620                None => {
1621                    Rc::make_mut(&mut self.last_placements).push(child);
1622                    changed = true;
1623                }
1624            }
1625            count += 1;
1626        }
1627        if self.last_placements.len() > count {
1628            Rc::make_mut(&mut self.last_placements).truncate(count);
1629            changed = true;
1630        }
1631        if changed && let Some(id) = self.node_id {
1632            crate::render_state::record_geometry_scene_node(id);
1633        }
1634    }
1635
1636    fn new(measure_policy: Rc<MeasurePolicy>) -> Self {
1637        Self {
1638            modifier: Modifier::empty(),
1639            modifier_chain: ModifierChainHandle::new(),
1640            resolved_modifiers: ResolvedModifiers::default(),
1641            modifier_capabilities: NodeCapabilities::default(),
1642            state: SubcomposeState::default(),
1643            measure_policy,
1644            children: Vec::new(),
1645            slots: Rc::new(SlotsHost::new(SlotTable::default())),
1646            debug_modifiers: false,
1647            node_id: None,
1648            last_placements: Rc::new(Vec::new()),
1649            placement_scratch: Vec::new(),
1650            measured_children_scratch: Rc::new(RefCell::new(HashMap::default())),
1651            captured_context: None,
1652            density: crate::density::Density::default(),
1653        }
1654    }
1655
1656    fn set_measure_policy(&mut self, policy: Rc<MeasurePolicy>) {
1657        self.measure_policy = policy;
1658        if let Err(err) = self.slots.reset() {
1659            log::error!(
1660                "failed to reset root measurement slots after measure policy update: {err}"
1661            );
1662        }
1663    }
1664
1665    fn set_modifier_collect(&mut self, modifier: Modifier) -> (ModifierInvalidations, bool) {
1666        let modifier_changed = !self.modifier.structural_eq(&modifier);
1667        self.modifier = modifier;
1668        self.modifier_chain.set_debug_logging(self.debug_modifiers);
1669        let invalidations = self
1670            .modifier_chain
1671            .update_on_node(self.node_id, &self.modifier);
1672        self.resolved_modifiers = self.modifier_chain.resolved_modifiers();
1673        self.modifier_capabilities = self.modifier_chain.capabilities();
1674
1675        (invalidations, modifier_changed)
1676    }
1677
1678    fn set_debug_modifiers(&mut self, enabled: bool) {
1679        self.debug_modifiers = enabled;
1680        self.modifier_chain.set_debug_logging(enabled);
1681    }
1682}
1683
1684#[cfg(test)]
1685#[path = "tests/subcompose_layout_tests.rs"]
1686mod tests;