Skip to main content

cranpose_ui/layout/
coordinator.rs

1//! Node coordinator system mirroring Jetpack Compose's NodeCoordinator pattern.
2//!
3//! Coordinators wrap modifier nodes and form a chain that drives measurement, placement,
4//! drawing, and hit testing. Each LayoutModifierNode gets its own coordinator instance
5//! that persists across recomposition, enabling proper state and invalidation tracking.
6//!
7//! **Content Offset Tracking**: Each coordinator contributes its placement offset to a
8//! shared accumulator during measurement. The final `content_offset()` is read from this
9//! accumulator via the outermost CoordinatorPlaceable.
10
11use cranpose_core::NodeId;
12use cranpose_foundation::ModifierNodeContext;
13use cranpose_ui_layout::{Constraints, Measurable, Placeable};
14use std::cell::{Cell, RefCell};
15use std::rc::Rc;
16
17use crate::layout::{LayoutNodeContext, MeasurePolicy, MeasureResult};
18use crate::modifier::{Point, Size};
19
20/// Core coordinator trait that all coordinators implement.
21///
22/// Coordinators are chained together, with each one wrapping the next.
23/// Coordinators wrap either other coordinators or the inner coordinator.
24pub trait NodeCoordinator: Measurable {
25    /// Returns the accumulated placement offset from this coordinator
26    /// down through the wrapped chain (inner-most coordinator).
27    fn total_content_offset(&self) -> Point;
28}
29
30/// Coordinator that wraps a single LayoutModifierNode from the reconciled chain.
31///
32/// This is analogous to Jetpack Compose's LayoutModifierNodeCoordinator.
33/// It delegates measurement to the wrapped node, passing the inner coordinator as the measurable.
34pub struct LayoutModifierCoordinator<'a> {
35    /// Direct reference to the layout modifier node.
36    /// This Rc<RefCell<>> allows the coordinator to hold a shared reference
37    /// and call the node's measure() method directly without proxies.
38    node: Rc<RefCell<Box<dyn cranpose_foundation::ModifierNode>>>,
39    /// The inner (wrapped) coordinator.
40    wrapped: Box<dyn NodeCoordinator + 'a>,
41    /// The measured size from the last measure pass.
42    measured_size: Cell<Size>,
43    /// The ACCUMULATED placement offset from this coordinator through the entire chain.
44    /// This is local_offset + wrapped.total_content_offset(), stored for O(1) access.
45    accumulated_offset: Cell<Point>,
46    /// Shared context for invalidation tracking.
47    context: Rc<RefCell<LayoutNodeContext>>,
48}
49
50impl<'a> LayoutModifierCoordinator<'a> {
51    /// Creates a new coordinator wrapping the specified node.
52    #[allow(private_interfaces)]
53    pub fn new(
54        node: Rc<RefCell<Box<dyn cranpose_foundation::ModifierNode>>>,
55        wrapped: Box<dyn NodeCoordinator + 'a>,
56        context: Rc<RefCell<LayoutNodeContext>>,
57    ) -> Self {
58        Self {
59            node,
60            wrapped,
61            measured_size: Cell::new(Size::default()),
62            accumulated_offset: Cell::new(Point::default()),
63            context,
64        }
65    }
66}
67
68impl<'a> NodeCoordinator for LayoutModifierCoordinator<'a> {
69    fn total_content_offset(&self) -> Point {
70        // O(1): just return the pre-computed accumulated offset
71        self.accumulated_offset.get()
72    }
73}
74
75impl<'a> Measurable for LayoutModifierCoordinator<'a> {
76    /// Measure through this coordinator
77    fn measure(&self, constraints: Constraints) -> Placeable {
78        let node_borrow = self.node.borrow();
79
80        let result = {
81            if let Some(layout_node) = node_borrow.as_layout_node() {
82                match self.context.try_borrow_mut() {
83                    Ok(mut context) => {
84                        layout_node.measure(&mut *context, self.wrapped.as_ref(), constraints)
85                    }
86                    Err(_) => {
87                        // Context already borrowed - use a temporary context
88                        let mut temp = LayoutNodeContext::new();
89                        let result =
90                            layout_node.measure(&mut temp, self.wrapped.as_ref(), constraints);
91
92                        // Merge invalidations from temp context to shared
93                        if let Ok(mut shared) = self.context.try_borrow_mut() {
94                            for kind in temp.take_invalidations() {
95                                shared.invalidate(kind);
96                            }
97                        }
98
99                        result
100                    }
101                }
102            } else {
103                // Node is not a layout modifier - pass through to wrapped coordinator
104                let placeable = self.wrapped.measure(constraints);
105                // Pass through the child's accumulated offset (stored from its measure())
106                let child_accumulated = self.wrapped.total_content_offset();
107                self.accumulated_offset.set(child_accumulated);
108                return Placeable::value_with_offset(
109                    placeable.width(),
110                    placeable.height(),
111                    NodeId::default(),
112                    (child_accumulated.x, child_accumulated.y),
113                );
114            }
115        };
116
117        // Store size
118        self.measured_size.set(result.size);
119
120        // Compute local offset from this coordinator
121        let local_offset = Point {
122            x: result.placement_offset_x,
123            y: result.placement_offset_y,
124        };
125
126        // Get wrapped's accumulated offset (O(1) - just reads its stored value)
127        // Note: wrapped.measure() was called by layout_node.measure(), so its offset is already set
128        let child_accumulated = self.wrapped.total_content_offset();
129
130        // Store OUR accumulated offset (local + child)
131        let accumulated = Point {
132            x: local_offset.x + child_accumulated.x,
133            y: local_offset.y + child_accumulated.y,
134        };
135        self.accumulated_offset.set(accumulated);
136
137        Placeable::value_with_offset(
138            result.size.width,
139            result.size.height,
140            NodeId::default(),
141            (accumulated.x, accumulated.y),
142        )
143    }
144
145    fn min_intrinsic_width(&self, height: f32) -> f32 {
146        let node_borrow = self.node.borrow();
147        if let Some(layout_node) = node_borrow.as_layout_node() {
148            layout_node.min_intrinsic_width(self.wrapped.as_ref(), height)
149        } else {
150            self.wrapped.min_intrinsic_width(height)
151        }
152    }
153
154    fn max_intrinsic_width(&self, height: f32) -> f32 {
155        let node_borrow = self.node.borrow();
156        if let Some(layout_node) = node_borrow.as_layout_node() {
157            layout_node.max_intrinsic_width(self.wrapped.as_ref(), height)
158        } else {
159            self.wrapped.max_intrinsic_width(height)
160        }
161    }
162
163    fn min_intrinsic_height(&self, width: f32) -> f32 {
164        let node_borrow = self.node.borrow();
165        if let Some(layout_node) = node_borrow.as_layout_node() {
166            layout_node.min_intrinsic_height(self.wrapped.as_ref(), width)
167        } else {
168            self.wrapped.min_intrinsic_height(width)
169        }
170    }
171
172    fn max_intrinsic_height(&self, width: f32) -> f32 {
173        let node_borrow = self.node.borrow();
174        if let Some(layout_node) = node_borrow.as_layout_node() {
175            layout_node.max_intrinsic_height(self.wrapped.as_ref(), width)
176        } else {
177            self.wrapped.max_intrinsic_height(width)
178        }
179    }
180}
181
182/// Inner coordinator that wraps the layout node's intrinsic content (MeasurePolicy).
183///
184/// This is analogous to Jetpack Compose's InnerNodeCoordinator.
185pub struct InnerCoordinator<'a> {
186    /// The measure policy to execute.
187    measure_policy: Rc<dyn MeasurePolicy>,
188    /// Child measurables.
189    measurables: &'a [Box<dyn Measurable>],
190    /// Measured size from last measure pass.
191    measured_size: Cell<Size>,
192    /// Position relative to parent.
193    /// Shared result holder to store the measure result for placement.
194    result_holder: Rc<RefCell<Option<MeasureResult>>>,
195}
196
197impl<'a> InnerCoordinator<'a> {
198    /// Creates a new inner coordinator with the given measure policy and children.
199    pub fn new(
200        measure_policy: Rc<dyn MeasurePolicy>,
201        measurables: &'a [Box<dyn Measurable>],
202        result_holder: Rc<RefCell<Option<MeasureResult>>>,
203    ) -> Self {
204        Self {
205            measure_policy,
206            measurables,
207            measured_size: Cell::new(Size::ZERO),
208            result_holder,
209        }
210    }
211}
212
213impl<'a> NodeCoordinator for InnerCoordinator<'a> {
214    fn total_content_offset(&self) -> Point {
215        Point::default()
216    }
217}
218
219impl<'a> Measurable for InnerCoordinator<'a> {
220    fn measure(&self, constraints: Constraints) -> Placeable {
221        // Execute the measure policy
222        let result = self.measure_policy.measure(self.measurables, constraints);
223
224        // Store measured size
225        let size = result.size;
226        self.measured_size.set(size);
227
228        // Store the result in the shared holder for placement extraction
229        *self.result_holder.borrow_mut() = Some(result);
230
231        // InnerCoordinator has no offset contribution
232        Placeable::value(size.width, size.height, NodeId::default())
233    }
234
235    fn min_intrinsic_width(&self, height: f32) -> f32 {
236        self.measure_policy
237            .min_intrinsic_width(self.measurables, height)
238    }
239
240    fn max_intrinsic_width(&self, height: f32) -> f32 {
241        self.measure_policy
242            .max_intrinsic_width(self.measurables, height)
243    }
244
245    fn min_intrinsic_height(&self, width: f32) -> f32 {
246        self.measure_policy
247            .min_intrinsic_height(self.measurables, width)
248    }
249
250    fn max_intrinsic_height(&self, width: f32) -> f32 {
251        self.measure_policy
252            .max_intrinsic_height(self.measurables, width)
253    }
254}