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. A layout compares
300/// the policy it is given with the one its node holds, through [`Any`].
301pub trait MeasurePolicy: Any {
302 /// Runs the measurement pass with the provided children and constraints.
303 fn measure(
304 &self,
305 scope: &dyn MeasureScope,
306 measurables: &[Box<dyn Measurable>],
307 constraints: Constraints,
308 ) -> MeasureResult;
309
310 /// Runs measurement into caller-owned placement storage.
311 ///
312 /// The default preserves the public [`MeasurePolicy::measure`] contract for custom
313 /// policies. Built-in policies override this to avoid allocating a fresh placement
314 /// vector on every measure pass.
315 fn measure_into(
316 &self,
317 scope: &dyn MeasureScope,
318 measurables: &[Box<dyn Measurable>],
319 constraints: Constraints,
320 placements: &mut Vec<Placement>,
321 ) -> Measurement {
322 let result = self.measure(scope, measurables, constraints);
323 placements.clear();
324 placements.extend(result.placements);
325 Measurement {
326 size: result.size,
327 alignment_lines: result.alignment_lines,
328 }
329 }
330
331 /// The incoming constraints the measure that sized the layout `size`
332 /// under `constraints` holds for, when the policy can tell: see
333 /// [`crate::ConstraintsHold`]. `None`, the default, holds for
334 /// `constraints` alone.
335 fn measure_hold(
336 &self,
337 _measurables: &[Box<dyn Measurable>],
338 _constraints: Constraints,
339 _size: Size,
340 ) -> Option<crate::ConstraintsHold> {
341 None
342 }
343
344 /// Whether this policy may lay its children out otherwise when a child's
345 /// parent data goes from `before` to `after`. The default reads all of
346 /// it; a policy that reads part of it answers for that part, so a change
347 /// elsewhere keeps its measurement.
348 fn reads_parent_data_change(&self, before: &ParentData, after: &ParentData) -> bool {
349 before != after
350 }
351
352 /// Whether the measure under `constraints` gave a size and child
353 /// positions that no child's size enters: a child that measures to
354 /// another size then leaves this layout as it is, and the layout keeps
355 /// its measurement. `false`, the default, measures the layout again.
356 fn ignores_child_sizes(
357 &self,
358 _measurables: &[Box<dyn Measurable>],
359 _constraints: Constraints,
360 ) -> bool {
361 false
362 }
363
364 /// Computes the minimum intrinsic width of this policy.
365 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
366
367 /// Computes the maximum intrinsic width of this policy.
368 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32;
369
370 /// Computes the minimum intrinsic height of this policy.
371 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
372
373 /// Computes the maximum intrinsic height of this policy.
374 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32;
375}
376
377/// The size and explicit alignment lines produced by a measure policy.
378#[derive(Clone, Copy, Debug, Default, PartialEq)]
379pub struct Measurement {
380 /// The size occupied by the measured layout.
381 pub size: Size,
382 /// Explicit baselines in this layout's coordinates; unspecified lines are inherited.
383 pub alignment_lines: crate::AlignmentLines,
384}
385
386impl From<Size> for Measurement {
387 fn from(size: Size) -> Self {
388 Self {
389 size,
390 alignment_lines: crate::AlignmentLines::default(),
391 }
392 }
393}
394
395/// Result of a measurement operation.
396#[derive(Clone, Debug)]
397pub struct MeasureResult {
398 /// The size the layout takes.
399 pub size: Size,
400 /// Explicit baselines in this layout's coordinates; unspecified lines are inherited.
401 pub alignment_lines: crate::AlignmentLines,
402 /// Where the children go.
403 pub placements: Vec<Placement>,
404}
405
406impl MeasureResult {
407 /// Creates a result from a size or a measurement that already has alignment lines.
408 pub fn new(measurement: impl Into<Measurement>, placements: Vec<Placement>) -> Self {
409 let measurement = measurement.into();
410 Self {
411 size: measurement.size,
412 alignment_lines: measurement.alignment_lines,
413 placements,
414 }
415 }
416
417 /// Reports explicit lines relative to this layout's top edge.
418 pub fn with_alignment_lines(mut self, alignment_lines: crate::AlignmentLines) -> Self {
419 self.alignment_lines = alignment_lines;
420 self
421 }
422}
423
424/// Placement information for a measured child.
425#[derive(Clone, Copy, Debug)]
426pub struct Placement {
427 /// The child placed.
428 pub node_id: NodeId,
429 /// Its left edge in the parent, in logical pixels.
430 pub x: f32,
431 /// Its top edge in the parent, in logical pixels.
432 pub y: f32,
433 /// Its draw order among its siblings: higher draws on top.
434 pub z_index: i32,
435}
436
437impl Placement {
438 /// `node_id` at `(x, y)` in the parent, drawn in `z_index` order.
439 pub fn new(node_id: NodeId, x: f32, y: f32, z_index: i32) -> Self {
440 Self {
441 node_id,
442 x,
443 y,
444 z_index,
445 }
446 }
447}
448
449/// Result of a layout modifier measurement operation.
450///
451/// Unlike `MeasureResult` which is for `MeasurePolicy` (multiple children),
452/// this type is specifically for layout modifiers which wrap a single piece
453/// of content and need to specify where that wrapped content should be placed.
454#[derive(Clone, Copy, Debug)]
455pub struct LayoutModifierMeasureResult {
456 /// The size this modifier will occupy.
457 pub size: Size,
458 /// The offset at which to place the wrapped content relative to
459 /// the top-left corner of this modifier's bounds.
460 /// For example, PaddingNode returns (padding.left, padding.top) here
461 /// to offset the child by the padding amount.
462 pub placement_offset_x: f32,
463 /// The vertical offset of the wrapped content, as `placement_offset_x`.
464 pub placement_offset_y: f32,
465 /// Explicit alignment lines. Unspecified lines are inherited from wrapped content.
466 pub alignment_lines: crate::AlignmentLines,
467}
468
469impl LayoutModifierMeasureResult {
470 /// A modifier of `size` that places its content at the offset.
471 pub fn new(size: Size, placement_offset_x: f32, placement_offset_y: f32) -> Self {
472 Self {
473 size,
474 placement_offset_x,
475 placement_offset_y,
476 alignment_lines: crate::AlignmentLines::default(),
477 }
478 }
479
480 /// Creates a result with zero placement offset (wrapped content placed at 0,0).
481 pub fn with_size(size: Size) -> Self {
482 Self {
483 size,
484 placement_offset_x: 0.0,
485 placement_offset_y: 0.0,
486 alignment_lines: crate::AlignmentLines::default(),
487 }
488 }
489
490 /// Overrides the wrapped content's alignment lines in this modifier's coordinates.
491 pub fn with_alignment_lines(mut self, alignment_lines: crate::AlignmentLines) -> Self {
492 self.alignment_lines = alignment_lines;
493 self
494 }
495}
496
497#[cfg(test)]
498#[path = "tests/core_tests.rs"]
499mod tests;