Skip to main content

ply_engine/
layout.rs

1use crate::align::{AlignX, AlignY};
2use crate::engine;
3
4/// Per-corner border radius for rounded rectangles.
5#[derive(Debug, Clone, Copy, Default)]
6pub struct CornerRadius {
7    pub top_left: f32,
8    pub top_right: f32,
9    pub bottom_left: f32,
10    pub bottom_right: f32,
11}
12
13impl CornerRadius {
14    /// Returns `true` when all four corners have a radius of zero.
15    pub fn is_zero(&self) -> bool {
16        self.top_left == 0.0
17            && self.top_right == 0.0
18            && self.bottom_left == 0.0
19            && self.bottom_right == 0.0
20    }
21}
22
23impl From<f32> for CornerRadius {
24    /// Creates a corner radius with the same value for all corners.
25    fn from(value: f32) -> Self {
26        Self {
27            top_left: value,
28            top_right: value,
29            bottom_left: value,
30            bottom_right: value,
31        }
32    }
33}
34
35impl From<(f32, f32, f32, f32)> for CornerRadius {
36    /// Creates corner radii from a tuple in CSS order: (top-left, top-right, bottom-right, bottom-left).
37    fn from((tl, tr, br, bl): (f32, f32, f32, f32)) -> Self {
38        Self {
39            top_left: tl,
40            top_right: tr,
41            bottom_left: bl,
42            bottom_right: br,
43        }
44    }
45}
46
47/// Defines different sizing behaviors for an element.
48#[derive(Debug, Clone, Copy)]
49#[repr(u8)]
50pub enum SizingType {
51    /// The element's size is determined by its content and constrained by min/max values.
52    Fit,
53    /// The element expands to fill available space within min/max constraints.
54    Grow,
55    /// The element's size is fixed to a percentage of its parent.
56    Percent,
57    /// The element's size is set to a fixed value.
58    Fixed,
59}
60
61/// Represents different sizing strategies for layout elements.
62#[derive(Debug, Clone, Copy)]
63pub enum Sizing {
64    /// Fits the element’s width/height within a min and max constraint.
65    Fit(f32, f32),
66    /// Expands the element to fill available space within min/max constraints.
67    ///
68    /// The third argument is the grow weight. A weight of `1.0` is the default behavior.
69    /// A weight of `0.0` means the element does not grow (behaves like `Fit` in practice).
70    Grow(f32, f32, f32),
71    /// Sets a fixed width/height.
72    Fixed(f32),
73    /// Sets width/height as a percentage of its parent. Value should be between `0.0` and `1.0`.
74    Percent(f32),
75}
76
77/// Converts a `Sizing` value into an engine `SizingAxis`.
78impl From<Sizing> for engine::SizingAxis {
79    fn from(value: Sizing) -> Self {
80        match value {
81            Sizing::Fit(min, max) => Self {
82                type_: engine::SizingType::Fit,
83                min_max: engine::SizingMinMax { min, max },
84                percent: 0.0,
85                grow_weight: 1.0,
86            },
87            Sizing::Grow(min, max, weight) => {
88                assert!(weight >= 0.0, "Grow weight must be non-negative.");
89
90                if weight == 0.0 {
91                    Self {
92                        type_: engine::SizingType::Fit,
93                        min_max: engine::SizingMinMax { min, max },
94                        percent: 0.0,
95                        grow_weight: 1.0,
96                    }
97                } else {
98                    Self {
99                        type_: engine::SizingType::Grow,
100                        min_max: engine::SizingMinMax { min, max },
101                        percent: 0.0,
102                        grow_weight: weight,
103                    }
104                }
105            }
106            Sizing::Fixed(size) => Self {
107                type_: engine::SizingType::Fixed,
108                min_max: engine::SizingMinMax {
109                    min: size,
110                    max: size,
111                },
112                percent: 0.0,
113                grow_weight: 1.0,
114            },
115            Sizing::Percent(percent) => Self {
116                type_: engine::SizingType::Percent,
117                min_max: engine::SizingMinMax { min: 0.0, max: 0.0 },
118                percent,
119                grow_weight: 1.0,
120            },
121        }
122    }
123}
124
125/// Represents padding values for each side of an element.
126#[derive(Debug, Default)]
127pub struct Padding {
128    /// Padding on the left side.
129    pub left: u16,
130    /// Padding on the right side.
131    pub right: u16,
132    /// Padding on the top side.
133    pub top: u16,
134    /// Padding on the bottom side.
135    pub bottom: u16,
136}
137
138impl Padding {
139    /// Creates a new `Padding` with individual values for each side.
140    pub fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
141        Self {
142            left,
143            right,
144            top,
145            bottom,
146        }
147    }
148
149    /// Sets the same padding value for all sides.
150    pub fn all(value: u16) -> Self {
151        Self::new(value, value, value, value)
152    }
153
154    /// Sets the same padding for left and right sides.
155    /// Top and bottom are set to `0`.
156    pub fn horizontal(value: u16) -> Self {
157        Self::new(value, value, 0, 0)
158    }
159
160    /// Sets the same padding for top and bottom sides.
161    /// Left and right are set to `0`.
162    pub fn vertical(value: u16) -> Self {
163        Self::new(0, 0, value, value)
164    }
165}
166
167impl From<u16> for Padding {
168    /// Creates padding with the same value for all sides.
169    fn from(value: u16) -> Self {
170        Self::all(value)
171    }
172}
173
174impl From<(u16, u16, u16, u16)> for Padding {
175    /// Creates padding from a tuple in CSS order: (top, right, bottom, left).
176    fn from((top, right, bottom, left): (u16, u16, u16, u16)) -> Self {
177        Self { left, right, top, bottom }
178    }
179}
180
181/// Defines the layout direction for arranging child elements.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
183#[repr(u8)]
184pub enum LayoutDirection {
185    /// Arranges elements from left to right.
186    #[default]
187    LeftToRight,
188    /// Arranges elements from top to bottom.
189    TopToBottom,
190}
191
192/// Builder for configuring layout properties using a closure.
193/// No lifetime parameters — works cleanly with closures.
194pub struct LayoutBuilder {
195    pub(crate) config: engine::LayoutConfig,
196}
197
198impl LayoutBuilder {
199    /// Sets the spacing between child elements.
200    #[inline]
201    pub fn gap(&mut self, gap: u16) -> &mut Self {
202        self.config.child_gap = gap;
203        self
204    }
205
206    /// Sets the alignment of child elements using separate X and Y values.
207    #[inline]
208    pub fn align(&mut self, x: AlignX, y: AlignY) -> &mut Self {
209        self.config.child_alignment.x = x;
210        self.config.child_alignment.y = y;
211        self
212    }
213
214    /// Sets the layout direction.
215    #[inline]
216    pub fn direction(&mut self, direction: LayoutDirection) -> &mut Self {
217        self.config.layout_direction = direction;
218        self
219    }
220
221    /// Enables child wrapping to the cross axis when children exceed available space on the main axis.
222    #[inline]
223    pub fn wrap(&mut self) -> &mut Self {
224        self.config.wrap = true;
225        self
226    }
227
228    /// Sets spacing between wrapped lines (or columns, depending on direction).
229    #[inline]
230    pub fn wrap_gap(&mut self, gap: u16) -> &mut Self {
231        self.config.wrap_gap = gap;
232        self
233    }
234
235    /// Sets padding values for the layout.
236    #[inline]
237    pub fn padding(&mut self, padding: impl Into<Padding>) -> &mut Self {
238        let padding = padding.into();
239        self.config.padding.left = padding.left;
240        self.config.padding.right = padding.right;
241        self.config.padding.top = padding.top;
242        self.config.padding.bottom = padding.bottom;
243        self
244    }
245}
246
247/// Shorthand macro for [`Sizing::Fit`]. Defaults max to `f32::MAX` if omitted.
248#[macro_export]
249macro_rules! fit {
250    ($min:expr, $max:expr) => {
251        $crate::layout::Sizing::Fit($min, $max)
252    };
253    ($min:expr) => {
254        fit!($min, f32::MAX)
255    };
256    () => {
257        fit!(0.0)
258    };
259
260    ($($name:ident : $value:expr),+ $(,)?) => {
261        $crate::fit!(@named (0.0, f32::MAX); $($name : $value,)+)
262    };
263
264    (@named ($min:expr, $max:expr); ) => {
265        $crate::layout::Sizing::Fit($min, $max)
266    };
267    (@named ($min:expr, $max:expr); min : $value:expr, $($rest:tt)*) => {
268        $crate::fit!(@named ($value, $max); $($rest)*)
269    };
270    (@named ($min:expr, $max:expr); max : $value:expr, $($rest:tt)*) => {
271        $crate::fit!(@named ($min, $value); $($rest)*)
272    };
273    (@named ($min:expr, $max:expr); $unknown:ident : $value:expr, $($rest:tt)*) => {
274        compile_error!("Unknown named argument for fit!(). Expected: min, max.");
275    };
276
277    ($first:expr, $($rest:tt)+) => {
278        compile_error!("Do not mix positional and named arguments in fit!().");
279    };
280}
281
282/// Shorthand macro for [`Sizing::Grow`]. Defaults max to `f32::MAX` and weight to `1.0` if omitted.
283#[macro_export]
284macro_rules! grow {
285    ($min:expr, $max:expr, $weight:expr) => {
286        $crate::layout::Sizing::Grow($min, $max, $weight)
287    };
288    ($min:expr, $max:expr) => {
289        grow!($min, $max, 1.0)
290    };
291    ($min:expr) => {
292        grow!($min, f32::MAX)
293    };
294    () => {
295        grow!(0.0)
296    };
297
298    ($($name:ident : $value:expr),+ $(,)?) => {
299        $crate::grow!(@named (0.0, f32::MAX, 1.0); $($name : $value,)+)
300    };
301
302    (@named ($min:expr, $max:expr, $weight:expr); ) => {
303        $crate::layout::Sizing::Grow($min, $max, $weight)
304    };
305    (@named ($min:expr, $max:expr, $weight:expr); min : $value:expr, $($rest:tt)*) => {
306        $crate::grow!(@named ($value, $max, $weight); $($rest)*)
307    };
308    (@named ($min:expr, $max:expr, $weight:expr); max : $value:expr, $($rest:tt)*) => {
309        $crate::grow!(@named ($min, $value, $weight); $($rest)*)
310    };
311    (@named ($min:expr, $max:expr, $weight:expr); weight : $value:expr, $($rest:tt)*) => {
312        $crate::grow!(@named ($min, $max, $value); $($rest)*)
313    };
314    (@named ($min:expr, $max:expr, $weight:expr); $unknown:ident : $value:expr, $($rest:tt)*) => {
315        compile_error!("Unknown named argument for grow!(). Expected: min, max, weight.");
316    };
317}
318
319/// Shorthand macro for [`Sizing::Fixed`].
320#[macro_export]
321macro_rules! fixed {
322    ($val:expr) => {
323        $crate::layout::Sizing::Fixed($val)
324    };
325}
326
327/// Shorthand macro for [`Sizing::Percent`].
328/// The value has to be in range `0.0..=1.0`.
329#[macro_export]
330macro_rules! percent {
331    ($percent:expr) => {{
332        const _: () = assert!(
333            $percent >= 0.0 && $percent <= 1.0,
334            "Percent value must be between 0.0 and 1.0 inclusive!"
335        );
336        $crate::layout::Sizing::Percent($percent)
337    }};
338}
339
340#[cfg(test)]
341mod test {
342    use super::*;
343
344    #[test]
345    fn fit_macro() {
346        let both_args = fit!(12.0, 34.0);
347        assert!(matches!(both_args, Sizing::Fit(12.0, 34.0)));
348
349        let one_arg = fit!(12.0);
350        assert!(matches!(one_arg, Sizing::Fit(12.0, f32::MAX)));
351
352        let zero_args = fit!();
353        assert!(matches!(zero_args, Sizing::Fit(0.0, f32::MAX)));
354
355        let named_max = fit!(max: 34.0);
356        assert!(matches!(named_max, Sizing::Fit(0.0, 34.0)));
357
358        let named_min = fit!(min: 12.0);
359        assert!(matches!(named_min, Sizing::Fit(12.0, f32::MAX)));
360
361        let named_both = fit!(max: 34.0, min: 12.0);
362        assert!(matches!(named_both, Sizing::Fit(12.0, 34.0)));
363    }
364
365    #[test]
366    fn grow_macro() {
367        let three_args = grow!(12.0, 34.0, 2.5);
368        assert!(matches!(three_args, Sizing::Grow(12.0, 34.0, 2.5)));
369
370        let both_args = grow!(12.0, 34.0);
371        assert!(matches!(both_args, Sizing::Grow(12.0, 34.0, 1.0)));
372
373        let one_arg = grow!(12.0);
374        assert!(matches!(one_arg, Sizing::Grow(12.0, f32::MAX, 1.0)));
375
376        let zero_args = grow!();
377        assert!(matches!(zero_args, Sizing::Grow(0.0, f32::MAX, 1.0)));
378
379        let named_weight = grow!(weight: 2.0);
380        assert!(matches!(named_weight, Sizing::Grow(0.0, f32::MAX, 2.0)));
381
382        let named_min_weight = grow!(min: 12.0, weight: 2.0);
383        assert!(matches!(named_min_weight, Sizing::Grow(12.0, f32::MAX, 2.0)));
384
385        let named_max_weight = grow!(max: 34.0, weight: 3.0);
386        assert!(matches!(named_max_weight, Sizing::Grow(0.0, 34.0, 3.0)));
387
388        let named_all = grow!(weight: 2.0, max: 34.0, min: 12.0);
389        assert!(matches!(named_all, Sizing::Grow(12.0, 34.0, 2.0)));
390    }
391
392    #[test]
393    fn zero_weight_grow_converts_to_fit_axis() {
394        let axis: engine::SizingAxis = grow!(12.0, 34.0, 0.0).into();
395        assert_eq!(axis.type_, engine::SizingType::Fit);
396        assert_eq!(axis.min_max.min, 12.0);
397        assert_eq!(axis.min_max.max, 34.0);
398    }
399
400    #[test]
401    #[should_panic(expected = "Grow weight must be non-negative.")]
402    fn negative_grow_weight_panics() {
403        let _axis: engine::SizingAxis = grow!(0.0, f32::MAX, -1.0).into();
404    }
405
406    #[test]
407    fn fixed_macro() {
408        let value = fixed!(123.0);
409        assert!(matches!(value, Sizing::Fixed(123.0)));
410    }
411
412    #[test]
413    fn percent_macro() {
414        let value = percent!(0.5);
415        assert!(matches!(value, Sizing::Percent(0.5)));
416    }
417}