Skip to main content

cranpose_ui/
subcompose_layout.rs

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