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