cranpose_ui_layout/core.rs
1//! Core layout traits and types shared by Compose UI widgets.
2
3use std::{any::Any, fmt, 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 /// A child's share of the room left over, and whether it fills it.
24 pub fn new(weight: f32, fill: bool) -> Self {
25 Self { weight, fill }
26 }
27
28 /// Whether the child takes a share of the room left over.
29 pub fn has_weight(&self) -> bool {
30 self.weight > 0.0
31 }
32}
33
34/// Data a child's modifier gives one kind of parent layout, which that
35/// layout's measure policy reads by its type: a grid cell, or the data of
36/// an app's own layout. Compose's `ParentDataModifierNode` data. Every
37/// `'static` type with `Debug` and `PartialEq` is layout data.
38pub trait LayoutData: Any + fmt::Debug {
39 /// Whether `other` holds the same data, so a parent can keep its last
40 /// measure.
41 fn same_as(&self, other: &dyn LayoutData) -> bool;
42
43 /// The data as `Any`, to read it by its type.
44 fn as_any(&self) -> &dyn Any;
45}
46
47impl<T: Any + fmt::Debug + PartialEq> LayoutData for T {
48 fn same_as(&self, other: &dyn LayoutData) -> bool {
49 other
50 .as_any()
51 .downcast_ref::<T>()
52 .is_some_and(|other| other == self)
53 }
54
55 fn as_any(&self) -> &dyn Any {
56 self
57 }
58}
59
60/// Layout metadata supplied by a child for its direct parent.
61///
62/// Weight/fill apply to Row and Column. Alignment values override the
63/// corresponding parent layout's default alignment for this child only.
64#[derive(Clone, Debug, Default)]
65pub struct ParentData {
66 /// The child's share of a Row's or Column's room left over; 0 for none.
67 pub weight: f32,
68 /// Whether a weighted child fills its share or may be smaller.
69 pub fill: bool,
70 /// Where the child sits in a Box, over the Box's own alignment.
71 pub box_alignment: Option<Alignment>,
72 /// Where the child sits across a Row, over the Row's own alignment.
73 pub row_alignment: Option<VerticalAlignment>,
74 /// Aligns this child's first baseline with other baseline-aligned Row children.
75 pub row_baseline: bool,
76 /// Where the child sits across a Column, over the Column's own alignment.
77 pub column_alignment: Option<HorizontalAlignment>,
78 /// The fraction of the parent's width the child's outermost horizontal
79 /// fill modifier asks for; 0 without one. A FlexBox reads it as the
80 /// child's default grow. Compose's `FillModifierParentData`.
81 pub fill_width_fraction: f32,
82 /// The same for height.
83 pub fill_height_fraction: f32,
84 /// The data of the child's outermost [`LayoutData`] modifier, for a
85 /// parent that reads it by its type.
86 pub layout_data: Option<Rc<dyn LayoutData>>,
87}
88
89impl PartialEq for ParentData {
90 fn eq(&self, other: &Self) -> bool {
91 self.weight == other.weight
92 && self.fill == other.fill
93 && self.box_alignment == other.box_alignment
94 && self.row_alignment == other.row_alignment
95 && self.row_baseline == other.row_baseline
96 && self.column_alignment == other.column_alignment
97 && self.fill_width_fraction == other.fill_width_fraction
98 && self.fill_height_fraction == other.fill_height_fraction
99 && match (&self.layout_data, &other.layout_data) {
100 (None, None) => true,
101 (Some(data), Some(other)) => Rc::ptr_eq(data, other) || data.same_as(&**other),
102 _ => false,
103 }
104 }
105}
106
107impl From<FlexParentData> for ParentData {
108 fn from(value: FlexParentData) -> Self {
109 Self {
110 weight: value.weight,
111 fill: value.fill,
112 ..Self::default()
113 }
114 }
115}
116
117impl ParentData {
118 /// Whether the child takes a share of the room left over.
119 pub fn has_weight(&self) -> bool {
120 self.weight > 0.0
121 }
122
123 /// The child's [`LayoutData`] when it is a `T`.
124 pub fn layout_data<T: Any>(&self) -> Option<&T> {
125 self.layout_data.as_deref()?.as_any().downcast_ref()
126 }
127}
128
129/// Object capable of measuring a layout child and exposing intrinsic sizes.
130pub trait Measurable {
131 /// Measures the child with the provided constraints, returning a [`Placeable`].
132 fn measure(&self, constraints: Constraints) -> Placeable;
133
134 /// The size the last measure gave its parent and the incoming
135 /// constraints that measurement holds for: see
136 /// [`crate::ConstraintsHold`]. `None`, the default, when it cannot tell.
137 fn measured_hold(&self) -> Option<(Size, crate::ConstraintsHold)> {
138 None
139 }
140
141 /// Returns the minimum width achievable for the given height.
142 fn min_intrinsic_width(&self, height: f32) -> f32;
143
144 /// Returns the maximum width achievable for the given height.
145 fn max_intrinsic_width(&self, height: f32) -> f32;
146
147 /// Returns the minimum height achievable for the given width.
148 fn min_intrinsic_height(&self, width: f32) -> f32;
149
150 /// Returns the maximum height achievable for the given width.
151 fn max_intrinsic_height(&self, width: f32) -> f32;
152
153 /// Returns flex parent data if this measurable has weight/fill properties.
154 /// Default implementation returns None (no weight).
155 fn flex_parent_data(&self) -> Option<FlexParentData> {
156 None
157 }
158
159 /// Returns all metadata consumed by the direct parent layout.
160 ///
161 /// The default preserves the older `flex_parent_data` customization
162 /// point so existing custom measurables continue to provide weights.
163 fn parent_data(&self) -> ParentData {
164 self.flex_parent_data().map(Into::into).unwrap_or_default()
165 }
166}
167
168/// Result of running a measurement pass for a single child.
169///
170/// Carries measurement and alignment lines without allocating a trait object.
171pub struct Placeable {
172 width: f32,
173 height: f32,
174 node_id: NodeId,
175 content_offset_x: f32,
176 content_offset_y: f32,
177 alignment_lines: crate::AlignmentLines,
178 place_target: Option<Rc<dyn PlaceTarget>>,
179}
180
181/// What a node-backed [`Placeable`] tells when its parent places it.
182pub trait PlaceTarget {
183 /// Places the node at `(x, y)` in its parent.
184 fn place(&self, x: f32, y: f32);
185}
186
187impl<F: Fn(f32, f32)> PlaceTarget for F {
188 fn place(&self, x: f32, y: f32) {
189 self(x, y);
190 }
191}
192
193impl Placeable {
194 /// Creates a pure-value placeable with no side effects on `place()`.
195 pub fn value(width: f32, height: f32, node_id: NodeId) -> Self {
196 Self {
197 width,
198 height,
199 node_id,
200 content_offset_x: 0.0,
201 content_offset_y: 0.0,
202 alignment_lines: crate::AlignmentLines::default(),
203 place_target: None,
204 }
205 }
206
207 /// Creates a pure-value placeable with a content offset.
208 pub fn value_with_offset(
209 width: f32,
210 height: f32,
211 node_id: NodeId,
212 content_offset: (f32, f32),
213 ) -> Self {
214 Self {
215 width,
216 height,
217 node_id,
218 content_offset_x: content_offset.0,
219 content_offset_y: content_offset.1,
220 alignment_lines: crate::AlignmentLines::default(),
221 place_target: None,
222 }
223 }
224
225 /// Creates a node-backed placeable whose `place()` tells `target`. The
226 /// target is shared, so a node's own measure state can be it without an
227 /// allocation per measure.
228 pub fn with_place_target(
229 width: f32,
230 height: f32,
231 node_id: NodeId,
232 target: Rc<dyn PlaceTarget>,
233 ) -> Self {
234 Self {
235 width,
236 height,
237 node_id,
238 content_offset_x: 0.0,
239 content_offset_y: 0.0,
240 alignment_lines: crate::AlignmentLines::default(),
241 place_target: Some(target),
242 }
243 }
244
245 /// Places the child at the provided coordinates relative to its parent.
246 pub fn place(&self, x: f32, y: f32) {
247 if let Some(target) = &self.place_target {
248 target.place(x, y);
249 }
250 }
251
252 /// Returns the measured width of the child.
253 pub fn width(&self) -> f32 {
254 self.width
255 }
256
257 /// Returns the measured height of the child.
258 pub fn height(&self) -> f32 {
259 self.height
260 }
261
262 /// Returns the identifier for the underlying layout node.
263 pub fn node_id(&self) -> NodeId {
264 self.node_id
265 }
266
267 /// Returns the accumulated content offset from the coordinator chain.
268 pub fn content_offset(&self) -> (f32, f32) {
269 (self.content_offset_x, self.content_offset_y)
270 }
271
272 /// Supplies alignment lines relative to this placeable's top edge.
273 pub fn with_alignment_lines(mut self, alignment_lines: crate::AlignmentLines) -> Self {
274 self.alignment_lines = alignment_lines;
275 self
276 }
277
278 /// Returns the text baselines relative to this placeable's top edge.
279 pub fn alignment_lines(&self) -> crate::AlignmentLines {
280 self.alignment_lines
281 }
282}
283
284/// Scope for measurement operations.
285///
286/// This is Compose's `MeasureScope` -- the receiver `MeasurePolicy.measure` runs
287/// on, which is what lets a measure pass see the density of the subtree it is
288/// measuring rather than some process-wide default. There is no sensible
289/// fallback value for either method: a policy that reads density must be given
290/// the real grid, so both are required rather than defaulted.
291pub trait MeasureScope {
292 /// Returns the current density for converting Dp to pixels.
293 fn density(&self) -> f32;
294
295 /// Returns the current font scale for converting Sp to pixels.
296 fn font_scale(&self) -> f32;
297}
298
299/// Policy responsible for measuring and placing children.
300pub trait MeasurePolicy {
301 /// Runs the measurement pass with the provided children and constraints.
302 fn measure(
303 &self,
304 scope: &dyn MeasureScope,
305 measurables: &[Box<dyn Measurable>],
306 constraints: Constraints,
307 ) -> MeasureResult;
308
309 /// Runs measurement into caller-owned placement storage.
310 ///
311 /// The default preserves the public [`MeasurePolicy::measure`] contract for custom
312 /// policies. Built-in policies override this to avoid allocating a fresh placement
313 /// vector on every measure pass.
314 fn measure_into(
315 &self,
316 scope: &dyn MeasureScope,
317 measurables: &[Box<dyn Measurable>],
318 constraints: Constraints,
319 placements: &mut Vec<Placement>,
320 ) -> Measurement {
321 let result = self.measure(scope, measurables, constraints);
322 placements.clear();
323 placements.extend(result.placements);
324 Measurement {
325 size: result.size,
326 alignment_lines: result.alignment_lines,
327 }
328 }
329
330 /// The incoming constraints the measure that sized the layout `size`
331 /// under `constraints` holds for, when the policy can tell: see
332 /// [`crate::ConstraintsHold`]. `None`, the default, holds for
333 /// `constraints` alone.
334 fn measure_hold(
335 &self,
336 _measurables: &[Box<dyn Measurable>],
337 _constraints: Constraints,
338 _size: Size,
339 ) -> Option<crate::ConstraintsHold> {
340 None
341 }
342
343 /// Computes the minimum intrinsic width of this policy.
344 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
345
346 /// Computes the maximum intrinsic width of this policy.
347 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
348
349 /// Computes the minimum intrinsic height of this policy.
350 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
351
352 /// Computes the maximum intrinsic height of this policy.
353 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
354}
355
356/// The size and explicit alignment lines produced by a measure policy.
357#[derive(Clone, Copy, Debug, Default, PartialEq)]
358pub struct Measurement {
359 /// The size occupied by the measured layout.
360 pub size: Size,
361 /// Explicit baselines in this layout's coordinates; unspecified lines are inherited.
362 pub alignment_lines: crate::AlignmentLines,
363}
364
365impl From<Size> for Measurement {
366 fn from(size: Size) -> Self {
367 Self {
368 size,
369 alignment_lines: crate::AlignmentLines::default(),
370 }
371 }
372}
373
374/// Result of a measurement operation.
375#[derive(Clone, Debug)]
376pub struct MeasureResult {
377 /// The size the layout takes.
378 pub size: Size,
379 /// Explicit baselines in this layout's coordinates; unspecified lines are inherited.
380 pub alignment_lines: crate::AlignmentLines,
381 /// Where the children go.
382 pub placements: Vec<Placement>,
383}
384
385impl MeasureResult {
386 /// Creates a result from a size or a measurement that already has alignment lines.
387 pub fn new(measurement: impl Into<Measurement>, placements: Vec<Placement>) -> Self {
388 let measurement = measurement.into();
389 Self {
390 size: measurement.size,
391 alignment_lines: measurement.alignment_lines,
392 placements,
393 }
394 }
395
396 /// Reports explicit lines relative to this layout's top edge.
397 pub fn with_alignment_lines(mut self, alignment_lines: crate::AlignmentLines) -> Self {
398 self.alignment_lines = alignment_lines;
399 self
400 }
401}
402
403/// Placement information for a measured child.
404#[derive(Clone, Copy, Debug)]
405pub struct Placement {
406 /// The child placed.
407 pub node_id: NodeId,
408 /// Its left edge in the parent, in logical pixels.
409 pub x: f32,
410 /// Its top edge in the parent, in logical pixels.
411 pub y: f32,
412 /// Its draw order among its siblings: higher draws on top.
413 pub z_index: i32,
414}
415
416impl Placement {
417 /// `node_id` at `(x, y)` in the parent, drawn in `z_index` order.
418 pub fn new(node_id: NodeId, x: f32, y: f32, z_index: i32) -> Self {
419 Self {
420 node_id,
421 x,
422 y,
423 z_index,
424 }
425 }
426}
427
428/// Result of a layout modifier measurement operation.
429///
430/// Unlike `MeasureResult` which is for `MeasurePolicy` (multiple children),
431/// this type is specifically for layout modifiers which wrap a single piece
432/// of content and need to specify where that wrapped content should be placed.
433#[derive(Clone, Copy, Debug)]
434pub struct LayoutModifierMeasureResult {
435 /// The size this modifier will occupy.
436 pub size: Size,
437 /// The offset at which to place the wrapped content relative to
438 /// the top-left corner of this modifier's bounds.
439 /// For example, PaddingNode returns (padding.left, padding.top) here
440 /// to offset the child by the padding amount.
441 pub placement_offset_x: f32,
442 /// The vertical offset of the wrapped content, as `placement_offset_x`.
443 pub placement_offset_y: f32,
444 /// Explicit alignment lines. Unspecified lines are inherited from wrapped content.
445 pub alignment_lines: crate::AlignmentLines,
446}
447
448impl LayoutModifierMeasureResult {
449 /// A modifier of `size` that places its content at the offset.
450 pub fn new(size: Size, placement_offset_x: f32, placement_offset_y: f32) -> Self {
451 Self {
452 size,
453 placement_offset_x,
454 placement_offset_y,
455 alignment_lines: crate::AlignmentLines::default(),
456 }
457 }
458
459 /// Creates a result with zero placement offset (wrapped content placed at 0,0).
460 pub fn with_size(size: Size) -> Self {
461 Self {
462 size,
463 placement_offset_x: 0.0,
464 placement_offset_y: 0.0,
465 alignment_lines: crate::AlignmentLines::default(),
466 }
467 }
468
469 /// Overrides the wrapped content's alignment lines in this modifier's coordinates.
470 pub fn with_alignment_lines(mut self, alignment_lines: crate::AlignmentLines) -> Self {
471 self.alignment_lines = alignment_lines;
472 self
473 }
474}
475
476#[cfg(test)]
477#[path = "tests/core_tests.rs"]
478mod tests;