1use kurbo::Size;
9
10fn non_negative(size: Size) -> Size {
12 Size::new(size.width.max(0.0), size.height.max(0.0))
13}
14
15#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct BoxConstraints {
23 min: Size,
24 max: Size,
25}
26
27impl BoxConstraints {
28 pub fn new(min: Size, max: Size) -> Self {
38 let min = non_negative(min);
39 let max = non_negative(max);
40 #[cfg(debug_assertions)]
41 if max.width < min.width || max.height < min.height {
42 eprintln!(
43 "frust-core: BoxConstraints::new received inverted bounds \
44 (min {min:?} > max {max:?}); widening max to min — likely an \
45 upstream layout bug"
46 );
47 }
48 let max = Size::new(max.width.max(min.width), max.height.max(min.height));
49 Self { min, max }
50 }
51
52 pub fn tight(size: Size) -> Self {
54 let size = non_negative(size);
55 Self {
56 min: size,
57 max: size,
58 }
59 }
60
61 pub fn loose(max: Size) -> Self {
63 Self {
64 min: Size::ZERO,
65 max: non_negative(max),
66 }
67 }
68
69 pub fn min(&self) -> Size {
71 self.min
72 }
73
74 pub fn max(&self) -> Size {
76 self.max
77 }
78
79 pub fn constrain(&self, size: Size) -> Size {
81 Size::new(
82 size.width.clamp(self.min.width, self.max.width),
83 size.height.clamp(self.min.height, self.max.height),
84 )
85 }
86
87 pub fn loosen(&self) -> Self {
89 Self {
90 min: Size::ZERO,
91 max: self.max,
92 }
93 }
94
95 pub fn tighten(&self, size: Size) -> Self {
100 let size = self.constrain(size);
101 Self {
102 min: size,
103 max: size,
104 }
105 }
106
107 pub fn is_tight(&self) -> bool {
109 self.min == self.max
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn constrain_clamps_into_range() {
119 let bc = BoxConstraints::new(Size::new(10.0, 10.0), Size::new(100.0, 100.0));
120 assert_eq!(bc.constrain(Size::new(5.0, 5.0)), Size::new(10.0, 10.0));
122 assert_eq!(
124 bc.constrain(Size::new(200.0, 200.0)),
125 Size::new(100.0, 100.0)
126 );
127 assert_eq!(bc.constrain(Size::new(50.0, 40.0)), Size::new(50.0, 40.0));
129 }
130
131 #[test]
132 fn loose_and_loosen_zero_the_minimum() {
133 let bc = BoxConstraints::loose(Size::new(80.0, 60.0));
134 assert_eq!(bc.min(), Size::ZERO);
135 assert_eq!(bc.max(), Size::new(80.0, 60.0));
136
137 let tight = BoxConstraints::tight(Size::new(30.0, 30.0));
138 let loosened = tight.loosen();
139 assert_eq!(loosened.min(), Size::ZERO);
140 assert_eq!(loosened.max(), Size::new(30.0, 30.0));
141 }
142
143 #[test]
144 fn tight_forces_exact_size() {
145 let bc = BoxConstraints::tight(Size::new(42.0, 24.0));
146 assert!(bc.is_tight());
147 assert_eq!(bc.min(), bc.max());
148 assert_eq!(
149 bc.constrain(Size::new(1000.0, 1000.0)),
150 Size::new(42.0, 24.0)
151 );
152 }
153
154 #[test]
155 fn tighten_clamps_then_locks() {
156 let bc = BoxConstraints::new(Size::new(10.0, 10.0), Size::new(100.0, 100.0));
157 let t = bc.tighten(Size::new(500.0, 5.0));
159 assert!(t.is_tight());
160 assert_eq!(t.min(), Size::new(100.0, 10.0));
161 }
162
163 #[test]
164 fn constructors_reject_negative_sizes() {
165 let bc = BoxConstraints::loose(Size::new(-5.0, -5.0));
166 assert_eq!(bc.max(), Size::ZERO);
167 }
168
169 #[test]
170 fn new_widens_max_when_one_axis_is_inverted() {
171 let bc = BoxConstraints::new(Size::new(100.0, 10.0), Size::new(10.0, 100.0));
173 assert!(bc.min().width <= bc.max().width);
174 assert!(bc.min().height <= bc.max().height);
175 assert_eq!(bc.min(), Size::new(100.0, 10.0));
176 assert_eq!(bc.max(), Size::new(100.0, 100.0));
177
178 let constrained = bc.constrain(Size::new(0.0, 0.0));
180 assert_eq!(constrained, Size::new(100.0, 10.0));
181 }
182
183 #[test]
184 fn new_widens_max_when_both_axes_are_inverted() {
185 let bc = BoxConstraints::new(Size::new(100.0, 100.0), Size::new(10.0, 10.0));
186 assert!(bc.min().width <= bc.max().width);
187 assert!(bc.min().height <= bc.max().height);
188 assert_eq!(bc.min(), Size::new(100.0, 100.0));
189 assert_eq!(bc.max(), Size::new(100.0, 100.0));
190 assert!(bc.is_tight());
191
192 let constrained = bc.constrain(Size::new(1000.0, 1000.0));
194 assert_eq!(constrained, Size::new(100.0, 100.0));
195 }
196}