Skip to main content

cranpose_ui/
subcompose_layout.rs

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