Skip to main content

saorsa_core/
geometry.rs

1//! Geometry primitives: Position, Size, Rect.
2
3/// A position in terminal coordinates.
4#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
5pub struct Position {
6    /// X coordinate (column).
7    pub x: u16,
8    /// Y coordinate (row).
9    pub y: u16,
10}
11
12impl Position {
13    /// Create a new position.
14    pub const fn new(x: u16, y: u16) -> Self {
15        Self { x, y }
16    }
17}
18
19impl From<(u16, u16)> for Position {
20    fn from((x, y): (u16, u16)) -> Self {
21        Self { x, y }
22    }
23}
24
25/// A size in terminal cells.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
27pub struct Size {
28    /// Width in columns.
29    pub width: u16,
30    /// Height in rows.
31    pub height: u16,
32}
33
34impl Size {
35    /// Create a new size.
36    pub const fn new(width: u16, height: u16) -> Self {
37        Self { width, height }
38    }
39
40    /// Returns the area (width * height).
41    pub const fn area(self) -> u32 {
42        self.width as u32 * self.height as u32
43    }
44
45    /// Returns true if either dimension is zero.
46    pub const fn is_empty(self) -> bool {
47        self.width == 0 || self.height == 0
48    }
49}
50
51impl From<(u16, u16)> for Size {
52    fn from((width, height): (u16, u16)) -> Self {
53        Self { width, height }
54    }
55}
56
57/// A rectangle defined by position and size.
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
59pub struct Rect {
60    /// Top-left position.
61    pub position: Position,
62    /// Dimensions.
63    pub size: Size,
64}
65
66impl Rect {
67    /// Create a new rectangle.
68    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
69        Self {
70            position: Position::new(x, y),
71            size: Size::new(width, height),
72        }
73    }
74
75    /// X coordinate of the right edge (exclusive).
76    pub const fn right(self) -> u16 {
77        self.position.x.saturating_add(self.size.width)
78    }
79
80    /// Y coordinate of the bottom edge (exclusive).
81    pub const fn bottom(self) -> u16 {
82        self.position.y.saturating_add(self.size.height)
83    }
84
85    /// Area of the rectangle.
86    pub const fn area(self) -> u32 {
87        self.size.area()
88    }
89
90    /// Returns true if the rectangle has zero area.
91    pub const fn is_empty(self) -> bool {
92        self.size.is_empty()
93    }
94
95    /// Returns true if the point is inside this rectangle.
96    pub const fn contains(self, pos: Position) -> bool {
97        pos.x >= self.position.x
98            && pos.x < self.right()
99            && pos.y >= self.position.y
100            && pos.y < self.bottom()
101    }
102
103    /// Returns true if two rectangles overlap.
104    pub const fn intersects(self, other: &Rect) -> bool {
105        self.position.x < other.right()
106            && self.right() > other.position.x
107            && self.position.y < other.bottom()
108            && self.bottom() > other.position.y
109    }
110
111    /// Returns the intersection of two rectangles, or `None` if they don't overlap.
112    pub fn intersection(self, other: &Rect) -> Option<Rect> {
113        if !self.intersects(other) {
114            return None;
115        }
116        let x = self.position.x.max(other.position.x);
117        let y = self.position.y.max(other.position.y);
118        let right = self.right().min(other.right());
119        let bottom = self.bottom().min(other.bottom());
120        Some(Rect::new(x, y, right - x, bottom - y))
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn position_from_tuple() {
130        let pos: Position = (5, 10).into();
131        assert_eq!(pos, Position::new(5, 10));
132    }
133
134    #[test]
135    fn size_from_tuple() {
136        let sz: Size = (80, 24).into();
137        assert_eq!(sz, Size::new(80, 24));
138    }
139
140    #[test]
141    fn size_area() {
142        assert_eq!(Size::new(10, 5).area(), 50);
143    }
144
145    #[test]
146    fn size_empty() {
147        assert!(Size::new(0, 10).is_empty());
148        assert!(Size::new(10, 0).is_empty());
149        assert!(!Size::new(1, 1).is_empty());
150    }
151
152    #[test]
153    fn rect_right_bottom() {
154        let r = Rect::new(5, 10, 20, 15);
155        assert_eq!(r.right(), 25);
156        assert_eq!(r.bottom(), 25);
157    }
158
159    #[test]
160    fn rect_contains() {
161        let r = Rect::new(10, 10, 20, 20);
162        assert!(r.contains(Position::new(10, 10)));
163        assert!(r.contains(Position::new(29, 29)));
164        assert!(!r.contains(Position::new(30, 30)));
165        assert!(!r.contains(Position::new(9, 10)));
166    }
167
168    #[test]
169    fn rect_intersects() {
170        let a = Rect::new(0, 0, 10, 10);
171        let b = Rect::new(5, 5, 10, 10);
172        let c = Rect::new(20, 20, 5, 5);
173        assert!(a.intersects(&b));
174        assert!(b.intersects(&a));
175        assert!(!a.intersects(&c));
176    }
177
178    #[test]
179    fn rect_intersection() {
180        let a = Rect::new(0, 0, 10, 10);
181        let b = Rect::new(5, 5, 10, 10);
182        let i = a.intersection(&b);
183        assert_eq!(i, Some(Rect::new(5, 5, 5, 5)));
184    }
185
186    #[test]
187    fn rect_no_intersection() {
188        let a = Rect::new(0, 0, 5, 5);
189        let b = Rect::new(10, 10, 5, 5);
190        assert_eq!(a.intersection(&b), None);
191    }
192
193    #[test]
194    fn rect_empty() {
195        assert!(Rect::new(0, 0, 0, 5).is_empty());
196        assert!(!Rect::new(0, 0, 1, 1).is_empty());
197    }
198
199    #[test]
200    fn rect_area() {
201        assert_eq!(Rect::new(0, 0, 10, 5).area(), 50);
202    }
203
204    #[test]
205    fn rect_saturating_overflow() {
206        let r = Rect::new(u16::MAX, u16::MAX, 10, 10);
207        assert_eq!(r.right(), u16::MAX);
208        assert_eq!(r.bottom(), u16::MAX);
209    }
210}