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