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::{InvalidationKind, ModifierInvalidation, NodeCapabilities};
11pub use cranpose_ui_layout::{Constraints, MeasureResult, Placement};
12use smallvec::SmallVec;
13use web_time::Instant;
14
15use crate::{
16    layout::MeasuredNode,
17    modifier::{
18        Modifier, ModifierChainHandle, ModifierNodeSlices, Point, ResolvedModifiers, Size,
19        collect_modifier_slices_into,
20    },
21    widgets::nodes::{
22        LayoutNode, LayoutNodeCacheHandles, LayoutState, allocate_virtual_node_id, is_virtual_node,
23        register_layout_node,
24    },
25};
26
27fn subcompose_telemetry_enabled() -> bool {
28    cranpose_core::env_flag!("CRANPOSE_SUBCOMPOSE_TELEMETRY")
29}
30
31/// Representation of a subcomposed child that can later be measured by the policy.
32///
33/// In lazy layouts, this represents an item that has been composed but may or
34/// may not have been measured yet. Call `measure()` to get the actual size.
35#[derive(Clone, Copy, Debug)]
36pub struct SubcomposeChild {
37    node_id: NodeId,
38    /// Measured size of the child (set after measurement).
39    /// Width in x, height in y.
40    measured_size: Option<Size>,
41}
42
43impl SubcomposeChild {
44    pub fn new(node_id: NodeId) -> Self {
45        Self {
46            node_id,
47            measured_size: None,
48        }
49    }
50
51    /// Creates a SubcomposeChild with a known size.
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    /// Returns the measured size of this child.
64    ///
65    /// Returns a default size if the child hasn't been measured yet.
66    /// For lazy layouts using placeholder sizes, this returns the estimated size.
67    pub fn size(&self) -> Size {
68        self.measured_size.unwrap_or(Size {
69            width: 0.0,
70            height: 0.0,
71        })
72    }
73
74    /// Returns the measured width.
75    pub fn width(&self) -> f32 {
76        self.size().width
77    }
78
79    /// Returns the measured height.
80    pub fn height(&self) -> f32 {
81        self.size().height
82    }
83
84    /// Sets the measured size for this child.
85    pub fn set_size(&mut self, size: Size) {
86        self.measured_size = Some(size);
87    }
88}
89
90impl PartialEq for SubcomposeChild {
91    fn eq(&self, other: &Self) -> bool {
92        self.node_id == other.node_id
93    }
94}
95
96/// A measured child that is ready to be placed.
97///
98/// This is a type alias for the concrete `Placeable` struct from `cranpose_ui_layout`.
99/// In subcompose layouts, placement is handled by returning a list of `Placement`s,
100/// so the `place()` callback is not used.
101pub type SubcomposePlaceable = cranpose_ui_layout::Placeable;
102
103type CachedMeasureBatchRegistrar<'a> =
104    Box<dyn FnMut(&[NodeId], Constraints, &mut Vec<Option<Size>>) + 'a>;
105type RetainedMeasureLookup<'a> = Box<dyn FnMut(NodeId) -> Option<Rc<MeasuredNode>> + 'a>;
106type RetainedMeasureRegistrar<'a> = Box<dyn FnMut(&[Rc<MeasuredNode>]) + 'a>;
107
108pub(crate) struct CachedBatchMeasureInputs<'a> {
109    pub(crate) measurer: Box<dyn FnMut(NodeId, Constraints) -> Size + 'a>,
110    pub(crate) cached_measure_batch_registrar: CachedMeasureBatchRegistrar<'a>,
111    pub(crate) retained_measure_lookup: RetainedMeasureLookup<'a>,
112    pub(crate) retained_measure_registrar: RetainedMeasureRegistrar<'a>,
113    pub(crate) error: &'a RefCell<Option<NodeError>>,
114}
115
116/// Base trait for measurement scopes.
117pub trait SubcomposeLayoutScope: cranpose_ui_layout::MeasureScope {
118    fn constraints(&self) -> Constraints;
119
120    fn layout<I>(&mut self, width: f32, height: f32, placements: I) -> MeasureResult
121    where
122        I: IntoIterator<Item = Placement>,
123    {
124        MeasureResult::new(Size { width, height }, placements.into_iter().collect())
125    }
126}
127
128/// Public trait exposed to measure policies for subcomposition.
129pub trait SubcomposeMeasureScope: SubcomposeLayoutScope {
130    fn subcompose<Content>(&mut self, slot_id: SlotId, content: Content) -> Vec<SubcomposeChild>
131    where
132        Content: FnMut() + 'static;
133
134    /// Measures a subcomposed child with the given constraints.
135    fn measure(&mut self, child: SubcomposeChild, constraints: Constraints) -> SubcomposePlaceable;
136
137    /// Checks if a node has no parent (is a root node).
138    /// Used to filter subcompose results to only include true root nodes.
139    fn node_has_no_parent(&self, node_id: NodeId) -> bool;
140}
141
142/// Concrete implementation of [`SubcomposeMeasureScope`].
143pub struct SubcomposeMeasureScopeImpl<'a> {
144    composer: Composer,
145    density_scope: crate::density::DensityMeasureScope,
146    state: &'a mut SubcomposeState,
147    constraints: Constraints,
148    measurer: Box<dyn FnMut(NodeId, Constraints) -> Size + 'a>,
149    cached_measure_batch_registrar: CachedMeasureBatchRegistrar<'a>,
150    retained_measure_lookup: RetainedMeasureLookup<'a>,
151    retained_measure_registrar: RetainedMeasureRegistrar<'a>,
152    error: &'a RefCell<Option<NodeError>>,
153    parent_handle: SubcomposeLayoutNodeHandle,
154    root_id: NodeId,
155    placement_scratch: Vec<Placement>,
156    cached_measure_node_scratch: Vec<NodeId>,
157    cached_measure_size_scratch: Vec<Option<Size>>,
158    cached_measure_missing_scratch: Vec<NodeId>,
159    registered_measurement_node_ids: Vec<NodeId>,
160    pending_commands_applied: bool,
161}
162
163struct SubcomposeMeasureScopeInit<'a> {
164    composer: Composer,
165    density: crate::density::Density,
166    state: &'a mut SubcomposeState,
167    constraints: Constraints,
168    measurer: Box<dyn FnMut(NodeId, Constraints) -> Size + 'a>,
169    cached_measure_batch_registrar: CachedMeasureBatchRegistrar<'a>,
170    retained_measure_lookup: RetainedMeasureLookup<'a>,
171    retained_measure_registrar: RetainedMeasureRegistrar<'a>,
172    error: &'a RefCell<Option<NodeError>>,
173    parent_handle: SubcomposeLayoutNodeHandle,
174    root_id: NodeId,
175    placement_scratch: Vec<Placement>,
176}
177
178impl<'a> SubcomposeMeasureScopeImpl<'a> {
179    fn new(init: SubcomposeMeasureScopeInit<'a>) -> Self {
180        Self {
181            composer: init.composer,
182            density_scope: crate::density::DensityMeasureScope::new(init.density),
183            state: init.state,
184            constraints: init.constraints,
185            measurer: init.measurer,
186            cached_measure_batch_registrar: init.cached_measure_batch_registrar,
187            retained_measure_lookup: init.retained_measure_lookup,
188            retained_measure_registrar: init.retained_measure_registrar,
189            error: init.error,
190            parent_handle: init.parent_handle,
191            root_id: init.root_id,
192            placement_scratch: init.placement_scratch,
193            cached_measure_node_scratch: Vec::new(),
194            cached_measure_size_scratch: Vec::new(),
195            cached_measure_missing_scratch: Vec::new(),
196            registered_measurement_node_ids: Vec::new(),
197            pending_commands_applied: false,
198        }
199    }
200
201    fn register_measurement_node_id(&mut self, node_id: NodeId) {
202        if !self.registered_measurement_node_ids.contains(&node_id) {
203            self.registered_measurement_node_ids.push(node_id);
204        }
205    }
206
207    fn into_placement_scratch(self) -> Vec<Placement> {
208        self.placement_scratch
209    }
210
211    pub(crate) fn layout_with_placement_builder(
212        &mut self,
213        width: f32,
214        height: f32,
215        build: impl FnOnce(&mut Vec<Placement>),
216    ) -> MeasureResult {
217        self.placement_scratch.clear();
218        build(&mut self.placement_scratch);
219        MeasureResult::new(
220            Size { width, height },
221            std::mem::take(&mut self.placement_scratch),
222        )
223    }
224
225    fn record_error(&self, err: NodeError) {
226        let mut slot = self.error.borrow_mut();
227        if slot.is_none() {
228            eprintln!("[SubcomposeLayout] Error suppressed: {:?}", err);
229            *slot = Some(err);
230        }
231    }
232
233    fn ensure_pending_commands_applied(&mut self) -> bool {
234        if self.pending_commands_applied {
235            return true;
236        }
237
238        let telemetry_start = subcompose_telemetry_enabled().then(Instant::now);
239        if let Err(err) = self.composer.apply_pending_commands() {
240            self.record_error(err);
241            return false;
242        }
243        if let Some(start) = telemetry_start {
244            log::warn!(
245                "[subcompose-telemetry] apply_pending_commands_ms={:.2}",
246                start.elapsed().as_secs_f64() * 1000.0
247            );
248        }
249
250        self.pending_commands_applied = true;
251        true
252    }
253
254    fn perform_subcompose<Content>(&mut self, slot_id: SlotId, content: Content) -> Vec<NodeId>
255    where
256        Content: FnMut() + 'static,
257    {
258        let telemetry_start = subcompose_telemetry_enabled().then(Instant::now);
259        let mut inner = self.parent_handle.inner.borrow_mut();
260
261        // Reuse or create virtual node
262        let (virtual_node_id, is_reused) =
263            if let Some(node_id) = self.state.take_node_from_reusables(slot_id) {
264                (node_id, true)
265            } else {
266                let id = allocate_virtual_node_id();
267                let node = LayoutNode::new_virtual();
268                // CRITICAL FIX: Register virtual node in Applier so that insert_child commands
269                // can find it. Previously, virtual nodes were only stored in inner.virtual_nodes
270                // which caused applier.get_mut(virtual_node_id) to fail, breaking child attachment.
271                if let Err(e) = self
272                    .composer
273                    .register_virtual_node(id, Box::new(node.clone()))
274                {
275                    eprintln!(
276                        "[Subcompose] Failed to register virtual node {}: {:?}",
277                        id, e
278                    );
279                }
280                register_layout_node(id, &node);
281
282                inner.virtual_nodes.insert(id, Rc::new(node));
283                inner.children.push(id);
284                (id, false)
285            };
286
287        // Keep the slot root in the outer parent frame so SyncChildren preserves
288        // the virtual subtree instead of deleting it after subcomposition.
289        self.composer.record_subcompose_child(virtual_node_id);
290
291        // Keep both the retained virtual-node clone and the applier copy wired to the
292        // live subcompose root so bubbling can cross the virtual slot boundary.
293        if let Some(v_node) = inner.virtual_nodes.get(&virtual_node_id) {
294            v_node.set_parent(self.root_id);
295        }
296
297        drop(inner);
298
299        let content_holder = self.state.callback_holder(slot_id);
300        content_holder.update(content);
301
302        let _ = self
303            .composer
304            .with_node_mut::<LayoutNode, _>(virtual_node_id, |node| {
305                node.set_parent(self.root_id);
306            });
307
308        let slot_host = self.state.get_or_create_slots(slot_id);
309        self.parent_handle.note_slot_host(&slot_host);
310        let holder_for_slot = content_holder.clone();
311        let scopes = self
312            .composer
313            .subcompose_slot(&slot_host, Some(virtual_node_id), move |_| {
314                compose_subcompose_slot_content(holder_for_slot.clone());
315            })
316            .map(|(_, scopes)| scopes)
317            .unwrap_or_default();
318        self.pending_commands_applied = false;
319
320        self.state
321            .register_active(slot_id, &[virtual_node_id], &scopes);
322
323        // CRITICAL FIX: Read children from the Applier's copy of the virtual node,
324        // NOT from inner.virtual_nodes. The Applier's copy received insert_child calls
325        // during subcomposition, while inner.virtual_nodes is an out-of-sync clone.
326        let children = self.composer.get_node_children(virtual_node_id).to_vec();
327        if let Some(start) = telemetry_start {
328            log::warn!(
329                "[subcompose-telemetry] slot={} reused={} children={} subcompose_ms={:.2}",
330                slot_id.raw(),
331                is_reused,
332                children.len(),
333                start.elapsed().as_secs_f64() * 1000.0
334            );
335        }
336        children
337    }
338
339    pub(crate) fn activate_exact_retained_slot_with_known_children(
340        &mut self,
341        slot_id: SlotId,
342        known_children: &[u64],
343    ) -> Option<(Vec<SubcomposeChild>, bool)> {
344        let mut expected_children = Vec::with_capacity(known_children.len());
345        for &node_id in known_children {
346            expected_children.push(NodeId::try_from(node_id).ok()?);
347        }
348
349        let virtual_node_ids = match self.activate_current_active_slot_roots(slot_id) {
350            Some(virtual_node_ids) => {
351                for virtual_node_id in &virtual_node_ids {
352                    self.composer.record_subcompose_child(*virtual_node_id);
353                }
354                virtual_node_ids
355            }
356            None => self.activate_recycled_exact_retained_slot_roots(slot_id)?,
357        };
358
359        // Always read the LIVE roots out of the applier and compare them against
360        // the caller's cached list. Reporting `children_match = true` from the
361        // cached ids alone is a lie whenever the slot recomposed and emitted a
362        // different set of root children: the caller would reuse the cached
363        // measurement, and any newly composed root would never be measured or
364        // placed.
365        //
366        // Flush queued composer commands first so the read sees the tree the
367        // last recomposition actually produced. Every caller runs this on the
368        // very next statement (`children_need_relayout`), and it is a no-op once
369        // applied, so this only moves the flush one statement earlier.
370        if !self.ensure_pending_commands_applied() {
371            return None;
372        }
373
374        let mut activated_children = Vec::with_capacity(expected_children.len());
375        for virtual_node_id in virtual_node_ids {
376            activated_children.extend(
377                self.composer
378                    .get_node_children(virtual_node_id)
379                    .iter()
380                    .copied(),
381            );
382        }
383        let children_match = activated_children == expected_children;
384        Some((
385            activated_children
386                .into_iter()
387                .map(SubcomposeChild::new)
388                .collect(),
389            children_match,
390        ))
391    }
392
393    fn activate_current_active_slot_roots(&mut self, slot_id: SlotId) -> Option<Vec<NodeId>> {
394        self.state.activate_current_active_slot(slot_id)
395    }
396
397    fn activate_recycled_exact_retained_slot_roots(
398        &mut self,
399        slot_id: SlotId,
400    ) -> Option<Vec<NodeId>> {
401        let activation = self.state.take_exact_slot_activation(slot_id)?;
402        let virtual_node_ids = activation.nodes;
403        let scopes = activation.scopes;
404        let reactivate_scopes = activation.reactivate_scopes;
405
406        if reactivate_scopes {
407            let inner = self.parent_handle.inner.borrow();
408            for virtual_node_id in &virtual_node_ids {
409                self.composer.record_subcompose_child(*virtual_node_id);
410                if let Some(v_node) = inner.virtual_nodes.get(virtual_node_id) {
411                    v_node.set_parent(self.root_id);
412                }
413            }
414            for virtual_node_id in &virtual_node_ids {
415                let _ = self
416                    .composer
417                    .with_node_mut::<LayoutNode, _>(*virtual_node_id, |node| {
418                        node.set_parent(self.root_id);
419                    });
420            }
421        } else {
422            for virtual_node_id in &virtual_node_ids {
423                self.composer.record_subcompose_child(*virtual_node_id);
424            }
425        }
426
427        self.state.register_active_with_scope_reactivation(
428            slot_id,
429            &virtual_node_ids,
430            &scopes,
431            reactivate_scopes,
432        );
433        Some(virtual_node_ids)
434    }
435}
436
437impl<'a> SubcomposeLayoutScope for SubcomposeMeasureScopeImpl<'a> {
438    fn constraints(&self) -> Constraints {
439        self.constraints
440    }
441
442    fn layout<I>(&mut self, width: f32, height: f32, placements: I) -> MeasureResult
443    where
444        I: IntoIterator<Item = Placement>,
445    {
446        self.layout_with_placement_builder(width, height, |scratch| {
447            scratch.extend(placements);
448        })
449    }
450}
451
452impl cranpose_ui_layout::MeasureScope for SubcomposeMeasureScopeImpl<'_> {
453    // The grid is captured onto the node by `SubcomposeLayout` when the
454    // composition that asked for the subcomposition ran -- the same moment
455    // `Layout` captures a density onto `LayoutNode` for the plain path -- and
456    // carried into the scope from there. A plain field read cannot panic and
457    // cannot disagree with whichever composer the thread happens to be inside.
458    fn density(&self) -> f32 {
459        self.density_scope.density()
460    }
461
462    fn font_scale(&self) -> f32 {
463        self.density_scope.font_scale()
464    }
465}
466
467impl<'a> SubcomposeMeasureScope for SubcomposeMeasureScopeImpl<'a> {
468    fn subcompose<Content>(&mut self, slot_id: SlotId, content: Content) -> Vec<SubcomposeChild>
469    where
470        Content: FnMut() + 'static,
471    {
472        let nodes = self.perform_subcompose(slot_id, content);
473        nodes.into_iter().map(SubcomposeChild::new).collect()
474    }
475
476    fn measure(&mut self, child: SubcomposeChild, constraints: Constraints) -> SubcomposePlaceable {
477        if self.error.borrow().is_some() {
478            // Already in error state - return zero-size placeable
479            return SubcomposePlaceable::value(0.0, 0.0, child.node_id);
480        }
481
482        let telemetry_start = subcompose_telemetry_enabled().then(Instant::now);
483        if !self.ensure_pending_commands_applied() {
484            return SubcomposePlaceable::value(0.0, 0.0, child.node_id);
485        }
486
487        let size = (self.measurer)(child.node_id, constraints);
488        self.register_measurement_node_id(child.node_id);
489        if let Some(start) = telemetry_start {
490            log::warn!(
491                "[subcompose-telemetry] child={} measure_ms={:.2} size=({:.2},{:.2})",
492                child.node_id,
493                start.elapsed().as_secs_f64() * 1000.0,
494                size.width,
495                size.height
496            );
497        }
498        SubcomposePlaceable::value(size.width, size.height, child.node_id)
499    }
500
501    fn node_has_no_parent(&self, node_id: NodeId) -> bool {
502        self.composer.node_has_no_parent(node_id)
503    }
504}
505
506impl<'a> SubcomposeMeasureScopeImpl<'a> {
507    /// Returns the number of active slots in the subcompose state.
508    ///
509    /// Used by lazy layouts to report statistics about slot usage.
510    pub fn active_slots_count(&self) -> usize {
511        self.state.active_slots_count()
512    }
513
514    /// Returns the number of reusable slots in the pool.
515    ///
516    /// Used by lazy layouts to report statistics about cached slots.
517    pub fn reusable_slots_count(&self) -> usize {
518        self.state.reusable_slots_count()
519    }
520
521    /// Registers the content type for a slot.
522    ///
523    /// Call this before `subcompose()` to enable content-type-aware slot reuse.
524    /// If the policy supports content types (like `ContentTypeReusePolicy`),
525    /// slots with matching content types can reuse each other's nodes.
526    pub fn register_content_type(&mut self, slot_id: SlotId, content_type: u64) {
527        self.state.register_content_type(slot_id, content_type);
528    }
529
530    /// Updates the content type for a slot, handling Some→None transitions.
531    ///
532    /// If `content_type` is `Some(type)`, registers the type for the slot.
533    /// If `content_type` is `None`, removes any previously registered type.
534    /// This ensures stale types don't drive incorrect reuse after transitions.
535    pub fn update_content_type(&mut self, slot_id: SlotId, content_type: Option<u64>) {
536        self.state.update_content_type(slot_id, content_type);
537    }
538
539    pub(crate) fn set_reusable_pool_limits(&mut self, per_type: usize, untyped: usize) {
540        self.state.set_reusable_pool_limits(per_type, untyped);
541    }
542
543    pub(crate) fn recycle_active_slots_where(&mut self, predicate: impl FnMut(SlotId) -> bool) {
544        let disposed = self.state.recycle_active_slots_where(predicate);
545        debug_assert!(
546            disposed.is_empty(),
547            "lazy subcompose reusable pool limits must retain recycled active slots"
548        );
549    }
550
551    /// Returns whether the last subcomposed slot was reused.
552    ///
553    /// Returns `Some(true)` if the slot already existed (was reused from pool or
554    /// was recomposed), `Some(false)` if it was newly created, or `None` if no
555    /// slot has been subcomposed yet this pass.
556    ///
557    /// This is useful for tracking composition statistics in lazy layouts.
558    pub fn was_last_slot_reused(&self) -> Option<bool> {
559        self.state.was_last_slot_reused()
560    }
561
562    pub(crate) fn measure_retained(
563        &mut self,
564        child: SubcomposeChild,
565        constraints: Constraints,
566    ) -> (SubcomposePlaceable, Option<Rc<MeasuredNode>>) {
567        let placeable = self.measure(child, constraints);
568        let retained = (self.retained_measure_lookup)(child.node_id);
569        (placeable, retained)
570    }
571
572    pub(crate) fn register_retained_measurements(&mut self, measurements: &[Rc<MeasuredNode>]) {
573        if measurements.is_empty() {
574            return;
575        }
576
577        for measured in measurements {
578            self.register_measurement_node_id(measured.node_id());
579        }
580        (self.retained_measure_registrar)(measurements);
581    }
582
583    /// Whether any slot root child still owes layout work — either a re-measure
584    /// (`needs_measure`) or a placement-only repass (`needs_layout`).
585    ///
586    /// Both must be checked. `schedule_layout_repass` (scroll offsets, text
587    /// field panning) bubbles `needs_layout` *without* `needs_measure` on
588    /// purpose, so that reaching the root does not bump the global layout cache
589    /// epoch and wipe every cached measurement in the app. A caller that only
590    /// asked "do you need measuring?" would call such a subtree clean and reuse
591    /// the cached measurement, freezing the scroll on screen.
592    ///
593    /// Stays O(number of slot roots): dirty bubbling already lifted any
594    /// descendant's dirtiness onto these roots, so no subtree walk is needed.
595    pub(crate) fn children_need_relayout(&mut self, children: &[SubcomposeChild]) -> bool {
596        if !self.ensure_pending_commands_applied() {
597            return true;
598        }
599
600        let mut root_ids = smallvec::SmallVec::<[NodeId; 8]>::new();
601        root_ids.extend(children.iter().map(SubcomposeChild::node_id));
602        self.composer.nodes_need_measure(&root_ids) || self.composer.nodes_need_layout(&root_ids)
603    }
604
605    pub(crate) fn ensure_cached_measurement_node_ids<I>(
606        &mut self,
607        node_ids: I,
608        constraints: Constraints,
609    ) -> usize
610    where
611        I: IntoIterator<Item = NodeId>,
612    {
613        if self.error.borrow().is_some() || !self.ensure_pending_commands_applied() {
614            return 0;
615        }
616
617        self.cached_measure_node_scratch.clear();
618        self.cached_measure_node_scratch.extend(
619            node_ids
620                .into_iter()
621                .filter(|node_id| !self.registered_measurement_node_ids.contains(node_id)),
622        );
623        if self.cached_measure_node_scratch.is_empty() {
624            return 0;
625        }
626
627        self.cached_measure_size_scratch.clear();
628        (self.cached_measure_batch_registrar)(
629            &self.cached_measure_node_scratch,
630            constraints,
631            &mut self.cached_measure_size_scratch,
632        );
633        self.cached_measure_size_scratch
634            .resize(self.cached_measure_node_scratch.len(), None);
635
636        let mut cached_count = 0;
637        self.cached_measure_missing_scratch.clear();
638        for index in 0..self.cached_measure_node_scratch.len() {
639            let node_id = self.cached_measure_node_scratch[index];
640            if self.cached_measure_size_scratch[index].is_some() {
641                cached_count += 1;
642                self.register_measurement_node_id(node_id);
643            } else {
644                self.cached_measure_missing_scratch.push(node_id);
645            }
646        }
647
648        let mut missing = std::mem::take(&mut self.cached_measure_missing_scratch);
649        for node_id in missing.drain(..) {
650            let _ = self.measure(SubcomposeChild::new(node_id), constraints);
651        }
652        self.cached_measure_missing_scratch = missing;
653
654        cached_count
655    }
656}
657
658fn compose_subcompose_slot_content(holder: cranpose_core::CallbackHolder) {
659    cranpose_core::with_current_composer(|composer| {
660        let holder_for_recompose = holder.clone();
661        composer.set_recompose_callback(move |_composer| {
662            compose_subcompose_slot_content(holder_for_recompose.clone());
663        });
664    });
665
666    let invoke = holder.clone_rc();
667    invoke();
668}
669
670/// Trait object representing a reusable measure policy.
671pub type MeasurePolicy =
672    dyn for<'scope> Fn(&mut SubcomposeMeasureScopeImpl<'scope>, Constraints) -> MeasureResult;
673
674/// Node responsible for orchestrating measure-time subcomposition.
675pub struct SubcomposeLayoutNode {
676    inner: Rc<RefCell<SubcomposeLayoutNodeInner>>,
677    /// Parent tracking for dirty flag bubbling (P0.2 fix)
678    parent: Cell<Option<NodeId>>,
679    /// Node's own ID
680    id: Cell<Option<NodeId>>,
681    // Dirty flags for selective measure/layout/render
682    needs_measure: Cell<bool>,
683    needs_layout: Cell<bool>,
684    needs_semantics: Cell<bool>,
685    needs_redraw: Cell<bool>,
686    needs_pointer_pass: Cell<bool>,
687    needs_focus_sync: Cell<bool>,
688    virtual_children_count: Cell<usize>,
689    /// Retained layout state (size, position) for rendering.
690    layout_state: RefCell<LayoutState>,
691    cache_handles: LayoutNodeCacheHandles,
692    modifier_slices_snapshot: RefCell<Rc<ModifierNodeSlices>>,
693    modifier_slices_dirty: Cell<bool>,
694}
695
696impl SubcomposeLayoutNode {
697    pub fn new(modifier: Modifier, measure_policy: Rc<MeasurePolicy>) -> Self {
698        let inner = Rc::new(RefCell::new(SubcomposeLayoutNodeInner::new(measure_policy)));
699        let node = Self {
700            inner,
701            parent: Cell::new(None),
702            id: Cell::new(None),
703            needs_measure: Cell::new(true),
704            needs_layout: Cell::new(true),
705            needs_semantics: Cell::new(true),
706            needs_redraw: Cell::new(true),
707            needs_pointer_pass: Cell::new(false),
708            needs_focus_sync: Cell::new(false),
709            virtual_children_count: Cell::new(0),
710            layout_state: RefCell::new(LayoutState::default()),
711            cache_handles: LayoutNodeCacheHandles::default(),
712            modifier_slices_snapshot: RefCell::new(Rc::default()),
713            modifier_slices_dirty: Cell::new(true),
714        };
715        // Set modifier and dispatch invalidations after borrow is released
716        // Pass empty prev_caps since this is initial construction
717        let (invalidations, _) = node.inner.borrow_mut().set_modifier_collect(modifier);
718        node.dispatch_modifier_invalidations(&invalidations, NodeCapabilities::empty());
719        node.update_modifier_slices_cache();
720        node.note_host_to_the_composition_that_made_it();
721        node
722    }
723
724    fn note_host_to_the_composition_that_made_it(&self) {
725        let host = Rc::clone(&self.inner.borrow().slots);
726        cranpose_core::note_nested_slots_host(&host);
727    }
728
729    /// Creates a SubcomposeLayoutNode with ContentTypeReusePolicy.
730    ///
731    /// Use this for lazy lists to enable content-type-aware slot reuse.
732    /// Slots with matching content types can reuse each other's nodes,
733    /// improving efficiency when scrolling through items with different types.
734    pub fn with_content_type_policy(modifier: Modifier, measure_policy: Rc<MeasurePolicy>) -> Self {
735        let mut inner_data = SubcomposeLayoutNodeInner::new(measure_policy);
736        inner_data
737            .state
738            .set_policy(Box::new(cranpose_core::ContentTypeReusePolicy::new()));
739        let inner = Rc::new(RefCell::new(inner_data));
740        let node = Self {
741            inner,
742            parent: Cell::new(None),
743            id: Cell::new(None),
744            needs_measure: Cell::new(true),
745            needs_layout: Cell::new(true),
746            needs_semantics: Cell::new(true),
747            needs_redraw: Cell::new(true),
748            needs_pointer_pass: Cell::new(false),
749            needs_focus_sync: Cell::new(false),
750            virtual_children_count: Cell::new(0),
751            layout_state: RefCell::new(LayoutState::default()),
752            cache_handles: LayoutNodeCacheHandles::default(),
753            modifier_slices_snapshot: RefCell::new(Rc::default()),
754            modifier_slices_dirty: Cell::new(true),
755        };
756        // Set modifier and dispatch invalidations after borrow is released
757        // Pass empty prev_caps since this is initial construction
758        let (invalidations, _) = node.inner.borrow_mut().set_modifier_collect(modifier);
759        node.dispatch_modifier_invalidations(&invalidations, NodeCapabilities::empty());
760        node.update_modifier_slices_cache();
761        node.note_host_to_the_composition_that_made_it();
762        node
763    }
764
765    pub fn handle(&self) -> SubcomposeLayoutNodeHandle {
766        SubcomposeLayoutNodeHandle {
767            inner: Rc::clone(&self.inner),
768        }
769    }
770
771    #[doc(hidden)]
772    pub fn debug_scope_ids_by_slot(&self) -> Vec<(u64, Vec<usize>)> {
773        self.inner.borrow().state.debug_scope_ids_by_slot()
774    }
775
776    #[doc(hidden)]
777    pub fn debug_slot_table_for_slot(
778        &self,
779        slot_id: cranpose_core::SlotId,
780    ) -> Option<Vec<cranpose_core::SlotDebugEntry>> {
781        self.inner.borrow().state.debug_slot_table_for_slot(slot_id)
782    }
783
784    #[doc(hidden)]
785    pub fn debug_slot_table_groups_for_slot(
786        &self,
787        slot_id: cranpose_core::SlotId,
788    ) -> Option<Vec<cranpose_core::subcompose::DebugSlotGroup>> {
789        self.inner
790            .borrow()
791            .state
792            .debug_slot_table_groups_for_slot(slot_id)
793    }
794
795    pub fn set_measure_policy(&mut self, policy: Rc<MeasurePolicy>) {
796        let mut inner = self.inner.borrow_mut();
797        if Rc::ptr_eq(&inner.measure_policy, &policy) {
798            return;
799        }
800        inner.set_measure_policy(policy);
801        drop(inner);
802        self.invalidate_subcomposition();
803    }
804
805    /// Records the source composition context for measure-time subcomposition.
806    pub fn set_captured_context(&mut self, context: cranpose_core::CapturedCompositionContext) {
807        self.inner.borrow_mut().captured_context = Some(context);
808    }
809
810    /// Records the grid the composition provided, re-measuring if it moved.
811    ///
812    /// Mirrors [`LayoutNode::set_density`](crate::widgets::nodes::LayoutNode::set_density):
813    /// `SubcomposeLayout` captures this at the same composition site that
814    /// captures the subcomposition context, so the two cannot disagree.
815    pub fn set_density(&mut self, density: crate::density::Density) {
816        let mut inner = self.inner.borrow_mut();
817        if inner.density != density {
818            inner.density = density;
819            drop(inner);
820            self.mark_needs_measure();
821        }
822    }
823
824    pub fn set_modifier(&mut self, modifier: Modifier) {
825        // Capture capabilities BEFORE updating to detect removed modifiers
826        let prev_caps = self.modifier_capabilities();
827        // Collect invalidations while inner is borrowed, then dispatch after release
828        let (invalidations, modifier_changed) = {
829            let mut inner = self.inner.borrow_mut();
830            inner.set_modifier_collect(modifier)
831        };
832        // Now dispatch invalidations after the borrow is released
833        // Pass both prev and curr caps so removed modifiers still trigger invalidation
834        self.dispatch_modifier_invalidations(&invalidations, prev_caps);
835        self.update_modifier_slices_cache();
836        if modifier_changed {
837            self.request_semantics_update();
838        }
839    }
840
841    /// Updates the cached modifier slices from the modifier chain.
842    fn update_modifier_slices_cache(&self) {
843        let inner = self.inner.borrow();
844        let mut snapshot = self.modifier_slices_snapshot.borrow_mut();
845        collect_modifier_slices_into(inner.modifier_chain.chain(), Rc::make_mut(&mut snapshot));
846        self.modifier_slices_dirty.set(false);
847    }
848
849    pub(crate) fn mark_modifier_slices_dirty(&self) {
850        self.modifier_slices_dirty.set(true);
851    }
852
853    pub fn set_debug_modifiers(&mut self, enabled: bool) {
854        self.inner.borrow_mut().set_debug_modifiers(enabled);
855    }
856
857    pub fn modifier(&self) -> Modifier {
858        self.handle().modifier()
859    }
860
861    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
862        self.inner.borrow().resolved_modifiers
863    }
864
865    /// Returns a clone of the current layout state.
866    pub fn layout_state(&self) -> LayoutState {
867        self.layout_state.borrow().clone()
868    }
869
870    pub(crate) fn cache_handles(&self) -> LayoutNodeCacheHandles {
871        self.cache_handles.clone()
872    }
873
874    /// Updates the position of this node. Called during placement.
875    pub fn set_position(&self, position: Point) {
876        let mut state = self.layout_state.borrow_mut();
877        state.position = position;
878        state.is_placed = true;
879    }
880
881    /// Updates the measured size of this node. Called during measurement.
882    pub fn set_measured_size(&self, size: Size) {
883        let mut state = self.layout_state.borrow_mut();
884        state.size = size;
885    }
886
887    /// Clears the is_placed flag. Called at the start of a layout pass.
888    pub fn clear_placed(&self) {
889        self.layout_state.borrow_mut().is_placed = false;
890    }
891
892    /// Returns the modifier slices snapshot for rendering.
893    pub fn modifier_slices_snapshot(&self) -> Rc<ModifierNodeSlices> {
894        if self.modifier_slices_dirty.get() {
895            self.update_modifier_slices_cache();
896        }
897        self.modifier_slices_snapshot.borrow().clone()
898    }
899
900    pub fn state(&self) -> Ref<'_, SubcomposeState> {
901        Ref::map(self.inner.borrow(), |inner| &inner.state)
902    }
903
904    pub fn state_mut(&self) -> RefMut<'_, SubcomposeState> {
905        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.state)
906    }
907
908    pub fn invalidate_subcomposition(&self) {
909        self.inner.borrow().state.invalidate_scopes();
910        self.mark_needs_measure();
911        if let Some(id) = self.id.get() {
912            cranpose_core::bubble_measure_dirty_in_composer(id);
913        }
914    }
915
916    pub fn request_measure_recompose(&self) {
917        self.mark_needs_measure();
918        if let Some(id) = self.id.get() {
919            cranpose_core::bubble_measure_dirty_in_composer(id);
920        }
921    }
922
923    pub fn active_children(&self) -> Vec<NodeId> {
924        current_subcompose_children(&self.inner.borrow())
925    }
926
927    /// Mark this node as needing measure. Also marks it as needing layout.
928    pub fn mark_needs_measure(&self) {
929        self.needs_measure.set(true);
930        self.needs_layout.set(true);
931    }
932
933    /// Mark this node as needing layout (but not necessarily measure).
934    pub fn mark_needs_layout_flag(&self) {
935        self.needs_layout.set(true);
936    }
937
938    /// Mark this node as needing redraw without forcing measure/layout.
939    pub fn mark_needs_redraw(&self) {
940        self.needs_redraw.set(true);
941        if let Some(id) = self.id.get() {
942            crate::schedule_draw_repass(id);
943        }
944        crate::request_render_invalidation();
945    }
946
947    /// Check if this node needs measure.
948    pub fn needs_measure(&self) -> bool {
949        self.needs_measure.get()
950    }
951
952    pub(crate) fn clear_needs_measure(&self) {
953        self.needs_measure.set(false);
954    }
955
956    pub(crate) fn clear_needs_layout(&self) {
957        self.needs_layout.set(false);
958    }
959
960    /// Mark this node as needing semantics recomputation.
961    pub fn mark_needs_semantics(&self) {
962        self.needs_semantics.set(true);
963    }
964
965    pub(crate) fn clear_needs_semantics(&self) {
966        self.needs_semantics.set(false);
967    }
968
969    #[cfg(test)]
970    pub(crate) fn clear_needs_semantics_for_tests(&self) {
971        self.clear_needs_semantics();
972    }
973
974    /// Returns true when this node requested a redraw since the last render pass.
975    pub fn needs_redraw(&self) -> bool {
976        self.needs_redraw.get()
977    }
978
979    pub fn clear_needs_redraw(&self) {
980        self.needs_redraw.set(false);
981    }
982
983    /// Marks this node as needing a fresh pointer-input pass.
984    pub fn mark_needs_pointer_pass(&self) {
985        self.needs_pointer_pass.set(true);
986    }
987
988    /// Returns true when pointer-input state needs to be recomputed.
989    pub fn needs_pointer_pass(&self) -> bool {
990        self.needs_pointer_pass.get()
991    }
992
993    /// Clears the pointer-input dirty flag after hosts service it.
994    pub fn clear_needs_pointer_pass(&self) {
995        self.needs_pointer_pass.set(false);
996    }
997
998    /// Marks this node as needing a focus synchronization.
999    pub fn mark_needs_focus_sync(&self) {
1000        self.needs_focus_sync.set(true);
1001    }
1002
1003    /// Returns true when focus state needs to be synchronized.
1004    pub fn needs_focus_sync(&self) -> bool {
1005        self.needs_focus_sync.get()
1006    }
1007
1008    /// Clears the focus dirty flag after the focus manager processes it.
1009    pub fn clear_needs_focus_sync(&self) {
1010        self.needs_focus_sync.set(false);
1011    }
1012
1013    fn request_semantics_update(&self) {
1014        let already_dirty = self.needs_semantics.replace(true);
1015        if already_dirty {
1016            return;
1017        }
1018
1019        if let Some(id) = self.id.get() {
1020            cranpose_core::queue_semantics_invalidation(id);
1021        }
1022    }
1023
1024    /// Returns the modifier capabilities for this node.
1025    pub fn modifier_capabilities(&self) -> NodeCapabilities {
1026        self.inner.borrow().modifier_capabilities
1027    }
1028
1029    pub fn has_layout_modifier_nodes(&self) -> bool {
1030        self.modifier_capabilities()
1031            .contains(NodeCapabilities::LAYOUT)
1032    }
1033
1034    pub fn has_draw_modifier_nodes(&self) -> bool {
1035        self.modifier_capabilities()
1036            .contains(NodeCapabilities::DRAW)
1037    }
1038
1039    pub fn has_pointer_input_modifier_nodes(&self) -> bool {
1040        self.modifier_capabilities()
1041            .contains(NodeCapabilities::POINTER_INPUT)
1042    }
1043
1044    pub fn has_semantics_modifier_nodes(&self) -> bool {
1045        self.modifier_capabilities()
1046            .contains(NodeCapabilities::SEMANTICS)
1047    }
1048
1049    pub fn has_focus_modifier_nodes(&self) -> bool {
1050        self.modifier_capabilities()
1051            .contains(NodeCapabilities::FOCUS)
1052    }
1053
1054    /// Dispatches modifier invalidations to the appropriate subsystems.
1055    ///
1056    /// `prev_caps` contains the capabilities BEFORE the modifier update.
1057    /// Invalidations are dispatched if EITHER the previous OR current capabilities
1058    /// include the relevant type. This ensures that removing the last modifier
1059    /// of a type still triggers proper invalidation.
1060    fn dispatch_modifier_invalidations(
1061        &self,
1062        invalidations: &[ModifierInvalidation],
1063        prev_caps: NodeCapabilities,
1064    ) {
1065        let curr_caps = self.modifier_capabilities();
1066        for invalidation in invalidations {
1067            self.modifier_slices_dirty.set(true);
1068            let invalidation_caps = invalidation.capabilities();
1069            let has_capability = |capability| {
1070                curr_caps.contains(capability)
1071                    || prev_caps.contains(capability)
1072                    || invalidation_caps.contains(capability)
1073            };
1074            match invalidation.kind() {
1075                InvalidationKind::Layout => {
1076                    if has_capability(NodeCapabilities::LAYOUT) {
1077                        self.mark_needs_measure();
1078                    }
1079                }
1080                InvalidationKind::Draw => {
1081                    if has_capability(NodeCapabilities::DRAW) {
1082                        self.mark_needs_redraw();
1083                    }
1084                }
1085                InvalidationKind::PointerInput => {
1086                    if has_capability(NodeCapabilities::POINTER_INPUT) {
1087                        self.mark_needs_pointer_pass();
1088                        crate::request_pointer_invalidation();
1089                        // Schedule pointer repass for this node
1090                        if let Some(id) = self.id.get() {
1091                            crate::schedule_pointer_repass(id);
1092                        }
1093                    }
1094                }
1095                InvalidationKind::Semantics => {
1096                    self.request_semantics_update();
1097                }
1098                InvalidationKind::Focus => {
1099                    if has_capability(NodeCapabilities::FOCUS) {
1100                        self.mark_needs_focus_sync();
1101                        crate::request_focus_invalidation();
1102                        // Schedule focus invalidation for this node
1103                        if let Some(id) = self.id.get() {
1104                            crate::schedule_focus_invalidation(id);
1105                        }
1106                    }
1107                }
1108            }
1109        }
1110    }
1111}
1112
1113impl cranpose_core::Node for SubcomposeLayoutNode {
1114    fn mount(&mut self) {
1115        let mut inner = self.inner.borrow_mut();
1116        let (chain, mut context) = inner.modifier_chain.chain_and_context_mut();
1117        chain.repair_chain();
1118        chain.attach_nodes(&mut *context);
1119    }
1120
1121    fn unmount(&mut self) {
1122        self.inner
1123            .borrow_mut()
1124            .modifier_chain
1125            .chain_mut()
1126            .detach_nodes();
1127    }
1128
1129    fn insert_child(&mut self, child: NodeId) {
1130        let mut inner = self.inner.borrow_mut();
1131        if inner.children.contains(&child) {
1132            return;
1133        }
1134        if is_virtual_node(child) {
1135            let count = self.virtual_children_count.get();
1136            self.virtual_children_count.set(count + 1);
1137        }
1138        inner.children.push(child);
1139    }
1140
1141    fn remove_child(&mut self, child: NodeId) {
1142        let mut inner = self.inner.borrow_mut();
1143        let before = inner.children.len();
1144        inner.children.retain(|&id| id != child);
1145        if inner.children.len() < before && is_virtual_node(child) {
1146            let count = self.virtual_children_count.get();
1147            if count > 0 {
1148                self.virtual_children_count.set(count - 1);
1149            }
1150        }
1151    }
1152
1153    fn move_child(&mut self, from: usize, to: usize) {
1154        let mut inner = self.inner.borrow_mut();
1155        if from == to || from >= inner.children.len() {
1156            return;
1157        }
1158        let child = inner.children.remove(from);
1159        let target = to.min(inner.children.len());
1160        inner.children.insert(target, child);
1161    }
1162
1163    fn update_children(&mut self, children: &[NodeId]) {
1164        let mut inner = self.inner.borrow_mut();
1165        inner.children.clear();
1166        inner.children.extend_from_slice(children);
1167    }
1168
1169    fn children(&self) -> Vec<NodeId> {
1170        current_subcompose_children(&self.inner.borrow())
1171    }
1172
1173    fn collect_owned_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1174        out.clear();
1175        out.extend(self.inner.borrow().children.iter().copied());
1176    }
1177
1178    fn set_node_id(&mut self, id: NodeId) {
1179        self.id.set(Some(id));
1180        self.inner.borrow_mut().modifier_chain.set_node_id(Some(id));
1181        self.update_modifier_slices_cache();
1182    }
1183
1184    fn on_attached_to_parent(&mut self, parent: NodeId) {
1185        self.parent.set(Some(parent));
1186    }
1187
1188    fn on_removed_from_parent(&mut self) {
1189        self.parent.set(None);
1190    }
1191
1192    fn parent(&self) -> Option<NodeId> {
1193        self.parent.get()
1194    }
1195
1196    fn mark_needs_layout(&self) {
1197        self.needs_layout.set(true);
1198    }
1199
1200    fn needs_layout(&self) -> bool {
1201        self.needs_layout.get()
1202    }
1203
1204    fn mark_needs_measure(&self) {
1205        self.needs_measure.set(true);
1206        self.needs_layout.set(true); // Measure implies layout
1207    }
1208
1209    fn needs_measure(&self) -> bool {
1210        self.needs_measure.get()
1211    }
1212
1213    fn mark_needs_semantics(&self) {
1214        self.needs_semantics.set(true);
1215    }
1216
1217    fn needs_semantics(&self) -> bool {
1218        self.needs_semantics.get()
1219    }
1220
1221    /// Minimal parent setter for dirty flag bubbling.
1222    fn set_parent_for_bubbling(&mut self, parent: NodeId) {
1223        self.parent.set(Some(parent));
1224    }
1225}
1226
1227#[derive(Clone)]
1228pub struct SubcomposeLayoutNodeHandle {
1229    inner: Rc<RefCell<SubcomposeLayoutNodeInner>>,
1230}
1231
1232impl SubcomposeLayoutNodeHandle {
1233    pub(crate) fn note_slot_host(&self, slot_host: &Rc<cranpose_core::SlotsHost>) {
1234        let Ok(inner) = self.inner.try_borrow() else {
1235            return;
1236        };
1237        if Rc::ptr_eq(&inner.slots, slot_host) {
1238            return;
1239        }
1240        inner.slots.note_nested_host(slot_host);
1241    }
1242
1243    pub(crate) fn measured_children_scratch(
1244        &self,
1245    ) -> Rc<RefCell<HashMap<NodeId, Rc<MeasuredNode>>>> {
1246        let scratch = {
1247            let inner = self.inner.borrow();
1248            Rc::clone(&inner.measured_children_scratch)
1249        };
1250        scratch.borrow_mut().clear();
1251        scratch
1252    }
1253
1254    pub fn modifier(&self) -> Modifier {
1255        self.inner.borrow().modifier.clone()
1256    }
1257
1258    pub fn layout_properties(&self) -> crate::modifier::LayoutProperties {
1259        self.resolved_modifiers().layout_properties()
1260    }
1261
1262    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
1263        self.inner.borrow().resolved_modifiers
1264    }
1265
1266    pub fn total_offset(&self) -> Point {
1267        self.resolved_modifiers().offset()
1268    }
1269
1270    pub fn modifier_capabilities(&self) -> NodeCapabilities {
1271        self.inner.borrow().modifier_capabilities
1272    }
1273
1274    pub fn has_layout_modifier_nodes(&self) -> bool {
1275        self.modifier_capabilities()
1276            .contains(NodeCapabilities::LAYOUT)
1277    }
1278
1279    pub fn has_draw_modifier_nodes(&self) -> bool {
1280        self.modifier_capabilities()
1281            .contains(NodeCapabilities::DRAW)
1282    }
1283
1284    pub fn has_pointer_input_modifier_nodes(&self) -> bool {
1285        self.modifier_capabilities()
1286            .contains(NodeCapabilities::POINTER_INPUT)
1287    }
1288
1289    pub fn has_semantics_modifier_nodes(&self) -> bool {
1290        self.modifier_capabilities()
1291            .contains(NodeCapabilities::SEMANTICS)
1292    }
1293
1294    pub fn has_focus_modifier_nodes(&self) -> bool {
1295        self.modifier_capabilities()
1296            .contains(NodeCapabilities::FOCUS)
1297    }
1298
1299    pub fn set_debug_modifiers(&self, enabled: bool) {
1300        self.inner.borrow_mut().set_debug_modifiers(enabled);
1301    }
1302
1303    pub fn measure<'a>(
1304        &self,
1305        composer: &Composer,
1306        node_id: NodeId,
1307        constraints: Constraints,
1308        measurer: Box<dyn FnMut(NodeId, Constraints) -> Size + 'a>,
1309        mut cached_measure_registrar: Box<dyn FnMut(NodeId, Constraints) -> Option<Size> + 'a>,
1310        error: &'a RefCell<Option<NodeError>>,
1311    ) -> Result<MeasureResult, NodeError> {
1312        self.measure_with_cached_batch(
1313            composer,
1314            node_id,
1315            constraints,
1316            CachedBatchMeasureInputs {
1317                measurer,
1318                cached_measure_batch_registrar: Box::new(
1319                    move |node_ids, child_constraints, out| {
1320                        out.clear();
1321                        out.reserve(node_ids.len());
1322                        for &child_id in node_ids {
1323                            out.push(cached_measure_registrar(child_id, child_constraints));
1324                        }
1325                    },
1326                ),
1327                retained_measure_lookup: Box::new(|_| None),
1328                retained_measure_registrar: Box::new(|_| {}),
1329                error,
1330            },
1331        )
1332    }
1333
1334    pub(crate) fn measure_with_cached_batch<'a>(
1335        &self,
1336        composer: &Composer,
1337        node_id: NodeId,
1338        constraints: Constraints,
1339        callbacks: CachedBatchMeasureInputs<'a>,
1340    ) -> Result<MeasureResult, NodeError> {
1341        let CachedBatchMeasureInputs {
1342            measurer,
1343            cached_measure_batch_registrar,
1344            retained_measure_lookup,
1345            retained_measure_registrar,
1346            error,
1347        } = callbacks;
1348        let (policy, mut state, slots_host, placement_scratch, captured_context, density) = {
1349            let mut inner = self.inner.borrow_mut();
1350            let policy = Rc::clone(&inner.measure_policy);
1351            let state = std::mem::take(&mut inner.state);
1352            let slots_host = Rc::clone(&inner.slots);
1353            let placement_scratch = std::mem::take(&mut inner.placement_scratch);
1354            let captured_context = inner.captured_context.clone();
1355            let density = inner.density;
1356            (
1357                policy,
1358                state,
1359                slots_host,
1360                placement_scratch,
1361                captured_context,
1362                density,
1363            )
1364        };
1365        state.begin_pass();
1366
1367        let previous = composer.phase();
1368        if !matches!(previous, Phase::Measure | Phase::Layout) {
1369            composer.enter_phase(Phase::Measure);
1370        }
1371
1372        let constraints_copy = constraints;
1373        // Architecture Note: Using subcompose_slot (not subcompose_in) to preserve the
1374        // SlotTable across measurement passes. This matches JC's SubcomposeLayout behavior
1375        // where `subcompose()` is called during measure and the slot table persists between
1376        // frames. Without this, lazy list item groups would be wiped and recreated on every
1377        // scroll frame, causing O(visible_items) recomposition overhead ("thrashing").
1378        //
1379        // Reference: LazyLayoutMeasureScope.subcompose() in JC reuses existing slots by key,
1380        // and SubcomposeLayoutState holds `slotIdToNode` map across measurements.
1381        let fallback_context;
1382        let context = if let Some(context) = captured_context.as_ref() {
1383            context
1384        } else {
1385            fallback_context = composer.capture_composition_context();
1386            &fallback_context
1387        };
1388        let ((result, placement_scratch), _) = composer.subcompose_slot_with_context(
1389            &slots_host,
1390            Some(node_id),
1391            context,
1392            |inner_composer| {
1393                let mut scope = SubcomposeMeasureScopeImpl::new(SubcomposeMeasureScopeInit {
1394                    composer: inner_composer.clone(),
1395                    density,
1396                    state: &mut state,
1397                    constraints: constraints_copy,
1398                    measurer,
1399                    cached_measure_batch_registrar,
1400                    retained_measure_lookup,
1401                    retained_measure_registrar,
1402                    error,
1403                    parent_handle: self.clone(),
1404                    root_id: node_id,
1405                    placement_scratch,
1406                });
1407                let result = (policy)(&mut scope, constraints_copy);
1408                (result, scope.into_placement_scratch())
1409            },
1410        )?;
1411
1412        state.finish_pass();
1413
1414        if previous != composer.phase() {
1415            composer.enter_phase(previous);
1416        }
1417
1418        {
1419            let mut inner = self.inner.borrow_mut();
1420            inner.state = state;
1421            inner.placement_scratch = placement_scratch;
1422
1423            // Store placement children for children() traversal.
1424            // This avoids clearing/rebuilding the structural children set on every measure,
1425            // eliminating O(n) allocator churn. The structural children (virtual nodes) are
1426            // tracked via insert_child/remove_child, while last_placements tracks rendered nodes.
1427            inner.last_placements = result.placements.iter().map(|p| p.node_id).collect();
1428        }
1429
1430        Ok(result)
1431    }
1432
1433    pub(crate) fn recycle_placement_scratch(&self, mut placements: Vec<Placement>) {
1434        placements.clear();
1435        let mut inner = self.inner.borrow_mut();
1436        if placements.capacity() > inner.placement_scratch.capacity() {
1437            inner.placement_scratch = placements;
1438        }
1439    }
1440
1441    pub fn set_active_children<I>(&self, children: I)
1442    where
1443        I: IntoIterator<Item = NodeId>,
1444    {
1445        let mut inner = self.inner.borrow_mut();
1446        inner.last_placements.clear();
1447        inner.last_placements.extend(children);
1448    }
1449}
1450
1451fn current_subcompose_children(inner: &SubcomposeLayoutNodeInner) -> Vec<NodeId> {
1452    inner.last_placements.clone()
1453}
1454
1455struct SubcomposeLayoutNodeInner {
1456    modifier: Modifier,
1457    modifier_chain: ModifierChainHandle,
1458    resolved_modifiers: ResolvedModifiers,
1459    modifier_capabilities: NodeCapabilities,
1460    state: SubcomposeState,
1461    measure_policy: Rc<MeasurePolicy>,
1462    children: Vec<NodeId>,
1463    slots: Rc<SlotsHost>,
1464    debug_modifiers: bool,
1465    // Owns virtual nodes created during subcomposition
1466    virtual_nodes: HashMap<NodeId, Rc<LayoutNode>>,
1467    // Cached placement children from the last measure pass.
1468    // Used by children() for semantic/render traversal without clearing structural children.
1469    last_placements: Vec<NodeId>,
1470    placement_scratch: Vec<Placement>,
1471    measured_children_scratch: Rc<RefCell<HashMap<NodeId, Rc<MeasuredNode>>>>,
1472    /// Locals and source ownership captured where this node was composed.
1473    captured_context: Option<cranpose_core::CapturedCompositionContext>,
1474    /// The grid captured where this node was composed. Carried into the
1475    /// measure-time [`SubcomposeMeasureScopeImpl`] instead of being re-derived
1476    /// from ambient composer state during measurement.
1477    density: crate::density::Density,
1478}
1479
1480impl SubcomposeLayoutNodeInner {
1481    fn new(measure_policy: Rc<MeasurePolicy>) -> Self {
1482        Self {
1483            modifier: Modifier::empty(),
1484            modifier_chain: ModifierChainHandle::new(),
1485            resolved_modifiers: ResolvedModifiers::default(),
1486            modifier_capabilities: NodeCapabilities::default(),
1487            state: SubcomposeState::default(),
1488            measure_policy,
1489            children: Vec::new(),
1490            slots: Rc::new(SlotsHost::new(SlotTable::default())),
1491            debug_modifiers: false,
1492            virtual_nodes: HashMap::new(),
1493            last_placements: Vec::new(),
1494            placement_scratch: Vec::new(),
1495            measured_children_scratch: Rc::new(RefCell::new(HashMap::default())),
1496            captured_context: None,
1497            density: crate::density::Density::default(),
1498        }
1499    }
1500
1501    fn set_measure_policy(&mut self, policy: Rc<MeasurePolicy>) {
1502        self.measure_policy = policy;
1503        // The root measurement subcomposition caches its slot table separately
1504        // from per-item slot scopes. When a widget updates the data captured by
1505        // the measure lambda through shared cells, the next layout pass must not
1506        // reuse the previous root measure group wholesale.
1507        if let Err(err) = self.slots.reset() {
1508            log::error!(
1509                "failed to reset root measurement slots after measure policy update: {err}"
1510            );
1511        }
1512    }
1513
1514    /// Updates the modifier and collects invalidations without dispatching them.
1515    /// Returns the invalidations and whether the modifier changed.
1516    fn set_modifier_collect(&mut self, modifier: Modifier) -> (Vec<ModifierInvalidation>, bool) {
1517        let modifier_changed = !self.modifier.structural_eq(&modifier);
1518        self.modifier = modifier;
1519        self.modifier_chain.set_debug_logging(self.debug_modifiers);
1520        let modifier_local_invalidations = self.modifier_chain.update(&self.modifier);
1521        self.resolved_modifiers = self.modifier_chain.resolved_modifiers();
1522        self.modifier_capabilities = self.modifier_chain.capabilities();
1523
1524        // Collect invalidations from modifier chain updates
1525        let mut invalidations = self.modifier_chain.take_invalidations();
1526        invalidations.extend(modifier_local_invalidations);
1527
1528        (invalidations, modifier_changed)
1529    }
1530
1531    fn set_debug_modifiers(&mut self, enabled: bool) {
1532        self.debug_modifiers = enabled;
1533        self.modifier_chain.set_debug_logging(enabled);
1534    }
1535}
1536
1537#[cfg(test)]
1538#[path = "tests/subcompose_layout_tests.rs"]
1539mod tests;