Skip to main content

cranpose_ui/
subcompose_layout.rs

1use std::cell::{Cell, Ref, RefCell, RefMut};
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use cranpose_core::{
6    Composer, NodeError, NodeId, Phase, SlotId, SlotTable, SlotsHost, SubcomposeState,
7};
8
9use crate::modifier::{
10    collect_modifier_slices_into, Modifier, ModifierChainHandle, ModifierNodeSlices, Point,
11    ResolvedModifiers, Size,
12};
13use crate::widgets::nodes::{
14    allocate_virtual_node_id, is_virtual_node, register_layout_node, LayoutNode, LayoutState,
15};
16
17use cranpose_foundation::{InvalidationKind, ModifierInvalidation, NodeCapabilities};
18
19pub use cranpose_ui_layout::{Constraints, MeasureResult, Placement};
20
21/// Representation of a subcomposed child that can later be measured by the policy.
22///
23/// In lazy layouts, this represents an item that has been composed but may or
24/// may not have been measured yet. Call `measure()` to get the actual size.
25#[derive(Clone, Copy, Debug)]
26pub struct SubcomposeChild {
27    node_id: NodeId,
28    /// Measured size of the child (set after measurement).
29    /// Width in x, height in y.
30    measured_size: Option<Size>,
31}
32
33impl SubcomposeChild {
34    pub fn new(node_id: NodeId) -> Self {
35        Self {
36            node_id,
37            measured_size: None,
38        }
39    }
40
41    /// Creates a SubcomposeChild with a known size.
42    pub fn with_size(node_id: NodeId, size: Size) -> Self {
43        Self {
44            node_id,
45            measured_size: Some(size),
46        }
47    }
48
49    pub fn node_id(&self) -> NodeId {
50        self.node_id
51    }
52
53    /// Returns the measured size of this child.
54    ///
55    /// Returns a default size if the child hasn't been measured yet.
56    /// For lazy layouts using placeholder sizes, this returns the estimated size.
57    pub fn size(&self) -> Size {
58        self.measured_size.unwrap_or(Size {
59            width: 0.0,
60            height: 0.0,
61        })
62    }
63
64    /// Returns the measured width.
65    pub fn width(&self) -> f32 {
66        self.size().width
67    }
68
69    /// Returns the measured height.
70    pub fn height(&self) -> f32 {
71        self.size().height
72    }
73
74    /// Sets the measured size for this child.
75    pub fn set_size(&mut self, size: Size) {
76        self.measured_size = Some(size);
77    }
78}
79
80impl PartialEq for SubcomposeChild {
81    fn eq(&self, other: &Self) -> bool {
82        self.node_id == other.node_id
83    }
84}
85
86/// A measured child that is ready to be placed.
87///
88/// This is a type alias for the concrete `Placeable` struct from `cranpose_ui_layout`.
89/// In subcompose layouts, placement is handled by returning a list of `Placement`s,
90/// so the `place()` callback is not used.
91pub type SubcomposePlaceable = cranpose_ui_layout::Placeable;
92
93/// Base trait for measurement scopes.
94pub trait SubcomposeLayoutScope {
95    fn constraints(&self) -> Constraints;
96
97    fn layout<I>(&mut self, width: f32, height: f32, placements: I) -> MeasureResult
98    where
99        I: IntoIterator<Item = Placement>,
100    {
101        MeasureResult::new(Size { width, height }, placements.into_iter().collect())
102    }
103}
104
105/// Public trait exposed to measure policies for subcomposition.
106pub trait SubcomposeMeasureScope: SubcomposeLayoutScope {
107    fn subcompose<Content>(&mut self, slot_id: SlotId, content: Content) -> Vec<SubcomposeChild>
108    where
109        Content: FnMut() + 'static;
110
111    /// Measures a subcomposed child with the given constraints.
112    fn measure(&mut self, child: SubcomposeChild, constraints: Constraints) -> SubcomposePlaceable;
113
114    /// Checks if a node has no parent (is a root node).
115    /// Used to filter subcompose results to only include true root nodes.
116    fn node_has_no_parent(&self, node_id: NodeId) -> bool;
117}
118
119/// Concrete implementation of [`SubcomposeMeasureScope`].
120pub struct SubcomposeMeasureScopeImpl<'a> {
121    composer: Composer,
122    state: &'a mut SubcomposeState,
123    constraints: Constraints,
124    measurer: Box<dyn FnMut(NodeId, Constraints) -> Size + 'a>,
125    error: &'a RefCell<Option<NodeError>>,
126    parent_handle: SubcomposeLayoutNodeHandle,
127    root_id: NodeId,
128}
129
130impl<'a> SubcomposeMeasureScopeImpl<'a> {
131    pub fn new(
132        composer: Composer,
133        state: &'a mut SubcomposeState,
134        constraints: Constraints,
135        measurer: Box<dyn FnMut(NodeId, Constraints) -> Size + 'a>,
136        error: &'a RefCell<Option<NodeError>>,
137
138        parent_handle: SubcomposeLayoutNodeHandle,
139        root_id: NodeId,
140    ) -> Self {
141        Self {
142            composer,
143            state,
144            constraints,
145            measurer,
146            error,
147            parent_handle,
148            root_id,
149        }
150    }
151
152    fn record_error(&self, err: NodeError) {
153        let mut slot = self.error.borrow_mut();
154        if slot.is_none() {
155            eprintln!("[SubcomposeLayout] Error suppressed: {:?}", err);
156            *slot = Some(err);
157        }
158    }
159
160    fn perform_subcompose<Content>(&mut self, slot_id: SlotId, content: Content) -> Vec<NodeId>
161    where
162        Content: FnMut() + 'static,
163    {
164        let content_holder = self.state.callback_holder(slot_id);
165        content_holder.update(content);
166        let mut inner = self.parent_handle.inner.borrow_mut();
167
168        // Reuse or create virtual node
169        let (virtual_node_id, is_reused) =
170            if let Some(node_id) = self.state.take_node_from_reusables(slot_id) {
171                (node_id, true)
172            } else {
173                let id = allocate_virtual_node_id();
174                let node = LayoutNode::new_virtual();
175                // CRITICAL FIX: Register virtual node in Applier so that insert_child commands
176                // can find it. Previously, virtual nodes were only stored in inner.virtual_nodes
177                // which caused applier.get_mut(virtual_node_id) to fail, breaking child attachment.
178                if let Err(e) = self
179                    .composer
180                    .register_virtual_node(id, Box::new(node.clone()))
181                {
182                    eprintln!(
183                        "[Subcompose] Failed to register virtual node {}: {:?}",
184                        id, e
185                    );
186                }
187                register_layout_node(id, &node);
188
189                inner.virtual_nodes.insert(id, Rc::new(node));
190                inner.children.push(id);
191                (id, false)
192            };
193
194        // Keep the slot root in the outer parent frame so SyncChildren preserves
195        // the virtual subtree instead of deleting it after subcomposition.
196        self.composer.record_subcompose_child(virtual_node_id);
197
198        // Keep both the retained virtual-node clone and the applier copy wired to the
199        // live subcompose root so bubbling can cross the virtual slot boundary.
200        if let Some(v_node) = inner.virtual_nodes.get(&virtual_node_id) {
201            v_node.set_parent(self.root_id);
202        }
203
204        drop(inner);
205
206        let _ = self
207            .composer
208            .with_node_mut::<LayoutNode, _>(virtual_node_id, |node| {
209                node.set_parent(self.root_id);
210            });
211
212        // CRITICAL FIX: Clear children of reused virtual nodes BEFORE subcomposing new content.
213        // Without this, old children remain attached when the node is reused for different items,
214        // causing items from different scroll positions to interleave (e.g., [1,16,31,2,17,32...]).
215        if is_reused {
216            self.composer.clear_node_children(virtual_node_id);
217        }
218
219        let slot_host = self.state.get_or_create_slots(slot_id);
220        let holder_for_slot = content_holder.clone();
221        let scopes = self
222            .composer
223            .subcompose_slot(&slot_host, Some(virtual_node_id), move |_| {
224                compose_subcompose_slot_content(holder_for_slot.clone());
225            })
226            .map(|(_, scopes)| scopes)
227            .unwrap_or_default();
228
229        self.state
230            .register_active(slot_id, &[virtual_node_id], &scopes);
231
232        // CRITICAL FIX: Read children from the Applier's copy of the virtual node,
233        // NOT from inner.virtual_nodes. The Applier's copy received insert_child calls
234        // during subcomposition, while inner.virtual_nodes is an out-of-sync clone.
235        self.composer.get_node_children(virtual_node_id).to_vec()
236    }
237}
238
239impl<'a> SubcomposeLayoutScope for SubcomposeMeasureScopeImpl<'a> {
240    fn constraints(&self) -> Constraints {
241        self.constraints
242    }
243}
244
245impl<'a> SubcomposeMeasureScope for SubcomposeMeasureScopeImpl<'a> {
246    fn subcompose<Content>(&mut self, slot_id: SlotId, content: Content) -> Vec<SubcomposeChild>
247    where
248        Content: FnMut() + 'static,
249    {
250        let nodes = self.perform_subcompose(slot_id, content);
251        nodes.into_iter().map(SubcomposeChild::new).collect()
252    }
253
254    fn measure(&mut self, child: SubcomposeChild, constraints: Constraints) -> SubcomposePlaceable {
255        if self.error.borrow().is_some() {
256            // Already in error state - return zero-size placeable
257            return SubcomposePlaceable::value(0.0, 0.0, child.node_id);
258        }
259
260        if let Err(err) = self.composer.apply_pending_commands() {
261            self.record_error(err);
262            return SubcomposePlaceable::value(0.0, 0.0, child.node_id);
263        }
264
265        let size = (self.measurer)(child.node_id, constraints);
266        SubcomposePlaceable::value(size.width, size.height, child.node_id)
267    }
268
269    fn node_has_no_parent(&self, node_id: NodeId) -> bool {
270        self.composer.node_has_no_parent(node_id)
271    }
272}
273
274impl<'a> SubcomposeMeasureScopeImpl<'a> {
275    /// Subcomposes content and assigns estimated sizes to children.
276    ///
277    /// This is used by lazy layouts where true measurement happens later.
278    /// The `estimate_size` function provides size estimates based on index.
279    pub fn subcompose_with_size<Content, F>(
280        &mut self,
281        slot_id: SlotId,
282        content: Content,
283        estimate_size: F,
284    ) -> Vec<SubcomposeChild>
285    where
286        Content: FnMut() + 'static,
287        F: Fn(usize) -> Size,
288    {
289        let nodes = self.perform_subcompose(slot_id, content);
290        nodes
291            .into_iter()
292            .enumerate()
293            .map(|(i, node_id)| SubcomposeChild::with_size(node_id, estimate_size(i)))
294            .collect()
295    }
296
297    /// Returns the number of active slots in the subcompose state.
298    ///
299    /// Used by lazy layouts to report statistics about slot usage.
300    pub fn active_slots_count(&self) -> usize {
301        self.state.active_slots_count()
302    }
303
304    /// Returns the number of reusable slots in the pool.
305    ///
306    /// Used by lazy layouts to report statistics about cached slots.
307    pub fn reusable_slots_count(&self) -> usize {
308        self.state.reusable_slots_count()
309    }
310
311    /// Registers the content type for a slot.
312    ///
313    /// Call this before `subcompose()` to enable content-type-aware slot reuse.
314    /// If the policy supports content types (like `ContentTypeReusePolicy`),
315    /// slots with matching content types can reuse each other's nodes.
316    pub fn register_content_type(&mut self, slot_id: SlotId, content_type: u64) {
317        self.state.register_content_type(slot_id, content_type);
318    }
319
320    /// Updates the content type for a slot, handling Some→None transitions.
321    ///
322    /// If `content_type` is `Some(type)`, registers the type for the slot.
323    /// If `content_type` is `None`, removes any previously registered type.
324    /// This ensures stale types don't drive incorrect reuse after transitions.
325    pub fn update_content_type(&mut self, slot_id: SlotId, content_type: Option<u64>) {
326        self.state.update_content_type(slot_id, content_type);
327    }
328
329    /// Returns whether the last subcomposed slot was reused.
330    ///
331    /// Returns `Some(true)` if the slot already existed (was reused from pool or
332    /// was recomposed), `Some(false)` if it was newly created, or `None` if no
333    /// slot has been subcomposed yet this pass.
334    ///
335    /// This is useful for tracking composition statistics in lazy layouts.
336    pub fn was_last_slot_reused(&self) -> Option<bool> {
337        self.state.was_last_slot_reused()
338    }
339}
340
341fn compose_subcompose_slot_content(holder: cranpose_core::CallbackHolder) {
342    cranpose_core::with_current_composer(|composer| {
343        let holder_for_recompose = holder.clone();
344        composer.set_recranpose_callback(move |_composer| {
345            compose_subcompose_slot_content(holder_for_recompose.clone());
346        });
347    });
348
349    let invoke = holder.clone_rc();
350    invoke();
351}
352
353/// Trait object representing a reusable measure policy.
354pub type MeasurePolicy =
355    dyn for<'scope> Fn(&mut SubcomposeMeasureScopeImpl<'scope>, Constraints) -> MeasureResult;
356
357/// Node responsible for orchestrating measure-time subcomposition.
358pub struct SubcomposeLayoutNode {
359    inner: Rc<RefCell<SubcomposeLayoutNodeInner>>,
360    /// Parent tracking for dirty flag bubbling (P0.2 fix)
361    parent: Cell<Option<NodeId>>,
362    /// Node's own ID
363    id: Cell<Option<NodeId>>,
364    // Dirty flags for selective measure/layout/render
365    needs_measure: Cell<bool>,
366    needs_layout: Cell<bool>,
367    needs_semantics: Cell<bool>,
368    needs_redraw: Cell<bool>,
369    needs_pointer_pass: Cell<bool>,
370    needs_focus_sync: Cell<bool>,
371    virtual_children_count: Cell<usize>,
372    /// Retained layout state (size, position) for rendering.
373    layout_state: RefCell<LayoutState>,
374    modifier_slices_snapshot: RefCell<Rc<ModifierNodeSlices>>,
375    modifier_slices_dirty: Cell<bool>,
376}
377
378impl SubcomposeLayoutNode {
379    pub fn new(modifier: Modifier, measure_policy: Rc<MeasurePolicy>) -> Self {
380        let inner = Rc::new(RefCell::new(SubcomposeLayoutNodeInner::new(measure_policy)));
381        let node = Self {
382            inner,
383            parent: Cell::new(None),
384            id: Cell::new(None),
385            needs_measure: Cell::new(true),
386            needs_layout: Cell::new(true),
387            needs_semantics: Cell::new(true),
388            needs_redraw: Cell::new(true),
389            needs_pointer_pass: Cell::new(false),
390            needs_focus_sync: Cell::new(false),
391            virtual_children_count: Cell::new(0),
392            layout_state: RefCell::new(LayoutState::default()),
393            modifier_slices_snapshot: RefCell::new(Rc::default()),
394            modifier_slices_dirty: Cell::new(true),
395        };
396        // Set modifier and dispatch invalidations after borrow is released
397        // Pass empty prev_caps since this is initial construction
398        let (invalidations, _) = node.inner.borrow_mut().set_modifier_collect(modifier);
399        node.dispatch_modifier_invalidations(&invalidations, NodeCapabilities::empty());
400        node.update_modifier_slices_cache();
401        node
402    }
403
404    /// Creates a SubcomposeLayoutNode with ContentTypeReusePolicy.
405    ///
406    /// Use this for lazy lists to enable content-type-aware slot reuse.
407    /// Slots with matching content types can reuse each other's nodes,
408    /// improving efficiency when scrolling through items with different types.
409    pub fn with_content_type_policy(modifier: Modifier, measure_policy: Rc<MeasurePolicy>) -> Self {
410        let mut inner_data = SubcomposeLayoutNodeInner::new(measure_policy);
411        inner_data
412            .state
413            .set_policy(Box::new(cranpose_core::ContentTypeReusePolicy::new()));
414        let inner = Rc::new(RefCell::new(inner_data));
415        let node = Self {
416            inner,
417            parent: Cell::new(None),
418            id: Cell::new(None),
419            needs_measure: Cell::new(true),
420            needs_layout: Cell::new(true),
421            needs_semantics: Cell::new(true),
422            needs_redraw: Cell::new(true),
423            needs_pointer_pass: Cell::new(false),
424            needs_focus_sync: Cell::new(false),
425            virtual_children_count: Cell::new(0),
426            layout_state: RefCell::new(LayoutState::default()),
427            modifier_slices_snapshot: RefCell::new(Rc::default()),
428            modifier_slices_dirty: Cell::new(true),
429        };
430        // Set modifier and dispatch invalidations after borrow is released
431        // Pass empty prev_caps since this is initial construction
432        let (invalidations, _) = node.inner.borrow_mut().set_modifier_collect(modifier);
433        node.dispatch_modifier_invalidations(&invalidations, NodeCapabilities::empty());
434        node.update_modifier_slices_cache();
435        node
436    }
437
438    pub fn handle(&self) -> SubcomposeLayoutNodeHandle {
439        SubcomposeLayoutNodeHandle {
440            inner: Rc::clone(&self.inner),
441        }
442    }
443
444    #[doc(hidden)]
445    pub fn debug_scope_ids_by_slot(&self) -> Vec<(u64, Vec<usize>)> {
446        self.inner.borrow().state.debug_scope_ids_by_slot()
447    }
448
449    #[doc(hidden)]
450    pub fn debug_slot_table_for_slot(
451        &self,
452        slot_id: cranpose_core::SlotId,
453    ) -> Option<Vec<cranpose_core::SlotDebugEntry>> {
454        self.inner.borrow().state.debug_slot_table_for_slot(slot_id)
455    }
456
457    #[doc(hidden)]
458    pub fn debug_slot_table_groups_for_slot(
459        &self,
460        slot_id: cranpose_core::SlotId,
461    ) -> Option<Vec<cranpose_core::subcompose::DebugSlotGroup>> {
462        self.inner
463            .borrow()
464            .state
465            .debug_slot_table_groups_for_slot(slot_id)
466    }
467
468    pub fn set_measure_policy(&mut self, policy: Rc<MeasurePolicy>) {
469        let mut inner = self.inner.borrow_mut();
470        if Rc::ptr_eq(&inner.measure_policy, &policy) {
471            return;
472        }
473        inner.set_measure_policy(policy);
474        drop(inner);
475        self.invalidate_subcomposition();
476    }
477
478    pub fn set_modifier(&mut self, modifier: Modifier) {
479        // Capture capabilities BEFORE updating to detect removed modifiers
480        let prev_caps = self.modifier_capabilities();
481        // Collect invalidations while inner is borrowed, then dispatch after release
482        let (invalidations, modifier_changed) = {
483            let mut inner = self.inner.borrow_mut();
484            inner.set_modifier_collect(modifier)
485        };
486        // Now dispatch invalidations after the borrow is released
487        // Pass both prev and curr caps so removed modifiers still trigger invalidation
488        self.dispatch_modifier_invalidations(&invalidations, prev_caps);
489        self.update_modifier_slices_cache();
490        if modifier_changed {
491            self.request_semantics_update();
492        }
493    }
494
495    /// Updates the cached modifier slices from the modifier chain.
496    fn update_modifier_slices_cache(&self) {
497        let inner = self.inner.borrow();
498        let mut snapshot = self.modifier_slices_snapshot.borrow_mut();
499        collect_modifier_slices_into(inner.modifier_chain.chain(), Rc::make_mut(&mut snapshot));
500        self.modifier_slices_dirty.set(false);
501    }
502
503    pub fn set_debug_modifiers(&mut self, enabled: bool) {
504        self.inner.borrow_mut().set_debug_modifiers(enabled);
505    }
506
507    pub fn modifier(&self) -> Modifier {
508        self.handle().modifier()
509    }
510
511    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
512        self.inner.borrow().resolved_modifiers
513    }
514
515    /// Returns a clone of the current layout state.
516    pub fn layout_state(&self) -> LayoutState {
517        self.layout_state.borrow().clone()
518    }
519
520    /// Updates the position of this node. Called during placement.
521    pub fn set_position(&self, position: Point) {
522        let mut state = self.layout_state.borrow_mut();
523        state.position = position;
524        state.is_placed = true;
525    }
526
527    /// Updates the measured size of this node. Called during measurement.
528    pub fn set_measured_size(&self, size: Size) {
529        let mut state = self.layout_state.borrow_mut();
530        state.size = size;
531    }
532
533    /// Clears the is_placed flag. Called at the start of a layout pass.
534    pub fn clear_placed(&self) {
535        self.layout_state.borrow_mut().is_placed = false;
536    }
537
538    /// Returns the modifier slices snapshot for rendering.
539    pub fn modifier_slices_snapshot(&self) -> Rc<ModifierNodeSlices> {
540        if self.modifier_slices_dirty.get() {
541            self.update_modifier_slices_cache();
542        }
543        self.modifier_slices_snapshot.borrow().clone()
544    }
545
546    pub fn state(&self) -> Ref<'_, SubcomposeState> {
547        Ref::map(self.inner.borrow(), |inner| &inner.state)
548    }
549
550    pub fn state_mut(&self) -> RefMut<'_, SubcomposeState> {
551        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.state)
552    }
553
554    pub fn invalidate_subcomposition(&self) {
555        self.inner.borrow().state.invalidate_scopes();
556        self.mark_needs_measure();
557        if let Some(id) = self.id.get() {
558            cranpose_core::bubble_measure_dirty_in_composer(id);
559        }
560    }
561
562    pub fn request_measure_recompose(&self) {
563        self.mark_needs_measure();
564        if let Some(id) = self.id.get() {
565            cranpose_core::bubble_measure_dirty_in_composer(id);
566        }
567    }
568
569    pub fn active_children(&self) -> Vec<NodeId> {
570        current_subcompose_children(&self.inner.borrow())
571    }
572
573    /// Mark this node as needing measure. Also marks it as needing layout.
574    pub fn mark_needs_measure(&self) {
575        self.needs_measure.set(true);
576        self.needs_layout.set(true);
577    }
578
579    /// Mark this node as needing layout (but not necessarily measure).
580    pub fn mark_needs_layout_flag(&self) {
581        self.needs_layout.set(true);
582    }
583
584    /// Mark this node as needing redraw without forcing measure/layout.
585    pub fn mark_needs_redraw(&self) {
586        self.needs_redraw.set(true);
587        if let Some(id) = self.id.get() {
588            crate::schedule_draw_repass(id);
589        }
590        crate::request_render_invalidation();
591    }
592
593    /// Check if this node needs measure.
594    pub fn needs_measure(&self) -> bool {
595        self.needs_measure.get()
596    }
597
598    pub(crate) fn clear_needs_measure(&self) {
599        self.needs_measure.set(false);
600    }
601
602    pub(crate) fn clear_needs_layout(&self) {
603        self.needs_layout.set(false);
604    }
605
606    /// Mark this node as needing semantics recomputation.
607    pub fn mark_needs_semantics(&self) {
608        self.needs_semantics.set(true);
609    }
610
611    /// Returns true when semantics need to be recomputed.
612    pub fn needs_semantics_flag(&self) -> bool {
613        self.needs_semantics.get()
614    }
615
616    pub(crate) fn clear_needs_semantics(&self) {
617        self.needs_semantics.set(false);
618    }
619
620    #[cfg(test)]
621    pub(crate) fn clear_needs_semantics_for_tests(&self) {
622        self.clear_needs_semantics();
623    }
624
625    /// Returns true when this node requested a redraw since the last render pass.
626    pub fn needs_redraw(&self) -> bool {
627        self.needs_redraw.get()
628    }
629
630    pub fn clear_needs_redraw(&self) {
631        self.needs_redraw.set(false);
632    }
633
634    /// Marks this node as needing a fresh pointer-input pass.
635    pub fn mark_needs_pointer_pass(&self) {
636        self.needs_pointer_pass.set(true);
637    }
638
639    /// Returns true when pointer-input state needs to be recomputed.
640    pub fn needs_pointer_pass(&self) -> bool {
641        self.needs_pointer_pass.get()
642    }
643
644    /// Clears the pointer-input dirty flag after hosts service it.
645    pub fn clear_needs_pointer_pass(&self) {
646        self.needs_pointer_pass.set(false);
647    }
648
649    /// Marks this node as needing a focus synchronization.
650    pub fn mark_needs_focus_sync(&self) {
651        self.needs_focus_sync.set(true);
652    }
653
654    /// Returns true when focus state needs to be synchronized.
655    pub fn needs_focus_sync(&self) -> bool {
656        self.needs_focus_sync.get()
657    }
658
659    /// Clears the focus dirty flag after the focus manager processes it.
660    pub fn clear_needs_focus_sync(&self) {
661        self.needs_focus_sync.set(false);
662    }
663
664    fn request_semantics_update(&self) {
665        let already_dirty = self.needs_semantics.replace(true);
666        if already_dirty {
667            return;
668        }
669
670        if let Some(id) = self.id.get() {
671            cranpose_core::queue_semantics_invalidation(id);
672        }
673    }
674
675    /// Returns the modifier capabilities for this node.
676    pub fn modifier_capabilities(&self) -> NodeCapabilities {
677        self.inner.borrow().modifier_capabilities
678    }
679
680    pub fn has_layout_modifier_nodes(&self) -> bool {
681        self.modifier_capabilities()
682            .contains(NodeCapabilities::LAYOUT)
683    }
684
685    pub fn has_draw_modifier_nodes(&self) -> bool {
686        self.modifier_capabilities()
687            .contains(NodeCapabilities::DRAW)
688    }
689
690    pub fn has_pointer_input_modifier_nodes(&self) -> bool {
691        self.modifier_capabilities()
692            .contains(NodeCapabilities::POINTER_INPUT)
693    }
694
695    pub fn has_semantics_modifier_nodes(&self) -> bool {
696        self.modifier_capabilities()
697            .contains(NodeCapabilities::SEMANTICS)
698    }
699
700    pub fn has_focus_modifier_nodes(&self) -> bool {
701        self.modifier_capabilities()
702            .contains(NodeCapabilities::FOCUS)
703    }
704
705    /// Dispatches modifier invalidations to the appropriate subsystems.
706    ///
707    /// `prev_caps` contains the capabilities BEFORE the modifier update.
708    /// Invalidations are dispatched if EITHER the previous OR current capabilities
709    /// include the relevant type. This ensures that removing the last modifier
710    /// of a type still triggers proper invalidation.
711    fn dispatch_modifier_invalidations(
712        &self,
713        invalidations: &[ModifierInvalidation],
714        prev_caps: NodeCapabilities,
715    ) {
716        let curr_caps = self.modifier_capabilities();
717        for invalidation in invalidations {
718            self.modifier_slices_dirty.set(true);
719            match invalidation.kind() {
720                InvalidationKind::Layout => {
721                    if curr_caps.contains(NodeCapabilities::LAYOUT)
722                        || prev_caps.contains(NodeCapabilities::LAYOUT)
723                    {
724                        self.mark_needs_measure();
725                    }
726                }
727                InvalidationKind::Draw => {
728                    if curr_caps.contains(NodeCapabilities::DRAW)
729                        || prev_caps.contains(NodeCapabilities::DRAW)
730                    {
731                        self.mark_needs_redraw();
732                    }
733                }
734                InvalidationKind::PointerInput => {
735                    if curr_caps.contains(NodeCapabilities::POINTER_INPUT)
736                        || prev_caps.contains(NodeCapabilities::POINTER_INPUT)
737                    {
738                        self.mark_needs_pointer_pass();
739                        crate::request_pointer_invalidation();
740                        // Schedule pointer repass for this node
741                        if let Some(id) = self.id.get() {
742                            crate::schedule_pointer_repass(id);
743                        }
744                    }
745                }
746                InvalidationKind::Semantics => {
747                    self.request_semantics_update();
748                }
749                InvalidationKind::Focus => {
750                    if curr_caps.contains(NodeCapabilities::FOCUS)
751                        || prev_caps.contains(NodeCapabilities::FOCUS)
752                    {
753                        self.mark_needs_focus_sync();
754                        crate::request_focus_invalidation();
755                        // Schedule focus invalidation for this node
756                        if let Some(id) = self.id.get() {
757                            crate::schedule_focus_invalidation(id);
758                        }
759                    }
760                }
761            }
762        }
763    }
764}
765
766impl cranpose_core::Node for SubcomposeLayoutNode {
767    fn mount(&mut self) {
768        let mut inner = self.inner.borrow_mut();
769        let (chain, mut context) = inner.modifier_chain.chain_and_context_mut();
770        chain.repair_chain();
771        chain.attach_nodes(&mut *context);
772    }
773
774    fn unmount(&mut self) {
775        self.inner
776            .borrow_mut()
777            .modifier_chain
778            .chain_mut()
779            .detach_nodes();
780    }
781
782    fn insert_child(&mut self, child: NodeId) {
783        let mut inner = self.inner.borrow_mut();
784        if inner.children.contains(&child) {
785            return;
786        }
787        if is_virtual_node(child) {
788            let count = self.virtual_children_count.get();
789            self.virtual_children_count.set(count + 1);
790        }
791        inner.children.push(child);
792    }
793
794    fn remove_child(&mut self, child: NodeId) {
795        let mut inner = self.inner.borrow_mut();
796        let before = inner.children.len();
797        inner.children.retain(|&id| id != child);
798        if inner.children.len() < before && is_virtual_node(child) {
799            let count = self.virtual_children_count.get();
800            if count > 0 {
801                self.virtual_children_count.set(count - 1);
802            }
803        }
804    }
805
806    fn move_child(&mut self, from: usize, to: usize) {
807        let mut inner = self.inner.borrow_mut();
808        if from == to || from >= inner.children.len() {
809            return;
810        }
811        let child = inner.children.remove(from);
812        let target = to.min(inner.children.len());
813        inner.children.insert(target, child);
814    }
815
816    fn update_children(&mut self, children: &[NodeId]) {
817        let mut inner = self.inner.borrow_mut();
818        inner.children.clear();
819        inner.children.extend_from_slice(children);
820    }
821
822    fn children(&self) -> Vec<NodeId> {
823        current_subcompose_children(&self.inner.borrow())
824    }
825
826    fn set_node_id(&mut self, id: NodeId) {
827        self.id.set(Some(id));
828        self.inner.borrow_mut().modifier_chain.set_node_id(Some(id));
829    }
830
831    fn on_attached_to_parent(&mut self, parent: NodeId) {
832        self.parent.set(Some(parent));
833    }
834
835    fn on_removed_from_parent(&mut self) {
836        self.parent.set(None);
837    }
838
839    fn parent(&self) -> Option<NodeId> {
840        self.parent.get()
841    }
842
843    fn mark_needs_layout(&self) {
844        self.needs_layout.set(true);
845    }
846
847    fn needs_layout(&self) -> bool {
848        self.needs_layout.get()
849    }
850
851    fn mark_needs_measure(&self) {
852        self.needs_measure.set(true);
853        self.needs_layout.set(true); // Measure implies layout
854    }
855
856    fn needs_measure(&self) -> bool {
857        self.needs_measure.get()
858    }
859
860    fn mark_needs_semantics(&self) {
861        self.needs_semantics.set(true);
862    }
863
864    fn needs_semantics(&self) -> bool {
865        self.needs_semantics.get()
866    }
867
868    /// Minimal parent setter for dirty flag bubbling.
869    fn set_parent_for_bubbling(&mut self, parent: NodeId) {
870        self.parent.set(Some(parent));
871    }
872}
873
874#[derive(Clone)]
875pub struct SubcomposeLayoutNodeHandle {
876    inner: Rc<RefCell<SubcomposeLayoutNodeInner>>,
877}
878
879impl SubcomposeLayoutNodeHandle {
880    pub fn modifier(&self) -> Modifier {
881        self.inner.borrow().modifier.clone()
882    }
883
884    pub fn layout_properties(&self) -> crate::modifier::LayoutProperties {
885        self.resolved_modifiers().layout_properties()
886    }
887
888    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
889        self.inner.borrow().resolved_modifiers
890    }
891
892    pub fn total_offset(&self) -> Point {
893        self.resolved_modifiers().offset()
894    }
895
896    pub fn modifier_capabilities(&self) -> NodeCapabilities {
897        self.inner.borrow().modifier_capabilities
898    }
899
900    pub fn has_layout_modifier_nodes(&self) -> bool {
901        self.modifier_capabilities()
902            .contains(NodeCapabilities::LAYOUT)
903    }
904
905    pub fn has_draw_modifier_nodes(&self) -> bool {
906        self.modifier_capabilities()
907            .contains(NodeCapabilities::DRAW)
908    }
909
910    pub fn has_pointer_input_modifier_nodes(&self) -> bool {
911        self.modifier_capabilities()
912            .contains(NodeCapabilities::POINTER_INPUT)
913    }
914
915    pub fn has_semantics_modifier_nodes(&self) -> bool {
916        self.modifier_capabilities()
917            .contains(NodeCapabilities::SEMANTICS)
918    }
919
920    pub fn has_focus_modifier_nodes(&self) -> bool {
921        self.modifier_capabilities()
922            .contains(NodeCapabilities::FOCUS)
923    }
924
925    pub fn set_debug_modifiers(&self, enabled: bool) {
926        self.inner.borrow_mut().set_debug_modifiers(enabled);
927    }
928
929    pub fn measure<'a>(
930        &self,
931        composer: &Composer,
932        node_id: NodeId,
933        constraints: Constraints,
934        measurer: Box<dyn FnMut(NodeId, Constraints) -> Size + 'a>,
935        error: &'a RefCell<Option<NodeError>>,
936    ) -> Result<MeasureResult, NodeError> {
937        let (policy, mut state, slots_host) = {
938            let mut inner = self.inner.borrow_mut();
939            let policy = Rc::clone(&inner.measure_policy);
940            let state = std::mem::take(&mut inner.state);
941            let slots_host = Rc::clone(&inner.slots);
942            (policy, state, slots_host)
943        };
944        state.begin_pass();
945
946        let previous = composer.phase();
947        if !matches!(previous, Phase::Measure | Phase::Layout) {
948            composer.enter_phase(Phase::Measure);
949        }
950
951        let constraints_copy = constraints;
952        // Architecture Note: Using subcompose_slot (not subcompose_in) to preserve the
953        // SlotTable across measurement passes. This matches JC's SubcomposeLayout behavior
954        // where `subcompose()` is called during measure and the slot table persists between
955        // frames. Without this, lazy list item groups would be wiped and recreated on every
956        // scroll frame, causing O(visible_items) recomposition overhead ("thrashing").
957        //
958        // Reference: LazyLayoutMeasureScope.subcompose() in JC reuses existing slots by key,
959        // and SubcomposeLayoutState holds `slotIdToNode` map across measurements.
960        let (result, _) =
961            composer.subcompose_slot(&slots_host, Some(node_id), |inner_composer| {
962                let mut scope = SubcomposeMeasureScopeImpl::new(
963                    inner_composer.clone(),
964                    &mut state,
965                    constraints_copy,
966                    measurer,
967                    error,
968                    self.clone(), // Pass handle
969                    node_id,      // Pass root_id
970                );
971                (policy)(&mut scope, constraints_copy)
972            })?;
973
974        state.finish_pass();
975
976        if previous != composer.phase() {
977            composer.enter_phase(previous);
978        }
979
980        {
981            let mut inner = self.inner.borrow_mut();
982            inner.state = state;
983
984            // Store placement children for children() traversal.
985            // This avoids clearing/rebuilding the structural children set on every measure,
986            // eliminating O(n) allocator churn. The structural children (virtual nodes) are
987            // tracked via insert_child/remove_child, while last_placements tracks rendered nodes.
988            inner.last_placements = result.placements.iter().map(|p| p.node_id).collect();
989        }
990
991        Ok(result)
992    }
993
994    pub fn set_active_children<I>(&self, children: I)
995    where
996        I: IntoIterator<Item = NodeId>,
997    {
998        let mut inner = self.inner.borrow_mut();
999        inner.children.clear();
1000        inner.children.extend(children);
1001    }
1002}
1003
1004fn current_subcompose_children(inner: &SubcomposeLayoutNodeInner) -> Vec<NodeId> {
1005    if !inner.last_placements.is_empty() {
1006        inner.last_placements.clone()
1007    } else {
1008        inner.children.clone()
1009    }
1010}
1011
1012struct SubcomposeLayoutNodeInner {
1013    modifier: Modifier,
1014    modifier_chain: ModifierChainHandle,
1015    resolved_modifiers: ResolvedModifiers,
1016    modifier_capabilities: NodeCapabilities,
1017    state: SubcomposeState,
1018    measure_policy: Rc<MeasurePolicy>,
1019    children: Vec<NodeId>,
1020    slots: Rc<SlotsHost>,
1021    debug_modifiers: bool,
1022    // Owns virtual nodes created during subcomposition
1023    virtual_nodes: HashMap<NodeId, Rc<LayoutNode>>,
1024    // Cached placement children from the last measure pass.
1025    // Used by children() for semantic/render traversal without clearing structural children.
1026    last_placements: Vec<NodeId>,
1027}
1028
1029impl SubcomposeLayoutNodeInner {
1030    fn new(measure_policy: Rc<MeasurePolicy>) -> Self {
1031        Self {
1032            modifier: Modifier::empty(),
1033            modifier_chain: ModifierChainHandle::new(),
1034            resolved_modifiers: ResolvedModifiers::default(),
1035            modifier_capabilities: NodeCapabilities::default(),
1036            state: SubcomposeState::default(),
1037            measure_policy,
1038            children: Vec::new(),
1039            slots: Rc::new(SlotsHost::new(SlotTable::default())),
1040            debug_modifiers: false,
1041            virtual_nodes: HashMap::new(),
1042            last_placements: Vec::new(),
1043        }
1044    }
1045
1046    fn set_measure_policy(&mut self, policy: Rc<MeasurePolicy>) {
1047        self.measure_policy = policy;
1048        // The root measurement subcomposition caches its slot table separately
1049        // from per-item slot scopes. When a widget updates the data captured by
1050        // the measure lambda through shared cells, the next layout pass must not
1051        // reuse the previous root measure group wholesale.
1052        if let Err(err) = self.slots.reset() {
1053            log::error!(
1054                "failed to reset root measurement slots after measure policy update: {err}"
1055            );
1056        }
1057    }
1058
1059    /// Updates the modifier and collects invalidations without dispatching them.
1060    /// Returns the invalidations and whether the modifier changed.
1061    fn set_modifier_collect(&mut self, modifier: Modifier) -> (Vec<ModifierInvalidation>, bool) {
1062        let modifier_changed = !self.modifier.structural_eq(&modifier);
1063        self.modifier = modifier;
1064        self.modifier_chain.set_debug_logging(self.debug_modifiers);
1065        let modifier_local_invalidations = self.modifier_chain.update(&self.modifier);
1066        self.resolved_modifiers = self.modifier_chain.resolved_modifiers();
1067        self.modifier_capabilities = self.modifier_chain.capabilities();
1068
1069        // Collect invalidations from modifier chain updates
1070        let mut invalidations = self.modifier_chain.take_invalidations();
1071        invalidations.extend(modifier_local_invalidations);
1072
1073        (invalidations, modifier_changed)
1074    }
1075
1076    fn set_debug_modifiers(&mut self, enabled: bool) {
1077        self.debug_modifiers = enabled;
1078        self.modifier_chain.set_debug_logging(enabled);
1079    }
1080}
1081
1082#[cfg(test)]
1083#[path = "tests/subcompose_layout_tests.rs"]
1084mod tests;