Skip to main content

cranpose_ui_layout/
constraints.rs

1//! Layout constraints system
2
3/// Constraints used during layout measurement.
4#[derive(Clone, Copy, Debug, PartialEq)]
5pub struct Constraints {
6    pub min_width: f32,
7    pub max_width: f32,
8    pub min_height: f32,
9    pub max_height: f32,
10}
11
12impl Constraints {
13    /// Creates constraints with exact width and height.
14    pub fn tight(width: f32, height: f32) -> Self {
15        Self {
16            min_width: width,
17            max_width: width,
18            min_height: height,
19            max_height: height,
20        }
21    }
22
23    /// Creates constraints with loose bounds (min = 0, max = given values).
24    pub fn loose(max_width: f32, max_height: f32) -> Self {
25        Self {
26            min_width: 0.0,
27            max_width,
28            min_height: 0.0,
29            max_height,
30        }
31    }
32
33    /// Returns true if these constraints have a single size that satisfies them.
34    pub fn is_tight(&self) -> bool {
35        self.min_width == self.max_width && self.min_height == self.max_height
36    }
37
38    /// Returns true if all bounds are finite.
39    pub fn is_bounded(&self) -> bool {
40        self.max_width.is_finite() && self.max_height.is_finite()
41    }
42
43    /// Constrains the provided width and height to fit within these constraints.
44    pub fn constrain(&self, width: f32, height: f32) -> (f32, f32) {
45        (
46            width.clamp(self.min_width, self.max_width),
47            height.clamp(self.min_height, self.max_height),
48        )
49    }
50
51    /// Returns true if the width is bounded (max_width is finite).
52    #[inline]
53    pub fn has_bounded_width(&self) -> bool {
54        self.max_width.is_finite()
55    }
56
57    /// Returns true if the height is bounded (max_height is finite).
58    #[inline]
59    pub fn has_bounded_height(&self) -> bool {
60        self.max_height.is_finite()
61    }
62
63    /// Returns true if both width and height are tight (min == max for both).
64    #[inline]
65    pub fn has_tight_width(&self) -> bool {
66        self.min_width == self.max_width
67    }
68
69    /// Returns true if the height is tight (min == max).
70    #[inline]
71    pub fn has_tight_height(&self) -> bool {
72        self.min_height == self.max_height
73    }
74
75    /// Creates new constraints with tightened width (min = max = given width).
76    pub fn tighten_width(self, width: f32) -> Self {
77        Self {
78            min_width: width,
79            max_width: width,
80            ..self
81        }
82    }
83
84    /// Creates new constraints with tightened height (min = max = given height).
85    pub fn tighten_height(self, height: f32) -> Self {
86        Self {
87            min_height: height,
88            max_height: height,
89            ..self
90        }
91    }
92
93    /// Creates new constraints with the given width bounds.
94    pub fn copy_with_width(self, min_width: f32, max_width: f32) -> Self {
95        Self {
96            min_width,
97            max_width,
98            ..self
99        }
100    }
101
102    /// Creates new constraints with the given height bounds.
103    pub fn copy_with_height(self, min_height: f32, max_height: f32) -> Self {
104        Self {
105            min_height,
106            max_height,
107            ..self
108        }
109    }
110
111    /// Deflates constraints by the given amount on all sides.
112    /// This is useful for applying padding before measuring children.
113    pub fn deflate(self, horizontal: f32, vertical: f32) -> Self {
114        Self {
115            min_width: (self.min_width - horizontal).max(0.0),
116            max_width: (self.max_width - horizontal).max(0.0),
117            min_height: (self.min_height - vertical).max(0.0),
118            max_height: (self.max_height - vertical).max(0.0),
119        }
120    }
121
122    /// Creates new constraints with loosened minimums (min = 0).
123    pub fn loosen(self) -> Self {
124        Self {
125            min_width: 0.0,
126            min_height: 0.0,
127            ..self
128        }
129    }
130
131    /// Creates constraints that enforce the given size.
132    pub fn enforce(self, width: f32, height: f32) -> Self {
133        Self {
134            min_width: width.clamp(self.min_width, self.max_width),
135            max_width: width.clamp(self.min_width, self.max_width),
136            min_height: height.clamp(self.min_height, self.max_height),
137            max_height: height.clamp(self.min_height, self.max_height),
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    fn bounds() -> Constraints {
147        Constraints {
148            min_width: 20.0,
149            max_width: 200.0,
150            min_height: 10.0,
151            max_height: 100.0,
152        }
153    }
154
155    #[test]
156    fn a_constraint_reports_which_of_its_axes_are_bounded() {
157        assert!(bounds().has_bounded_width());
158        assert!(bounds().has_bounded_height());
159
160        // A scrolling parent measures its child with no limit along the scroll
161        // axis, which is what an infinite maximum means.
162        let scrolling = bounds().copy_with_height(0.0, f32::INFINITY);
163        assert!(scrolling.has_bounded_width());
164        assert!(!scrolling.has_bounded_height());
165        assert!(!scrolling.is_bounded());
166    }
167
168    #[test]
169    fn a_constraint_reports_which_of_its_axes_are_tight() {
170        let loose = Constraints::loose(200.0, 100.0);
171        assert!(!loose.has_tight_width());
172        assert!(!loose.has_tight_height());
173        assert!(!loose.is_tight());
174
175        let tight = Constraints::tight(200.0, 100.0);
176        assert!(tight.has_tight_width());
177        assert!(tight.has_tight_height());
178        assert!(tight.is_tight());
179
180        // One axis at a time: a row gives its children a tight height and lets
181        // them choose their width.
182        let row_child = loose.tighten_height(100.0);
183        assert!(!row_child.has_tight_width());
184        assert!(row_child.has_tight_height());
185        assert!(!row_child.is_tight());
186    }
187
188    #[test]
189    fn tightening_one_axis_leaves_the_other_alone() {
190        let tightened = bounds().tighten_width(64.0);
191        assert_eq!(tightened.min_width, 64.0);
192        assert_eq!(tightened.max_width, 64.0);
193        assert_eq!(tightened.min_height, 10.0);
194        assert_eq!(tightened.max_height, 100.0);
195
196        let tightened = bounds().tighten_height(64.0);
197        assert_eq!(tightened.min_height, 64.0);
198        assert_eq!(tightened.max_height, 64.0);
199        assert_eq!(tightened.min_width, 20.0);
200        assert_eq!(tightened.max_width, 200.0);
201    }
202
203    #[test]
204    fn replacing_one_axis_leaves_the_other_alone() {
205        let replaced = bounds().copy_with_width(0.0, 50.0);
206        assert_eq!((replaced.min_width, replaced.max_width), (0.0, 50.0));
207        assert_eq!((replaced.min_height, replaced.max_height), (10.0, 100.0));
208
209        let replaced = bounds().copy_with_height(5.0, 50.0);
210        assert_eq!((replaced.min_height, replaced.max_height), (5.0, 50.0));
211        assert_eq!((replaced.min_width, replaced.max_width), (20.0, 200.0));
212    }
213
214    #[test]
215    fn deflating_takes_padding_off_both_ends_and_stops_at_zero() {
216        let inner = bounds().deflate(16.0, 8.0);
217        assert_eq!(inner.min_width, 4.0);
218        assert_eq!(inner.max_width, 184.0);
219        assert_eq!(inner.min_height, 2.0);
220        assert_eq!(inner.max_height, 92.0);
221
222        // Padding wider than the space it is in leaves nothing, not a negative
223        // width that would measure a child at an impossible size.
224        let squeezed = Constraints::tight(10.0, 10.0).deflate(40.0, 40.0);
225        assert_eq!(squeezed.min_width, 0.0);
226        assert_eq!(squeezed.max_width, 0.0);
227        assert_eq!(squeezed.min_height, 0.0);
228        assert_eq!(squeezed.max_height, 0.0);
229    }
230
231    #[test]
232    fn loosening_drops_the_minimums_and_keeps_the_maximums() {
233        // What a parent does when it will place a child that measured smaller
234        // than the space rather than stretching it.
235        let loosened = Constraints::tight(200.0, 100.0).loosen();
236        assert_eq!(loosened.min_width, 0.0);
237        assert_eq!(loosened.min_height, 0.0);
238        assert_eq!(loosened.max_width, 200.0);
239        assert_eq!(loosened.max_height, 100.0);
240        assert!(!loosened.is_tight());
241    }
242}