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::{constraints::Constraints, Alignment, HorizontalAlignment, VerticalAlignment};
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 /// Optional side-effect executed by `place()`. `None` for pure-value
105 /// placeables (coordinators, subcompose).
106 place_fn: Option<Rc<dyn Fn(f32, f32)>>,
107}
108
109impl Placeable {
110 /// Creates a pure-value placeable with no side effects on `place()`.
111 pub fn value(width: f32, height: f32, node_id: NodeId) -> Self {
112 Self {
113 width,
114 height,
115 node_id,
116 content_offset_x: 0.0,
117 content_offset_y: 0.0,
118 place_fn: None,
119 }
120 }
121
122 /// Creates a pure-value placeable with a content offset.
123 pub fn value_with_offset(
124 width: f32,
125 height: f32,
126 node_id: NodeId,
127 content_offset: (f32, f32),
128 ) -> Self {
129 Self {
130 width,
131 height,
132 node_id,
133 content_offset_x: content_offset.0,
134 content_offset_y: content_offset.1,
135 place_fn: None,
136 }
137 }
138
139 /// Creates a node-backed placeable whose `place()` triggers a side effect.
140 pub fn with_place_fn(
141 width: f32,
142 height: f32,
143 node_id: NodeId,
144 place_fn: Rc<dyn Fn(f32, f32)>,
145 ) -> Self {
146 Self {
147 width,
148 height,
149 node_id,
150 content_offset_x: 0.0,
151 content_offset_y: 0.0,
152 place_fn: Some(place_fn),
153 }
154 }
155
156 /// Places the child at the provided coordinates relative to its parent.
157 pub fn place(&self, x: f32, y: f32) {
158 if let Some(f) = &self.place_fn {
159 f(x, y);
160 }
161 }
162
163 /// Returns the measured width of the child.
164 pub fn width(&self) -> f32 {
165 self.width
166 }
167
168 /// Returns the measured height of the child.
169 pub fn height(&self) -> f32 {
170 self.height
171 }
172
173 /// Returns the identifier for the underlying layout node.
174 pub fn node_id(&self) -> NodeId {
175 self.node_id
176 }
177
178 /// Returns the accumulated content offset from the coordinator chain.
179 pub fn content_offset(&self) -> (f32, f32) {
180 (self.content_offset_x, self.content_offset_y)
181 }
182}
183
184/// Scope for measurement operations.
185///
186/// This is Compose's `MeasureScope` -- the receiver `MeasurePolicy.measure` runs
187/// on, which is what lets a measure pass see the density of the subtree it is
188/// measuring rather than some process-wide default. There is no sensible
189/// fallback value for either method: a policy that reads density must be given
190/// the real grid, so both are required rather than defaulted.
191pub trait MeasureScope {
192 /// Returns the current density for converting Dp to pixels.
193 fn density(&self) -> f32;
194
195 /// Returns the current font scale for converting Sp to pixels.
196 fn font_scale(&self) -> f32;
197}
198
199/// Policy responsible for measuring and placing children.
200pub trait MeasurePolicy {
201 /// Runs the measurement pass with the provided children and constraints.
202 fn measure(
203 &self,
204 scope: &dyn MeasureScope,
205 measurables: &[Box<dyn Measurable>],
206 constraints: Constraints,
207 ) -> MeasureResult;
208
209 /// Runs measurement into caller-owned placement storage.
210 ///
211 /// The default preserves the public [`MeasurePolicy::measure`] contract for custom
212 /// policies. Built-in policies override this to avoid allocating a fresh placement
213 /// vector on every measure pass.
214 fn measure_into(
215 &self,
216 scope: &dyn MeasureScope,
217 measurables: &[Box<dyn Measurable>],
218 constraints: Constraints,
219 placements: &mut Vec<Placement>,
220 ) -> Size {
221 let result = self.measure(scope, measurables, constraints);
222 placements.clear();
223 placements.extend(result.placements);
224 result.size
225 }
226
227 /// Computes the minimum intrinsic width of this policy.
228 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
229
230 /// Computes the maximum intrinsic width of this policy.
231 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
232
233 /// Computes the minimum intrinsic height of this policy.
234 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
235
236 /// Computes the maximum intrinsic height of this policy.
237 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
238}
239
240/// Result of a measurement operation.
241#[derive(Clone, Debug)]
242pub struct MeasureResult {
243 pub size: Size,
244 pub placements: Vec<Placement>,
245}
246
247impl MeasureResult {
248 pub fn new(size: Size, placements: Vec<Placement>) -> Self {
249 Self { size, placements }
250 }
251}
252
253/// Placement information for a measured child.
254#[derive(Clone, Copy, Debug)]
255pub struct Placement {
256 pub node_id: NodeId,
257 pub x: f32,
258 pub y: f32,
259 pub z_index: i32,
260}
261
262impl Placement {
263 pub fn new(node_id: NodeId, x: f32, y: f32, z_index: i32) -> Self {
264 Self {
265 node_id,
266 x,
267 y,
268 z_index,
269 }
270 }
271}
272
273/// Result of a layout modifier measurement operation.
274///
275/// Unlike `MeasureResult` which is for `MeasurePolicy` (multiple children),
276/// this type is specifically for layout modifiers which wrap a single piece
277/// of content and need to specify where that wrapped content should be placed.
278#[derive(Clone, Copy, Debug)]
279pub struct LayoutModifierMeasureResult {
280 /// The size this modifier will occupy.
281 pub size: Size,
282 /// The offset at which to place the wrapped content relative to
283 /// the top-left corner of this modifier's bounds.
284 /// For example, PaddingNode returns (padding.left, padding.top) here
285 /// to offset the child by the padding amount.
286 pub placement_offset_x: f32,
287 pub placement_offset_y: f32,
288}
289
290impl LayoutModifierMeasureResult {
291 pub fn new(size: Size, placement_offset_x: f32, placement_offset_y: f32) -> Self {
292 Self {
293 size,
294 placement_offset_x,
295 placement_offset_y,
296 }
297 }
298
299 /// Creates a result with zero placement offset (wrapped content placed at 0,0).
300 pub fn with_size(size: Size) -> Self {
301 Self {
302 size,
303 placement_offset_x: 0.0,
304 placement_offset_y: 0.0,
305 }
306 }
307}