Skip to main content

sequoia_layout/
box_model.rs

1//! Box-model helpers for padding, margins, insets, and aspect ratios.
2
3use sequoia_core::{Edges, Rect, Size};
4
5/// Axes affected when shrinking a box by its padding or margin.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum ShrinkAxis {
9    /// Shrink both axes.
10    Both,
11    /// Shrink only the horizontal axis.
12    Horizontal,
13    /// Shrink only the vertical axis.
14    Vertical,
15}
16
17/// Returns the content size after subtracting `padding` from both axes.
18///
19/// The result never drops below zero.
20pub fn shrink_by_padding(size: Size, padding: Edges) -> Size {
21    Size::new(
22        (size.width - padding.horizontal_sum()).max(0.0),
23        (size.height - padding.vertical_sum()).max(0.0),
24    )
25}
26
27/// Returns the content size after subtracting `margin` from both axes.
28///
29/// The result never drops below zero.
30pub fn shrink_by_margin(size: Size, margin: Edges) -> Size {
31    Size::new(
32        (size.width - margin.horizontal_sum()).max(0.0),
33        (size.height - margin.vertical_sum()).max(0.0),
34    )
35}
36
37/// Insets a rectangle by `padding`, clamping the resulting size to zero.
38pub fn inset_rect(rect: Rect, padding: Edges) -> Rect {
39    Rect::from_xywh(
40        rect.left() + padding.left,
41        rect.top() + padding.top,
42        (rect.size.width - padding.horizontal_sum()).max(0.0),
43        (rect.size.height - padding.vertical_sum()).max(0.0),
44    )
45}
46
47/// Derives the auto-sized axis from the other axis using `ratio`.
48///
49/// `w_auto` and `h_auto` indicate which dimensions were left to the layout.
50/// When both axes are auto, the ratio is only applied if exactly one of them
51/// already has a non-zero size. Invalid ratios leave the size unchanged.
52pub fn apply_aspect_ratio(
53    w: f32,
54    h: f32,
55    w_auto: bool,
56    h_auto: bool,
57    ratio: Option<f32>,
58) -> (f32, f32) {
59    let Some(r) = ratio else { return (w, h) };
60    if !r.is_finite() || r <= 0.0 {
61        return (w, h);
62    }
63    match (w_auto, h_auto) {
64        (true, false) => ((h * r).max(0.0), h),
65        (false, true) => (w, if r != 0.0 { (w / r).max(0.0) } else { h }),
66        (true, true) => {
67            if w <= 0.0 && h > 0.0 {
68                ((h * r).max(0.0), h)
69            } else if h <= 0.0 && w > 0.0 {
70                (w, (w / r).max(0.0))
71            } else {
72                (w, h)
73            }
74        }
75        (false, false) => (w, h),
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn padding_shrinks_content_box() {
85        let s = shrink_by_padding(Size::new(100.0, 60.0), Edges::xy(10.0, 5.0));
86        assert_eq!(s, Size::new(80.0, 50.0));
87        let s = shrink_by_padding(Size::new(5.0, 5.0), Edges::all(10.0));
88        assert_eq!(s, Size::ZERO);
89    }
90
91    #[test]
92    fn aspect_derives_auto_axis() {
93        assert_eq!(
94            apply_aspect_ratio(0.0, 50.0, true, false, Some(2.0)),
95            (100.0, 50.0)
96        );
97        assert_eq!(
98            apply_aspect_ratio(100.0, 0.0, false, true, Some(2.0)),
99            (100.0, 50.0)
100        );
101        assert_eq!(
102            apply_aspect_ratio(100.0, 50.0, false, false, Some(2.0)),
103            (100.0, 50.0)
104        );
105        assert_eq!(
106            apply_aspect_ratio(10.0, 10.0, true, true, Some(2.0)),
107            (10.0, 10.0)
108        );
109    }
110}