1#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4pub struct Rect {
5 pub top: u16,
6 pub left: u16,
7 pub width: u16,
8 pub height: u16,
9}
10
11impl Rect {
12 pub fn new(top: u16, left: u16, width: u16, height: u16) -> Self {
13 Self {
14 top,
15 left,
16 width,
17 height,
18 }
19 }
20
21 pub fn bottom(&self) -> u16 {
22 self.top.saturating_add(self.height)
23 }
24
25 pub fn right(&self) -> u16 {
26 self.left.saturating_add(self.width)
27 }
28
29 pub fn contains(&self, row: u16, col: u16) -> bool {
30 row >= self.top && row < self.bottom() && col >= self.left && col < self.right()
31 }
32
33 pub fn contains_xy(&self, x: u16, y: u16) -> bool {
34 self.contains(y, x)
35 }
36
37 pub fn area(&self) -> u32 {
38 self.width as u32 * self.height as u32
39 }
40
41 pub fn is_empty(&self) -> bool {
42 self.width == 0 || self.height == 0
43 }
44
45 pub fn inset(self, amount: u16) -> Rect {
46 self.inset_by(Insets::uniform(amount))
47 }
48
49 pub fn inset_by(self, insets: Insets) -> Rect {
50 let dx = insets.left.saturating_add(insets.right);
51 let dy = insets.top.saturating_add(insets.bottom);
52 Rect::new(
53 self.top.saturating_add(insets.top),
54 self.left.saturating_add(insets.left),
55 self.width.saturating_sub(dx),
56 self.height.saturating_sub(dy),
57 )
58 }
59
60 pub fn translate(self, rows: i32, cols: i32) -> Rect {
61 Rect::new(
62 translate_u16(self.top, rows),
63 translate_u16(self.left, cols),
64 self.width,
65 self.height,
66 )
67 }
68
69 pub fn to_grid(self, origin: Rect) -> Rect {
70 Rect::new(
71 origin.top.saturating_add(self.top),
72 origin.left.saturating_add(self.left),
73 self.width,
74 self.height,
75 )
76 }
77
78 pub fn to_local(self, origin: Rect) -> Rect {
79 Rect::new(
80 self.top.saturating_sub(origin.top),
81 self.left.saturating_sub(origin.left),
82 self.width,
83 self.height,
84 )
85 }
86
87 pub fn intersection(self, other: Rect) -> Rect {
88 let top = self.top.max(other.top);
89 let left = self.left.max(other.left);
90 let bottom = self.bottom().min(other.bottom());
91 let right = self.right().min(other.right());
92 Rect::new(
93 top.min(bottom),
94 left.min(right),
95 right.saturating_sub(left),
96 bottom.saturating_sub(top),
97 )
98 }
99
100 pub fn clip_to(self, bounds: Rect) -> Rect {
101 self.intersection(bounds)
102 }
103
104 pub fn split_top(self, height: u16) -> (Rect, Rect) {
105 let top_height = height.min(self.height);
106 let top = Rect::new(self.top, self.left, self.width, top_height);
107 let rest = Rect::new(
108 self.top.saturating_add(top_height),
109 self.left,
110 self.width,
111 self.height.saturating_sub(top_height),
112 );
113 (top, rest)
114 }
115
116 pub fn split_bottom(self, height: u16) -> (Rect, Rect) {
117 let bottom_height = height.min(self.height);
118 let rest_height = self.height.saturating_sub(bottom_height);
119 let rest = Rect::new(self.top, self.left, self.width, rest_height);
120 let bottom = Rect::new(
121 self.top.saturating_add(rest_height),
122 self.left,
123 self.width,
124 bottom_height,
125 );
126 (rest, bottom)
127 }
128
129 pub fn split_left(self, width: u16) -> (Rect, Rect) {
130 let left_width = width.min(self.width);
131 let left = Rect::new(self.top, self.left, left_width, self.height);
132 let rest = Rect::new(
133 self.top,
134 self.left.saturating_add(left_width),
135 self.width.saturating_sub(left_width),
136 self.height,
137 );
138 (left, rest)
139 }
140
141 pub fn split_right(self, width: u16) -> (Rect, Rect) {
142 let right_width = width.min(self.width);
143 let rest_width = self.width.saturating_sub(right_width);
144 let rest = Rect::new(self.top, self.left, rest_width, self.height);
145 let right = Rect::new(
146 self.top,
147 self.left.saturating_add(rest_width),
148 right_width,
149 self.height,
150 );
151 (rest, right)
152 }
153
154 pub fn split_y(self, top_height: u16, gap: u16) -> (Rect, Rect) {
155 let top_height = top_height.min(self.height);
156 let bottom_top = self.top.saturating_add(top_height).saturating_add(gap);
157 let used = top_height.saturating_add(gap).min(self.height);
158 let top = Rect::new(self.top, self.left, self.width, top_height);
159 let bottom = Rect::new(
160 bottom_top.min(self.bottom()),
161 self.left,
162 self.width,
163 self.height.saturating_sub(used),
164 );
165 (top, bottom)
166 }
167
168 pub fn split_x(self, left_width: u16, gap: u16) -> (Rect, Rect) {
169 let left_width = left_width.min(self.width);
170 let right_left = self.left.saturating_add(left_width).saturating_add(gap);
171 let used = left_width.saturating_add(gap).min(self.width);
172 let left = Rect::new(self.top, self.left, left_width, self.height);
173 let right = Rect::new(
174 self.top,
175 right_left.min(self.right()),
176 self.width.saturating_sub(used),
177 self.height,
178 );
179 (left, right)
180 }
181
182 pub fn centered(self, width: u16, height: u16) -> Rect {
183 let width = width.min(self.width);
184 let height = height.min(self.height);
185 Rect::new(
186 self.top.saturating_add((self.height - height) / 2),
187 self.left.saturating_add((self.width - width) / 2),
188 width,
189 height,
190 )
191 }
192}
193
194#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
195pub struct Insets {
196 pub top: u16,
197 pub right: u16,
198 pub bottom: u16,
199 pub left: u16,
200}
201
202impl Insets {
203 pub fn new(top: u16, right: u16, bottom: u16, left: u16) -> Self {
204 Self {
205 top,
206 right,
207 bottom,
208 left,
209 }
210 }
211
212 pub fn uniform(amount: u16) -> Self {
213 Self::new(amount, amount, amount, amount)
214 }
215}
216
217fn translate_u16(value: u16, delta: i32) -> u16 {
218 if delta.is_negative() {
219 value.saturating_sub(delta.unsigned_abs().min(u16::MAX as u32) as u16)
220 } else {
221 value.saturating_add(delta.min(u16::MAX as i32) as u16)
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn bottom_is_top_plus_height() {
231 let r = Rect::new(3, 0, 10, 7);
232 assert_eq!(r.bottom(), 10);
233 }
234
235 #[test]
236 fn right_is_left_plus_width() {
237 let r = Rect::new(0, 5, 12, 4);
238 assert_eq!(r.right(), 17);
239 }
240
241 #[test]
242 fn area_multiplies_width_by_height() {
243 assert_eq!(Rect::new(0, 0, 80, 24).area(), 1920);
244 assert_eq!(Rect::new(0, 0, 0, 99).area(), 0);
245 }
246
247 #[test]
248 fn contains_includes_top_left_excludes_bottom_right() {
249 let r = Rect::new(5, 10, 4, 3); assert!(r.contains(5, 10), "top-left corner should be inside");
253 assert!(r.contains(7, 13), "last inclusive cell should be inside");
254 assert!(!r.contains(8, 10), "bottom edge is exclusive");
255 assert!(!r.contains(5, 14), "right edge is exclusive");
256 }
257
258 #[test]
259 fn zero_sized_rect_contains_nothing() {
260 let r = Rect::new(5, 5, 0, 0);
261 assert!(!r.contains(5, 5));
262 }
263
264 #[test]
265 fn inset_clamps_tiny_rects() {
266 assert_eq!(Rect::new(1, 2, 3, 2).inset(2), Rect::new(3, 4, 0, 0));
267 assert_eq!(
268 Rect::new(0, 0, 10, 8).inset_by(Insets::new(1, 2, 3, 4)),
269 Rect::new(1, 4, 4, 4)
270 );
271 }
272
273 #[test]
274 fn splits_with_gap_do_not_underflow() {
275 let r = Rect::new(2, 3, 5, 4);
276 assert_eq!(
277 r.split_y(3, 10),
278 (Rect::new(2, 3, 5, 3), Rect::new(6, 3, 5, 0))
279 );
280 assert_eq!(
281 r.split_x(4, 10),
282 (Rect::new(2, 3, 4, 4), Rect::new(2, 8, 0, 4))
283 );
284 }
285
286 #[test]
287 fn centered_clamps_oversized_child() {
288 let r = Rect::new(10, 20, 6, 4);
289 assert_eq!(r.centered(2, 2), Rect::new(11, 22, 2, 2));
290 assert_eq!(r.centered(99, 99), r);
291 assert_eq!(
292 Rect::new(u16::MAX - 1, u16::MAX - 1, 4, 4)
293 .centered(2, 2)
294 .top,
295 u16::MAX
296 );
297 }
298
299 #[test]
300 fn intersection_returns_overlap_or_empty_rect() {
301 assert_eq!(
302 Rect::new(2, 3, 10, 8).intersection(Rect::new(5, 1, 6, 4)),
303 Rect::new(5, 3, 4, 4)
304 );
305 assert_eq!(
306 Rect::new(0, 0, 2, 2).intersection(Rect::new(5, 5, 2, 2)),
307 Rect::new(2, 2, 0, 0)
308 );
309 }
310
311 #[test]
312 fn translate_saturates_at_zero() {
313 assert_eq!(
314 Rect::new(2, 3, 4, 5).translate(-10, -1),
315 Rect::new(0, 2, 4, 5)
316 );
317 assert_eq!(
318 Rect::new(60_000, 3, 4, 5).translate(-60_000, 4),
319 Rect::new(0, 7, 4, 5)
320 );
321 assert_eq!(Rect::new(2, 3, 4, 5).translate(2, 4), Rect::new(4, 7, 4, 5));
322 }
323
324 #[test]
325 fn grid_and_local_convert_coordinate_spaces() {
326 let origin = Rect::new(10, 20, 30, 40);
327 let local = Rect::new(2, 3, 4, 5);
328 let grid = Rect::new(12, 23, 4, 5);
329 assert_eq!(local.to_grid(origin), grid);
330 assert_eq!(grid.to_local(origin), local);
331 }
332}