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}