Skip to main content

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