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.
184pub trait MeasureScope {
185 /// Returns the current density for converting Dp to pixels.
186 fn density(&self) -> f32 {
187 1.0
188 }
189
190 /// Returns the current font scale for converting Sp to pixels.
191 fn font_scale(&self) -> f32 {
192 1.0
193 }
194}
195
196/// Policy responsible for measuring and placing children.
197pub trait MeasurePolicy {
198 /// Runs the measurement pass with the provided children and constraints.
199 fn measure(
200 &self,
201 measurables: &[Box<dyn Measurable>],
202 constraints: Constraints,
203 ) -> MeasureResult;
204
205 /// Runs measurement into caller-owned placement storage.
206 ///
207 /// The default preserves the public [`MeasurePolicy::measure`] contract for custom
208 /// policies. Built-in policies override this to avoid allocating a fresh placement
209 /// vector on every measure pass.
210 fn measure_into(
211 &self,
212 measurables: &[Box<dyn Measurable>],
213 constraints: Constraints,
214 placements: &mut Vec<Placement>,
215 ) -> Size {
216 let result = self.measure(measurables, constraints);
217 placements.clear();
218 placements.extend(result.placements);
219 result.size
220 }
221
222 /// Computes the minimum intrinsic width of this policy.
223 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
224
225 /// Computes the maximum intrinsic width of this policy.
226 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
227
228 /// Computes the minimum intrinsic height of this policy.
229 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
230
231 /// Computes the maximum intrinsic height of this policy.
232 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
233}
234
235/// Result of a measurement operation.
236#[derive(Clone, Debug)]
237pub struct MeasureResult {
238 pub size: Size,
239 pub placements: Vec<Placement>,
240}
241
242impl MeasureResult {
243 pub fn new(size: Size, placements: Vec<Placement>) -> Self {
244 Self { size, placements }
245 }
246}
247
248/// Placement information for a measured child.
249#[derive(Clone, Copy, Debug)]
250pub struct Placement {
251 pub node_id: NodeId,
252 pub x: f32,
253 pub y: f32,
254 pub z_index: i32,
255}
256
257impl Placement {
258 pub fn new(node_id: NodeId, x: f32, y: f32, z_index: i32) -> Self {
259 Self {
260 node_id,
261 x,
262 y,
263 z_index,
264 }
265 }
266}
267
268/// Result of a layout modifier measurement operation.
269///
270/// Unlike `MeasureResult` which is for `MeasurePolicy` (multiple children),
271/// this type is specifically for layout modifiers which wrap a single piece
272/// of content and need to specify where that wrapped content should be placed.
273#[derive(Clone, Copy, Debug)]
274pub struct LayoutModifierMeasureResult {
275 /// The size this modifier will occupy.
276 pub size: Size,
277 /// The offset at which to place the wrapped content relative to
278 /// the top-left corner of this modifier's bounds.
279 /// For example, PaddingNode returns (padding.left, padding.top) here
280 /// to offset the child by the padding amount.
281 pub placement_offset_x: f32,
282 pub placement_offset_y: f32,
283}
284
285impl LayoutModifierMeasureResult {
286 pub fn new(size: Size, placement_offset_x: f32, placement_offset_y: f32) -> Self {
287 Self {
288 size,
289 placement_offset_x,
290 placement_offset_y,
291 }
292 }
293
294 /// Creates a result with zero placement offset (wrapped content placed at 0,0).
295 pub fn with_size(size: Size) -> Self {
296 Self {
297 size,
298 placement_offset_x: 0.0,
299 placement_offset_y: 0.0,
300 }
301 }
302}