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) -> Box<dyn 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 Box::new(CoordinatorPlaceable {
109                    size: Size {
110                        width: placeable.width(),
111                        height: placeable.height(),
112                    },
113                    content_offset: child_accumulated,
114                });
115            }
116        };
117
118        // Store size
119        self.measured_size.set(result.size);
120
121        // Compute local offset from this coordinator
122        let local_offset = Point {
123            x: result.placement_offset_x,
124            y: result.placement_offset_y,
125        };
126
127        // Get wrapped's accumulated offset (O(1) - just reads its stored value)
128        // Note: wrapped.measure() was called by layout_node.measure(), so its offset is already set
129        let child_accumulated = self.wrapped.total_content_offset();
130
131        // Store OUR accumulated offset (local + child)
132        let accumulated = Point {
133            x: local_offset.x + child_accumulated.x,
134            y: local_offset.y + child_accumulated.y,
135        };
136        self.accumulated_offset.set(accumulated);
137
138        Box::new(CoordinatorPlaceable {
139            size: result.size,
140            content_offset: accumulated,
141        })
142    }
143
144    fn min_intrinsic_width(&self, height: f32) -> f32 {
145        let node_borrow = self.node.borrow();
146        if let Some(layout_node) = node_borrow.as_layout_node() {
147            layout_node.min_intrinsic_width(self.wrapped.as_ref(), height)
148        } else {
149            self.wrapped.min_intrinsic_width(height)
150        }
151    }
152
153    fn max_intrinsic_width(&self, height: f32) -> f32 {
154        let node_borrow = self.node.borrow();
155        if let Some(layout_node) = node_borrow.as_layout_node() {
156            layout_node.max_intrinsic_width(self.wrapped.as_ref(), height)
157        } else {
158            self.wrapped.max_intrinsic_width(height)
159        }
160    }
161
162    fn min_intrinsic_height(&self, width: f32) -> f32 {
163        let node_borrow = self.node.borrow();
164        if let Some(layout_node) = node_borrow.as_layout_node() {
165            layout_node.min_intrinsic_height(self.wrapped.as_ref(), width)
166        } else {
167            self.wrapped.min_intrinsic_height(width)
168        }
169    }
170
171    fn max_intrinsic_height(&self, width: f32) -> f32 {
172        let node_borrow = self.node.borrow();
173        if let Some(layout_node) = node_borrow.as_layout_node() {
174            layout_node.max_intrinsic_height(self.wrapped.as_ref(), width)
175        } else {
176            self.wrapped.max_intrinsic_height(width)
177        }
178    }
179}
180
181/// Inner coordinator that wraps the layout node's intrinsic content (MeasurePolicy).
182///
183/// This is analogous to Jetpack Compose's InnerNodeCoordinator.
184pub struct InnerCoordinator<'a> {
185    /// The measure policy to execute.
186    measure_policy: Rc<dyn MeasurePolicy>,
187    /// Child measurables.
188    measurables: &'a [Box<dyn Measurable>],
189    /// Measured size from last measure pass.
190    measured_size: Cell<Size>,
191    /// Position relative to parent.
192    /// Shared result holder to store the measure result for placement.
193    result_holder: Rc<RefCell<Option<MeasureResult>>>,
194}
195
196impl<'a> InnerCoordinator<'a> {
197    /// Creates a new inner coordinator with the given measure policy and children.
198    pub fn new(
199        measure_policy: Rc<dyn MeasurePolicy>,
200        measurables: &'a [Box<dyn Measurable>],
201        result_holder: Rc<RefCell<Option<MeasureResult>>>,
202    ) -> Self {
203        Self {
204            measure_policy,
205            measurables,
206            measured_size: Cell::new(Size::ZERO),
207            result_holder,
208        }
209    }
210}
211
212impl<'a> NodeCoordinator for InnerCoordinator<'a> {
213    fn total_content_offset(&self) -> Point {
214        Point::default()
215    }
216}
217
218impl<'a> Measurable for InnerCoordinator<'a> {
219    fn measure(&self, constraints: Constraints) -> Box<dyn Placeable> {
220        // Execute the measure policy
221        let result = self.measure_policy.measure(self.measurables, constraints);
222
223        // Store measured size
224        let size = result.size;
225        self.measured_size.set(size);
226
227        // Store the result in the shared holder for placement extraction
228        *self.result_holder.borrow_mut() = Some(result);
229
230        // InnerCoordinator has no offset contribution
231        Box::new(CoordinatorPlaceable {
232            size,
233            content_offset: Point::default(),
234        })
235    }
236
237    fn min_intrinsic_width(&self, height: f32) -> f32 {
238        self.measure_policy
239            .min_intrinsic_width(self.measurables, height)
240    }
241
242    fn max_intrinsic_width(&self, height: f32) -> f32 {
243        self.measure_policy
244            .max_intrinsic_width(self.measurables, height)
245    }
246
247    fn min_intrinsic_height(&self, width: f32) -> f32 {
248        self.measure_policy
249            .min_intrinsic_height(self.measurables, width)
250    }
251
252    fn max_intrinsic_height(&self, width: f32) -> f32 {
253        self.measure_policy
254            .max_intrinsic_height(self.measurables, width)
255    }
256}
257
258/// Placeable implementation for coordinators.
259/// Carries the accumulated content offset from the coordinator chain.
260struct CoordinatorPlaceable {
261    size: Size,
262    /// Accumulated content offset (sum of all offsets from this coordinator down).
263    content_offset: Point,
264}
265
266impl Placeable for CoordinatorPlaceable {
267    fn width(&self) -> f32 {
268        self.size.width
269    }
270
271    fn height(&self) -> f32 {
272        self.size.height
273    }
274
275    fn place(&self, _x: f32, _y: f32) {
276        // Placement is handled externally by the layout system
277    }
278
279    fn node_id(&self) -> NodeId {
280        NodeId::default()
281    }
282
283    fn content_offset(&self) -> (f32, f32) {
284        (self.content_offset.x, self.content_offset.y)
285    }
286}