1#![warn(missing_docs)]
2use std::{
3 cmp::{max, min},
4 fmt,
5};
6
7use crate::layout::{Margin, Position, Size};
8
9mod iter;
10pub use iter::*;
11
12#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Rect {
19 pub x: u16,
21 pub y: u16,
23 pub width: u16,
25 pub height: u16,
27}
28
29#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct Offset {
37 pub x: i32,
39 pub y: i32,
41}
42
43impl fmt::Display for Rect {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
46 }
47}
48
49impl Rect {
50 pub const ZERO: Self = Self {
52 x: 0,
53 y: 0,
54 width: 0,
55 height: 0,
56 };
57
58 pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
72 let max_width = u16::MAX - x;
74 let max_height = u16::MAX - y;
75 let width = if width > max_width { max_width } else { width };
76 let height = if height > max_height {
77 max_height
78 } else {
79 height
80 };
81 Self {
82 x,
83 y,
84 width,
85 height,
86 }
87 }
88
89 pub const fn area(self) -> u32 {
92 (self.width as u32) * (self.height as u32)
93 }
94
95 pub const fn is_empty(self) -> bool {
97 self.width == 0 || self.height == 0
98 }
99
100 pub const fn left(self) -> u16 {
102 self.x
103 }
104
105 pub const fn right(self) -> u16 {
111 self.x.saturating_add(self.width)
112 }
113
114 pub const fn top(self) -> u16 {
116 self.y
117 }
118
119 pub const fn bottom(self) -> u16 {
125 self.y.saturating_add(self.height)
126 }
127
128 #[must_use = "method returns the modified value"]
132 pub const fn inner(self, margin: Margin) -> Self {
133 let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
134 let doubled_margin_vertical = margin.vertical.saturating_mul(2);
135
136 if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
137 Self::ZERO
138 } else {
139 Self {
140 x: self.x.saturating_add(margin.horizontal),
141 y: self.y.saturating_add(margin.vertical),
142 width: self.width.saturating_sub(doubled_margin_horizontal),
143 height: self.height.saturating_sub(doubled_margin_vertical),
144 }
145 }
146 }
147
148 #[must_use = "method returns the modified value"]
157 pub fn offset(self, offset: Offset) -> Self {
158 Self {
159 x: i32::from(self.x)
160 .saturating_add(offset.x)
161 .clamp(0, i32::from(u16::MAX - self.width)) as u16,
162 y: i32::from(self.y)
163 .saturating_add(offset.y)
164 .clamp(0, i32::from(u16::MAX - self.height)) as u16,
165 ..self
166 }
167 }
168
169 #[must_use = "method returns the modified value"]
171 pub fn union(self, other: Self) -> Self {
172 let x1 = min(self.x, other.x);
173 let y1 = min(self.y, other.y);
174 let x2 = max(self.right(), other.right());
175 let y2 = max(self.bottom(), other.bottom());
176 Self {
177 x: x1,
178 y: y1,
179 width: x2.saturating_sub(x1),
180 height: y2.saturating_sub(y1),
181 }
182 }
183
184 #[must_use = "method returns the modified value"]
188 pub fn intersection(self, other: Self) -> Self {
189 let x1 = max(self.x, other.x);
190 let y1 = max(self.y, other.y);
191 let x2 = min(self.right(), other.right());
192 let y2 = min(self.bottom(), other.bottom());
193 Self {
194 x: x1,
195 y: y1,
196 width: x2.saturating_sub(x1),
197 height: y2.saturating_sub(y1),
198 }
199 }
200
201 pub const fn intersects(self, other: Self) -> bool {
203 self.x < other.right()
204 && self.right() > other.x
205 && self.y < other.bottom()
206 && self.bottom() > other.y
207 }
208
209 pub const fn contains(self, position: Position) -> bool {
222 position.x >= self.x
223 && position.x < self.right()
224 && position.y >= self.y
225 && position.y < self.bottom()
226 }
227
228 #[must_use = "method returns the modified value"]
254 pub fn clamp(self, other: Self) -> Self {
255 let width = self.width.min(other.width);
256 let height = self.height.min(other.height);
257 let x = self.x.clamp(other.x, other.right().saturating_sub(width));
258 let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
259 Self::new(x, y, width, height)
260 }
261
262 pub const fn rows(self) -> Rows {
276 Rows::new(self)
277 }
278
279 pub const fn columns(self) -> Columns {
296 Columns::new(self)
297 }
298
299 pub const fn positions(self) -> Positions {
315 Positions::new(self)
316 }
317
318 pub const fn as_position(self) -> Position {
329 Position {
330 x: self.x,
331 y: self.y,
332 }
333 }
334
335 pub const fn as_size(self) -> Size {
337 Size {
338 width: self.width,
339 height: self.height,
340 }
341 }
342
343 #[must_use]
347 pub(crate) const fn indent_x(self, offset: u16) -> Self {
348 Self {
349 x: self.x.saturating_add(offset),
350 width: self.width.saturating_sub(offset),
351 ..self
352 }
353 }
354}
355
356impl From<(Position, Size)> for Rect {
357 fn from((position, size): (Position, Size)) -> Self {
358 Self {
359 x: position.x,
360 y: position.y,
361 width: size.width,
362 height: size.height,
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use rstest::rstest;
370
371 use super::*;
372 use crate::layout::{Constraint, Layout};
373
374 #[test]
375 fn to_string() {
376 assert_eq!(Rect::new(1, 2, 3, 4).to_string(), "3x4+1+2");
377 }
378
379 #[test]
380 fn new() {
381 assert_eq!(
382 Rect::new(1, 2, 3, 4),
383 Rect {
384 x: 1,
385 y: 2,
386 width: 3,
387 height: 4
388 }
389 );
390 }
391
392 #[test]
393 fn area() {
394 assert_eq!(Rect::new(1, 2, 3, 4).area(), 12);
395 }
396
397 #[test]
398 fn is_empty() {
399 assert!(!Rect::new(1, 2, 3, 4).is_empty());
400 assert!(Rect::new(1, 2, 0, 4).is_empty());
401 assert!(Rect::new(1, 2, 3, 0).is_empty());
402 }
403
404 #[test]
405 fn left() {
406 assert_eq!(Rect::new(1, 2, 3, 4).left(), 1);
407 }
408
409 #[test]
410 fn right() {
411 assert_eq!(Rect::new(1, 2, 3, 4).right(), 4);
412 }
413
414 #[test]
415 fn top() {
416 assert_eq!(Rect::new(1, 2, 3, 4).top(), 2);
417 }
418
419 #[test]
420 fn bottom() {
421 assert_eq!(Rect::new(1, 2, 3, 4).bottom(), 6);
422 }
423
424 #[test]
425 fn inner() {
426 assert_eq!(
427 Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
428 Rect::new(2, 4, 1, 0)
429 );
430 }
431
432 #[test]
433 fn offset() {
434 assert_eq!(
435 Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
436 Rect::new(6, 8, 3, 4),
437 );
438 }
439
440 #[test]
441 fn negative_offset() {
442 assert_eq!(
443 Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
444 Rect::new(2, 2, 3, 4),
445 );
446 }
447
448 #[test]
449 fn negative_offset_saturate() {
450 assert_eq!(
451 Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
452 Rect::new(0, 0, 3, 4),
453 );
454 }
455
456 #[test]
458 fn offset_saturate_max() {
459 assert_eq!(
460 Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
461 Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
462 );
463 }
464
465 #[test]
466 fn union() {
467 assert_eq!(
468 Rect::new(1, 2, 3, 4).union(Rect::new(2, 3, 4, 5)),
469 Rect::new(1, 2, 5, 6)
470 );
471 }
472
473 #[test]
474 fn intersection() {
475 assert_eq!(
476 Rect::new(1, 2, 3, 4).intersection(Rect::new(2, 3, 4, 5)),
477 Rect::new(2, 3, 2, 3)
478 );
479 }
480
481 #[test]
482 fn intersection_underflow() {
483 assert_eq!(
484 Rect::new(1, 1, 2, 2).intersection(Rect::new(4, 4, 2, 2)),
485 Rect::new(4, 4, 0, 0)
486 );
487 }
488
489 #[test]
490 fn intersects() {
491 assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
492 assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
493 }
494
495 #[rstest]
497 #[case::inside_top_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 2 }, true)]
498 #[case::inside_top_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 2 }, true)]
499 #[case::inside_bottom_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 5 }, true)]
500 #[case::inside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 5 }, true)]
501 #[case::outside_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 2 }, false)]
502 #[case::outside_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 2 }, false)]
503 #[case::outside_top(Rect::new(1, 2, 3, 4), Position { x: 1, y: 1 }, false)]
504 #[case::outside_bottom(Rect::new(1, 2, 3, 4), Position { x: 1, y: 6 }, false)]
505 #[case::outside_top_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 1 }, false)]
506 #[case::outside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 6 }, false)]
507 fn contains(#[case] rect: Rect, #[case] position: Position, #[case] expected: bool) {
508 assert_eq!(
509 rect.contains(position),
510 expected,
511 "rect: {rect:?}, position: {position:?}",
512 );
513 }
514
515 #[test]
516 fn size_truncation() {
517 assert_eq!(
518 Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
519 Rect {
520 x: u16::MAX - 100,
521 y: u16::MAX - 1000,
522 width: 100,
523 height: 1000
524 }
525 );
526 }
527
528 #[test]
529 fn size_preservation() {
530 assert_eq!(
531 Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
532 Rect {
533 x: u16::MAX - 100,
534 y: u16::MAX - 1000,
535 width: 100,
536 height: 1000
537 }
538 );
539 }
540
541 #[test]
542 fn can_be_const() {
543 const RECT: Rect = Rect {
544 x: 0,
545 y: 0,
546 width: 10,
547 height: 10,
548 };
549 const _AREA: u32 = RECT.area();
550 const _LEFT: u16 = RECT.left();
551 const _RIGHT: u16 = RECT.right();
552 const _TOP: u16 = RECT.top();
553 const _BOTTOM: u16 = RECT.bottom();
554 assert!(RECT.intersects(RECT));
555 }
556
557 #[test]
558 fn split() {
559 let [a, b] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
560 .areas(Rect::new(0, 0, 2, 1));
561 assert_eq!(a, Rect::new(0, 0, 1, 1));
562 assert_eq!(b, Rect::new(1, 0, 1, 1));
563 }
564
565 #[test]
566 #[should_panic(expected = "invalid number of rects")]
567 fn split_invalid_number_of_recs() {
568 let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
569 let [_a, _b, _c] = layout.areas(Rect::new(0, 0, 2, 1));
570 }
571
572 #[rstest]
573 #[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
574 #[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
575 #[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
576 #[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
577 #[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
578 #[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
579 #[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
580 #[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
581 #[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
582 #[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
583 #[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
584 #[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
585 fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
586 let other = Rect::new(10, 10, 100, 100);
587 assert_eq!(rect.clamp(other), expected);
588 }
589
590 #[test]
591 fn rows() {
592 let area = Rect::new(0, 0, 3, 2);
593 let rows: Vec<Rect> = area.rows().collect();
594
595 let expected_rows: Vec<Rect> = vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)];
596
597 assert_eq!(rows, expected_rows);
598 }
599
600 #[test]
601 fn columns() {
602 let area = Rect::new(0, 0, 3, 2);
603 let columns: Vec<Rect> = area.columns().collect();
604
605 let expected_columns: Vec<Rect> = vec![
606 Rect::new(0, 0, 1, 2),
607 Rect::new(1, 0, 1, 2),
608 Rect::new(2, 0, 1, 2),
609 ];
610
611 assert_eq!(columns, expected_columns);
612 }
613
614 #[test]
615 fn as_position() {
616 let rect = Rect::new(1, 2, 3, 4);
617 let position = rect.as_position();
618 assert_eq!(position.x, 1);
619 assert_eq!(position.y, 2);
620 }
621
622 #[test]
623 fn as_size() {
624 assert_eq!(
625 Rect::new(1, 2, 3, 4).as_size(),
626 Size {
627 width: 3,
628 height: 4
629 }
630 );
631 }
632
633 #[test]
634 fn from_position_and_size() {
635 let position = Position { x: 1, y: 2 };
636 let size = Size {
637 width: 3,
638 height: 4,
639 };
640 assert_eq!(
641 Rect::from((position, size)),
642 Rect {
643 x: 1,
644 y: 2,
645 width: 3,
646 height: 4
647 }
648 );
649 }
650}