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