1use kurbo::Size;
5
6fn non_negative(size: Size) -> Size {
8 Size::new(size.width.max(0.0), size.height.max(0.0))
9}
10
11#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct BoxConstraints {
15 min: Size,
16 max: Size,
17}
18
19impl BoxConstraints {
20 pub fn new(min: Size, max: Size) -> Self {
23 let min = non_negative(min);
24 let max = non_negative(max);
25 #[cfg(debug_assertions)]
26 if max.width < min.width || max.height < min.height {
27 eprintln!(
28 "frust-core: BoxConstraints::new received inverted bounds \
29 (min {min:?} > max {max:?}); widening max to min — likely an \
30 upstream layout bug"
31 );
32 }
33 let max = Size::new(max.width.max(min.width), max.height.max(min.height));
34 Self { min, max }
35 }
36
37 pub fn tight(size: Size) -> Self {
39 let size = non_negative(size);
40 Self {
41 min: size,
42 max: size,
43 }
44 }
45
46 pub fn loose(max: Size) -> Self {
48 Self {
49 min: Size::ZERO,
50 max: non_negative(max),
51 }
52 }
53
54 pub fn min(&self) -> Size {
56 self.min
57 }
58
59 pub fn max(&self) -> Size {
61 self.max
62 }
63
64 pub fn constrain(&self, size: Size) -> Size {
66 Size::new(
67 size.width.clamp(self.min.width, self.max.width),
68 size.height.clamp(self.min.height, self.max.height),
69 )
70 }
71
72 pub fn loosen(&self) -> Self {
74 Self {
75 min: Size::ZERO,
76 max: self.max,
77 }
78 }
79
80 pub fn tighten(&self, size: Size) -> Self {
82 let size = self.constrain(size);
83 Self {
84 min: size,
85 max: size,
86 }
87 }
88
89 pub fn is_tight(&self) -> bool {
91 self.min == self.max
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn constrain_clamps_into_range() {
101 let bc = BoxConstraints::new(Size::new(10.0, 10.0), Size::new(100.0, 100.0));
102 assert_eq!(bc.constrain(Size::new(5.0, 5.0)), Size::new(10.0, 10.0));
104 assert_eq!(
106 bc.constrain(Size::new(200.0, 200.0)),
107 Size::new(100.0, 100.0)
108 );
109 assert_eq!(bc.constrain(Size::new(50.0, 40.0)), Size::new(50.0, 40.0));
111 }
112
113 #[test]
114 fn loose_and_loosen_zero_the_minimum() {
115 let bc = BoxConstraints::loose(Size::new(80.0, 60.0));
116 assert_eq!(bc.min(), Size::ZERO);
117 assert_eq!(bc.max(), Size::new(80.0, 60.0));
118
119 let tight = BoxConstraints::tight(Size::new(30.0, 30.0));
120 let loosened = tight.loosen();
121 assert_eq!(loosened.min(), Size::ZERO);
122 assert_eq!(loosened.max(), Size::new(30.0, 30.0));
123 }
124
125 #[test]
126 fn tight_forces_exact_size() {
127 let bc = BoxConstraints::tight(Size::new(42.0, 24.0));
128 assert!(bc.is_tight());
129 assert_eq!(bc.min(), bc.max());
130 assert_eq!(
131 bc.constrain(Size::new(1000.0, 1000.0)),
132 Size::new(42.0, 24.0)
133 );
134 }
135
136 #[test]
137 fn tighten_clamps_then_locks() {
138 let bc = BoxConstraints::new(Size::new(10.0, 10.0), Size::new(100.0, 100.0));
139 let t = bc.tighten(Size::new(500.0, 5.0));
141 assert!(t.is_tight());
142 assert_eq!(t.min(), Size::new(100.0, 10.0));
143 }
144
145 #[test]
146 fn constructors_reject_negative_sizes() {
147 let bc = BoxConstraints::loose(Size::new(-5.0, -5.0));
148 assert_eq!(bc.max(), Size::ZERO);
149 }
150
151 #[test]
152 fn new_widens_max_when_one_axis_is_inverted() {
153 let bc = BoxConstraints::new(Size::new(100.0, 10.0), Size::new(10.0, 100.0));
155 assert!(bc.min().width <= bc.max().width);
156 assert!(bc.min().height <= bc.max().height);
157 assert_eq!(bc.min(), Size::new(100.0, 10.0));
158 assert_eq!(bc.max(), Size::new(100.0, 100.0));
159
160 let constrained = bc.constrain(Size::new(0.0, 0.0));
162 assert_eq!(constrained, Size::new(100.0, 10.0));
163 }
164
165 #[test]
166 fn new_widens_max_when_both_axes_are_inverted() {
167 let bc = BoxConstraints::new(Size::new(100.0, 100.0), Size::new(10.0, 10.0));
168 assert!(bc.min().width <= bc.max().width);
169 assert!(bc.min().height <= bc.max().height);
170 assert_eq!(bc.min(), Size::new(100.0, 100.0));
171 assert_eq!(bc.max(), Size::new(100.0, 100.0));
172 assert!(bc.is_tight());
173
174 let constrained = bc.constrain(Size::new(1000.0, 1000.0));
176 assert_eq!(constrained, Size::new(100.0, 100.0));
177 }
178}