ailloli_ui_core/geometry/
constraints.rs

1//! Parent-provided minimum and maximum layout sizes.
2
3use super::{EdgeInsets, Size};
4
5/// Min/max size bounds passed from parent to child during layout.
6///
7/// # Examples
8///
9/// ```
10/// use ailloli_ui_core::{Constraints, Size};
11/// assert_eq!(Constraints::loose(100.0, 80.0).constrain(Size::new(120.0, 20.0)), Size::new(100.0, 20.0));
12/// ```
13#[derive(Debug, Clone, Copy)]
14pub struct Constraints {
15    /// Minimum width in logical pixels.
16    pub min_w: f32,
17    /// Maximum width in logical pixels; infinity represents an unbounded axis.
18    pub max_w: f32,
19    /// Minimum height in logical pixels.
20    pub min_h: f32,
21    /// Maximum height in logical pixels; infinity represents an unbounded axis.
22    pub max_h: f32,
23}
24
25impl Constraints {
26    /// Creates fixed bounds on both axes (`min == max`).
27    ///
28    /// Values are stored verbatim; callers should provide non-negative finite
29    /// dimensions for an ordinary tight layout constraint.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use ailloli_ui_core::Constraints;
35    /// let c = Constraints::tight(40.0, 20.0);
36    /// assert_eq!((c.min_w, c.max_w), (40.0, 40.0));
37    /// ```
38    pub fn tight(w: f32, h: f32) -> Self {
39        Self {
40            min_w: w,
41            max_w: w,
42            min_h: h,
43            max_h: h,
44        }
45    }
46
47    /// Creates zero minimums and caller-supplied maximums.
48    ///
49    /// Despite the name, an axis is truly unbounded only when its maximum is
50    /// [`f32::INFINITY`].
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use ailloli_ui_core::Constraints;
56    /// let c = Constraints::loose(40.0, 20.0);
57    /// assert_eq!((c.min_w, c.max_w), (0.0, 40.0));
58    /// ```
59    pub fn loose(max_w: f32, max_h: f32) -> Self {
60        Self {
61            min_w: 0.0,
62            max_w,
63            min_h: 0.0,
64            max_h,
65        }
66    }
67
68    /// Returns bounds with each axis ordered as `min <= max`.
69    ///
70    /// Infinities participate in the ordering. If exactly one bound on an axis
71    /// is NaN, floating-point `min`/`max` replace both results with the other
72    /// bound; two NaN bounds remain NaN.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use ailloli_ui_core::Constraints;
78    /// let c = Constraints { min_w: 20.0, max_w: 10.0, min_h: 0.0, max_h: 30.0 }.normalized();
79    /// assert_eq!((c.min_w, c.max_w), (10.0, 20.0));
80    /// ```
81    pub fn normalized(self) -> Self {
82        Self {
83            min_w: self.min_w.min(self.max_w),
84            max_w: self.max_w.max(self.min_w),
85            min_h: self.min_h.min(self.max_h),
86            max_h: self.max_h.max(self.min_h),
87        }
88    }
89
90    /// Clamps `size` to this normalized constraint box.
91    ///
92    /// # Panics
93    ///
94    /// Panics if either normalized interval contains NaN, because
95    /// [`f32::clamp`] requires ordered non-NaN bounds.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use ailloli_ui_core::{Constraints, Size};
101    /// assert_eq!(Constraints::loose(10.0, 20.0).constrain(Size::new(40.0, 5.0)), Size::new(10.0, 5.0));
102    /// ```
103    pub fn constrain(&self, size: Size) -> Size {
104        let c = self.normalized();
105        Size {
106            w: size.w.clamp(c.min_w, c.max_w),
107            h: size.h.clamp(c.min_h, c.max_h),
108        }
109    }
110
111    /// Returns the stored maximum dimensions without normalizing them.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use ailloli_ui_core::{Constraints, Size};
117    /// assert_eq!(Constraints::loose(10.0, 20.0).max_size(), Size::new(10.0, 20.0));
118    /// ```
119    pub fn max_size(&self) -> Size {
120        Size::new(self.max_w, self.max_h)
121    }
122
123    /// Returns the stored maximums with both minimums reset to zero.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use ailloli_ui_core::Constraints;
129    /// assert_eq!(Constraints::tight(10.0, 20.0).loosen().min_w, 0.0);
130    /// ```
131    pub fn loosen(&self) -> Self {
132        Self::loose(self.max_w, self.max_h)
133    }
134
135    /// Narrows constraints to exactly `size` after clamping it to current bounds.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use ailloli_ui_core::{Constraints, Size};
141    /// let c = Constraints::loose(10.0, 20.0).tighten(Size::new(40.0, 5.0));
142    /// assert_eq!((c.min_w, c.max_w), (10.0, 10.0));
143    /// ```
144    pub fn tighten(&self, size: Size) -> Self {
145        let s = self.constrain(size);
146        Self::tight(s.w, s.h)
147    }
148
149    /// Subtracts horizontal and vertical insets from every matching bound.
150    ///
151    /// Results are floored at zero. Negative insets expand rather than shrink
152    /// the bounds. A NaN or negative-infinite subtraction result becomes zero,
153    /// while positive infinity remains infinite.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use ailloli_ui_core::{Constraints, EdgeInsets};
159    /// let c = Constraints::tight(100.0, 80.0).deflate(EdgeInsets::all(10.0));
160    /// assert_eq!((c.max_w, c.max_h), (80.0, 60.0));
161    /// ```
162    pub fn deflate(&self, by: EdgeInsets) -> Self {
163        Self {
164            min_w: (self.min_w - by.horizontal()).max(0.0),
165            max_w: (self.max_w - by.horizontal()).max(0.0),
166            min_h: (self.min_h - by.vertical()).max(0.0),
167            max_h: (self.max_h - by.vertical()).max(0.0),
168        }
169    }
170}