Skip to main content

uncurses/layout/
rect.rs

1//! [`Rect`] — an axis-aligned rectangle in the cell grid.
2
3use core::fmt;
4
5use super::Position;
6
7/// An axis-aligned rectangle in the cell grid.
8///
9/// The rectangle is anchored at its top-left corner `(x, y)` and extends
10/// `width` cells to the right and `height` cells down. The right and
11/// bottom edges are exclusive: a cell at `(x, y)` is contained iff
12/// `left() <= x < right()` and `top() <= y < bottom()`.
13#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
14pub struct Rect {
15    /// Column of the top-left corner.
16    pub x: u16,
17    /// Row of the top-left corner.
18    pub y: u16,
19    /// Width in cells.
20    pub width: u16,
21    /// Height in cells.
22    pub height: u16,
23}
24
25impl Rect {
26    /// An empty rectangle at the origin.
27    pub const ZERO: Self = Self {
28        x: 0,
29        y: 0,
30        width: 0,
31        height: 0,
32    };
33
34    /// Create a rectangle from `(x, y, width, height)`.
35    ///
36    /// `width` and `height` are clamped so that `right()` and `bottom()`
37    /// stay within `u16`.
38    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
39        let width = x.saturating_add(width) - x;
40        let height = y.saturating_add(height) - y;
41        Self {
42            x,
43            y,
44            width,
45            height,
46        }
47    }
48
49    /// Area in cells.
50    pub const fn area(self) -> u32 {
51        (self.width as u32) * (self.height as u32)
52    }
53
54    /// True when the rectangle has zero area.
55    pub const fn is_empty(self) -> bool {
56        self.width == 0 || self.height == 0
57    }
58
59    /// Inclusive left edge (column of the leftmost cell).
60    pub const fn left(self) -> u16 {
61        self.x
62    }
63
64    /// Exclusive right edge (one past the rightmost column).
65    pub const fn right(self) -> u16 {
66        self.x.saturating_add(self.width)
67    }
68
69    /// Inclusive top edge (row of the topmost cell).
70    pub const fn top(self) -> u16 {
71        self.y
72    }
73
74    /// Exclusive bottom edge (one past the bottommost row).
75    pub const fn bottom(self) -> u16 {
76        self.y.saturating_add(self.height)
77    }
78
79    /// The top-left corner as a [`Position`].
80    pub const fn position(self) -> Position {
81        Position {
82            x: self.x,
83            y: self.y,
84        }
85    }
86
87    /// True when `pos` lies inside the rectangle.
88    pub fn contains(self, pos: impl Into<Position>) -> bool {
89        let p = pos.into();
90        p.x >= self.left() && p.x < self.right() && p.y >= self.top() && p.y < self.bottom()
91    }
92
93    /// The largest rectangle contained in both `self` and `other`.
94    pub fn intersection(self, other: Self) -> Self {
95        let x1 = self.x.max(other.x);
96        let y1 = self.y.max(other.y);
97        let x2 = self.right().min(other.right());
98        let y2 = self.bottom().min(other.bottom());
99        if x2 <= x1 || y2 <= y1 {
100            Self::ZERO
101        } else {
102            Self {
103                x: x1,
104                y: y1,
105                width: x2 - x1,
106                height: y2 - y1,
107            }
108        }
109    }
110
111    /// The smallest rectangle that contains both `self` and `other`.
112    pub fn union(self, other: Self) -> Self {
113        if self.is_empty() {
114            return other;
115        }
116        if other.is_empty() {
117            return self;
118        }
119        let x1 = self.x.min(other.x);
120        let y1 = self.y.min(other.y);
121        let x2 = self.right().max(other.right());
122        let y2 = self.bottom().max(other.bottom());
123        Self {
124            x: x1,
125            y: y1,
126            width: x2 - x1,
127            height: y2 - y1,
128        }
129    }
130}
131
132impl From<(u16, u16, u16, u16)> for Rect {
133    fn from((x, y, width, height): (u16, u16, u16, u16)) -> Self {
134        Self::new(x, y, width, height)
135    }
136}
137
138impl From<Rect> for (u16, u16, u16, u16) {
139    fn from(r: Rect) -> Self {
140        (r.x, r.y, r.width, r.height)
141    }
142}
143
144impl fmt::Display for Rect {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn rect_basics() {
156        let r = Rect::new(2, 3, 10, 4);
157        assert_eq!(r.left(), 2);
158        assert_eq!(r.top(), 3);
159        assert_eq!(r.right(), 12);
160        assert_eq!(r.bottom(), 7);
161        assert_eq!(r.area(), 40);
162        assert!(!r.is_empty());
163        assert!(r.contains((2, 3)));
164        assert!(r.contains((11, 6)));
165        assert!(!r.contains((12, 6)));
166        assert!(!r.contains((11, 7)));
167    }
168
169    #[test]
170    fn rect_new_saturates() {
171        let r = Rect::new(u16::MAX - 2, 0, 100, 1);
172        assert_eq!(r.right(), u16::MAX);
173        assert_eq!(r.width, 2);
174    }
175
176    #[test]
177    fn rect_intersection() {
178        let a = Rect::new(0, 0, 10, 10);
179        let b = Rect::new(5, 5, 10, 10);
180        assert_eq!(a.intersection(b), Rect::new(5, 5, 5, 5));
181
182        let disjoint = Rect::new(20, 20, 5, 5);
183        assert!(a.intersection(disjoint).is_empty());
184    }
185
186    #[test]
187    fn rect_union() {
188        let a = Rect::new(0, 0, 5, 5);
189        let b = Rect::new(10, 10, 5, 5);
190        assert_eq!(a.union(b), Rect::new(0, 0, 15, 15));
191        assert_eq!(a.union(Rect::ZERO), a);
192        assert_eq!(Rect::ZERO.union(b), b);
193    }
194}