Skip to main content

cranpose_ui/
subcompose_layout.rs

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