cranpose_ui_layout/core.rs
1//! Core layout traits and types shared by Compose UI widgets.
2
3use std::rc::Rc;
4
5use cranpose_core::NodeId;
6use cranpose_ui_graphics::Size;
7
8use crate::{Alignment, HorizontalAlignment, VerticalAlignment, constraints::Constraints};
9
10/// Parent data for flex layouts (Row/Column weights and alignment).
11#[derive(Clone, Copy, Debug, Default)]
12pub struct FlexParentData {
13 /// Weight for distributing remaining space in the main axis.
14 /// If > 0.0, this child participates in weighted distribution.
15 pub weight: f32,
16
17 /// Whether to fill the allocated space when using weight.
18 /// If true, child gets tight constraints; if false, child gets loose constraints.
19 pub fill: bool,
20}
21
22impl FlexParentData {
23 pub fn new(weight: f32, fill: bool) -> Self {
24 Self { weight, fill }
25 }
26
27 pub fn has_weight(&self) -> bool {
28 self.weight > 0.0
29 }
30}
31
32/// Layout metadata supplied by a child for its direct parent.
33///
34/// Weight/fill apply to Row and Column. Alignment values override the
35/// corresponding parent layout's default alignment for this child only.
36#[derive(Clone, Copy, Debug, Default, PartialEq)]
37pub struct ParentData {
38 pub weight: f32,
39 pub fill: bool,
40 pub box_alignment: Option<Alignment>,
41 pub row_alignment: Option<VerticalAlignment>,
42 pub column_alignment: Option<HorizontalAlignment>,
43}
44
45impl From<FlexParentData> for ParentData {
46 fn from(value: FlexParentData) -> Self {
47 Self {
48 weight: value.weight,
49 fill: value.fill,
50 ..Self::default()
51 }
52 }
53}
54
55impl ParentData {
56 pub fn has_weight(&self) -> bool {
57 self.weight > 0.0
58 }
59}
60
61/// Object capable of measuring a layout child and exposing intrinsic sizes.
62pub trait Measurable {
63 /// Measures the child with the provided constraints, returning a [`Placeable`].
64 fn measure(&self, constraints: Constraints) -> Placeable;
65
66 /// Returns the minimum width achievable for the given height.
67 fn min_intrinsic_width(&self, height: f32) -> f32;
68
69 /// Returns the maximum width achievable for the given height.
70 fn max_intrinsic_width(&self, height: f32) -> f32;
71
72 /// Returns the minimum height achievable for the given width.
73 fn min_intrinsic_height(&self, width: f32) -> f32;
74
75 /// Returns the maximum height achievable for the given width.
76 fn max_intrinsic_height(&self, width: f32) -> f32;
77
78 /// Returns flex parent data if this measurable has weight/fill properties.
79 /// Default implementation returns None (no weight).
80 fn flex_parent_data(&self) -> Option<FlexParentData> {
81 None
82 }
83
84 /// Returns all metadata consumed by the direct parent layout.
85 ///
86 /// The default preserves the older `flex_parent_data` customization
87 /// point so existing custom measurables continue to provide weights.
88 fn parent_data(&self) -> ParentData {
89 self.flex_parent_data().map(Into::into).unwrap_or_default()
90 }
91}
92
93/// Result of running a measurement pass for a single child.
94///
95/// Concrete struct replacing the former `dyn Placeable` trait object.
96/// This avoids a heap allocation per node per measure pass — the hot
97/// coordinator path (16-byte value) now lives entirely on the stack.
98pub struct Placeable {
99 width: f32,
100 height: f32,
101 node_id: NodeId,
102 content_offset_x: f32,
103 content_offset_y: f32,
104 place_target: Option<Rc<dyn PlaceTarget>>,
105}
106
107/// What a node-backed [`Placeable`] tells when its parent places it.
108pub trait PlaceTarget {
109 /// Places the node at `(x, y)` in its parent.
110 fn place(&self, x: f32, y: f32);
111}
112
113impl<F: Fn(f32, f32)> PlaceTarget for F {
114 fn place(&self, x: f32, y: f32) {
115 self(x, y);
116 }
117}
118
119impl Placeable {
120 /// Creates a pure-value placeable with no side effects on `place()`.
121 pub fn value(width: f32, height: f32, node_id: NodeId) -> Self {
122 Self {
123 width,
124 height,
125 node_id,
126 content_offset_x: 0.0,
127 content_offset_y: 0.0,
128 place_target: None,
129 }
130 }
131
132 /// Creates a pure-value placeable with a content offset.
133 pub fn value_with_offset(
134 width: f32,
135 height: f32,
136 node_id: NodeId,
137 content_offset: (f32, f32),
138 ) -> Self {
139 Self {
140 width,
141 height,
142 node_id,
143 content_offset_x: content_offset.0,
144 content_offset_y: content_offset.1,
145 place_target: None,
146 }
147 }
148
149 /// Creates a node-backed placeable whose `place()` tells `target`. The
150 /// target is shared, so a node's own measure state can be it without an
151 /// allocation per measure.
152 pub fn with_place_target(
153 width: f32,
154 height: f32,
155 node_id: NodeId,
156 target: Rc<dyn PlaceTarget>,
157 ) -> Self {
158 Self {
159 width,
160 height,
161 node_id,
162 content_offset_x: 0.0,
163 content_offset_y: 0.0,
164 place_target: Some(target),
165 }
166 }
167
168 /// Places the child at the provided coordinates relative to its parent.
169 pub fn place(&self, x: f32, y: f32) {
170 if let Some(target) = &self.place_target {
171 target.place(x, y);
172 }
173 }
174
175 /// Returns the measured width of the child.
176 pub fn width(&self) -> f32 {
177 self.width
178 }
179
180 /// Returns the measured height of the child.
181 pub fn height(&self) -> f32 {
182 self.height
183 }
184
185 /// Returns the identifier for the underlying layout node.
186 pub fn node_id(&self) -> NodeId {
187 self.node_id
188 }
189
190 /// Returns the accumulated content offset from the coordinator chain.
191 pub fn content_offset(&self) -> (f32, f32) {
192 (self.content_offset_x, self.content_offset_y)
193 }
194}
195
196/// Scope for measurement operations.
197///
198/// This is Compose's `MeasureScope` -- the receiver `MeasurePolicy.measure` runs
199/// on, which is what lets a measure pass see the density of the subtree it is
200/// measuring rather than some process-wide default. There is no sensible
201/// fallback value for either method: a policy that reads density must be given
202/// the real grid, so both are required rather than defaulted.
203pub trait MeasureScope {
204 /// Returns the current density for converting Dp to pixels.
205 fn density(&self) -> f32;
206
207 /// Returns the current font scale for converting Sp to pixels.
208 fn font_scale(&self) -> f32;
209}
210
211/// Policy responsible for measuring and placing children.
212pub trait MeasurePolicy {
213 /// Runs the measurement pass with the provided children and constraints.
214 fn measure(
215 &self,
216 scope: &dyn MeasureScope,
217 measurables: &[Box<dyn Measurable>],
218 constraints: Constraints,
219 ) -> MeasureResult;
220
221 /// Runs measurement into caller-owned placement storage.
222 ///
223 /// The default preserves the public [`MeasurePolicy::measure`] contract for custom
224 /// policies. Built-in policies override this to avoid allocating a fresh placement
225 /// vector on every measure pass.
226 fn measure_into(
227 &self,
228 scope: &dyn MeasureScope,
229 measurables: &[Box<dyn Measurable>],
230 constraints: Constraints,
231 placements: &mut Vec<Placement>,
232 ) -> Size {
233 let result = self.measure(scope, measurables, constraints);
234 placements.clear();
235 placements.extend(result.placements);
236 result.size
237 }
238
239 /// Computes the minimum intrinsic width of this policy.
240 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
241
242 /// Computes the maximum intrinsic width of this policy.
243 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
244
245 /// Computes the minimum intrinsic height of this policy.
246 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
247
248 /// Computes the maximum intrinsic height of this policy.
249 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
250}
251
252/// Result of a measurement operation.
253#[derive(Clone, Debug)]
254pub struct MeasureResult {
255 pub size: Size,
256 pub placements: Vec<Placement>,
257}
258
259impl MeasureResult {
260 pub fn new(size: Size, placements: Vec<Placement>) -> Self {
261 Self { size, placements }
262 }
263}
264
265/// Placement information for a measured child.
266#[derive(Clone, Copy, Debug)]
267pub struct Placement {
268 pub node_id: NodeId,
269 pub x: f32,
270 pub y: f32,
271 pub z_index: i32,
272}
273
274impl Placement {
275 pub fn new(node_id: NodeId, x: f32, y: f32, z_index: i32) -> Self {
276 Self {
277 node_id,
278 x,
279 y,
280 z_index,
281 }
282 }
283}
284
285/// Result of a layout modifier measurement operation.
286///
287/// Unlike `MeasureResult` which is for `MeasurePolicy` (multiple children),
288/// this type is specifically for layout modifiers which wrap a single piece
289/// of content and need to specify where that wrapped content should be placed.
290#[derive(Clone, Copy, Debug)]
291pub struct LayoutModifierMeasureResult {
292 /// The size this modifier will occupy.
293 pub size: Size,
294 /// The offset at which to place the wrapped content relative to
295 /// the top-left corner of this modifier's bounds.
296 /// For example, PaddingNode returns (padding.left, padding.top) here
297 /// to offset the child by the padding amount.
298 pub placement_offset_x: f32,
299 pub placement_offset_y: f32,
300}
301
302impl LayoutModifierMeasureResult {
303 pub fn new(size: Size, placement_offset_x: f32, placement_offset_y: f32) -> Self {
304 Self {
305 size,
306 placement_offset_x,
307 placement_offset_y,
308 }
309 }
310
311 /// Creates a result with zero placement offset (wrapped content placed at 0,0).
312 pub fn with_size(size: Size) -> Self {
313 Self {
314 size,
315 placement_offset_x: 0.0,
316 placement_offset_y: 0.0,
317 }
318 }
319}
320
321#[cfg(test)]
322#[path = "tests/core_tests.rs"]
323mod tests;