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