Skip to main content

slt/
rect.rs

1//! Axis-aligned rectangle type used throughout SLT for layout regions,
2//! clipping bounds, and hit-test areas.
3
4/// An axis-aligned rectangle with `u32` coordinates.
5///
6/// Uses `u32` rather than `u16` to avoid overflow bugs that affect other TUI
7/// libraries on large terminals. All coordinates are in terminal columns and
8/// rows, with `(0, 0)` at the top-left.
9///
10/// Note: [`Rect::right`] and [`Rect::bottom`] return **exclusive** bounds
11/// (one past the last column/row), consistent with Rust range conventions.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub struct Rect {
14    /// Left edge column, inclusive.
15    pub x: u32,
16    /// Top edge row, inclusive.
17    pub y: u32,
18    /// Width in terminal columns.
19    pub width: u32,
20    /// Height in terminal rows.
21    pub height: u32,
22}
23
24impl Rect {
25    /// Create a new rectangle from position and size.
26    #[inline]
27    pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
28        Self {
29            x,
30            y,
31            width,
32            height,
33        }
34    }
35
36    /// Total area in cells (`width * height`).
37    #[inline]
38    pub const fn area(&self) -> u32 {
39        self.width * self.height
40    }
41
42    /// Exclusive right edge (`x + width`).
43    ///
44    /// This is one column past the last column in the rectangle.
45    #[inline]
46    pub const fn right(&self) -> u32 {
47        self.x + self.width
48    }
49
50    /// Exclusive bottom edge (`y + height`).
51    ///
52    /// This is one row past the last row in the rectangle.
53    #[inline]
54    pub const fn bottom(&self) -> u32 {
55        self.y + self.height
56    }
57
58    /// Returns `true` if the rectangle has zero area (width or height is zero).
59    #[inline]
60    pub const fn is_empty(&self) -> bool {
61        self.width == 0 || self.height == 0
62    }
63
64    /// Returns a smaller Rect centered within self.
65    ///
66    /// If the inner dimensions exceed self's dimensions, they are clamped to self's size.
67    /// The returned rectangle is positioned such that it is centered both horizontally
68    /// and vertically within self.
69    ///
70    /// # Example
71    /// ```
72    /// use slt::Rect;
73    /// let outer = Rect::new(0, 0, 10, 10);
74    /// let inner = outer.centered(4, 4);
75    /// assert_eq!(inner, Rect::new(3, 3, 4, 4));
76    /// ```
77    #[inline]
78    pub fn centered(&self, inner_w: u32, inner_h: u32) -> Rect {
79        let w = inner_w.min(self.width);
80        let h = inner_h.min(self.height);
81        let x = self.x + (self.width.saturating_sub(w)) / 2;
82        let y = self.y + (self.height.saturating_sub(h)) / 2;
83        Rect {
84            x,
85            y,
86            width: w,
87            height: h,
88        }
89    }
90
91    /// Returns the smallest Rect containing both self and other.
92    ///
93    /// The union encompasses all cells in both rectangles. If either rectangle is empty,
94    /// the result may have unexpected dimensions; use `is_empty()` to check.
95    ///
96    /// # Example
97    /// ```
98    /// use slt::Rect;
99    /// let r1 = Rect::new(0, 0, 5, 5);
100    /// let r2 = Rect::new(3, 3, 5, 5);
101    /// let union = r1.union(r2);
102    /// assert_eq!(union, Rect::new(0, 0, 8, 8));
103    /// ```
104    #[inline]
105    pub fn union(&self, other: Rect) -> Rect {
106        let x = self.x.min(other.x);
107        let y = self.y.min(other.y);
108        let right = self.right().max(other.right());
109        let bottom = self.bottom().max(other.bottom());
110        Rect {
111            x,
112            y,
113            width: right - x,
114            height: bottom - y,
115        }
116    }
117
118    /// Returns the overlapping region between self and other, or None if they don't overlap.
119    ///
120    /// Two rectangles overlap if they share at least one cell. Adjacent rectangles
121    /// (touching at an edge but not overlapping) return None.
122    ///
123    /// # Example
124    /// ```
125    /// use slt::Rect;
126    /// let r1 = Rect::new(0, 0, 5, 5);
127    /// let r2 = Rect::new(3, 3, 5, 5);
128    /// let overlap = r1.intersection(r2);
129    /// assert_eq!(overlap, Some(Rect::new(3, 3, 2, 2)));
130    /// ```
131    #[inline]
132    pub fn intersection(&self, other: Rect) -> Option<Rect> {
133        let x = self.x.max(other.x);
134        let y = self.y.max(other.y);
135        let right = self.right().min(other.right());
136        let bottom = self.bottom().min(other.bottom());
137
138        if x < right && y < bottom {
139            Some(Rect {
140                x,
141                y,
142                width: right - x,
143                height: bottom - y,
144            })
145        } else {
146            None
147        }
148    }
149
150    /// Returns true if the point (x, y) is inside the rectangle.
151    ///
152    /// A point is considered inside if it is within the inclusive left/top bounds
153    /// and exclusive right/bottom bounds (consistent with Rust range conventions).
154    ///
155    /// # Example
156    /// ```
157    /// use slt::Rect;
158    /// let r = Rect::new(5, 5, 10, 10);
159    /// assert!(r.contains(5, 5));   // top-left corner
160    /// assert!(r.contains(14, 14)); // inside
161    /// assert!(!r.contains(15, 15)); // outside (exclusive right/bottom)
162    /// ```
163    #[inline]
164    pub fn contains(&self, x: u32, y: u32) -> bool {
165        x >= self.x && x < self.right() && y >= self.y && y < self.bottom()
166    }
167
168    /// Returns an iterator over row y-coordinates in this rectangle.
169    ///
170    /// Yields values from `self.y` to `self.bottom() - 1` (inclusive).
171    ///
172    /// # Example
173    /// ```
174    /// use slt::Rect;
175    /// let r = Rect::new(0, 2, 5, 3);
176    /// let rows: Vec<u32> = r.rows().collect();
177    /// assert_eq!(rows, vec![2, 3, 4]);
178    /// ```
179    #[inline]
180    pub fn rows(&self) -> impl Iterator<Item = u32> {
181        self.y..self.bottom()
182    }
183
184    /// Returns an iterator over all (x, y) positions in this rectangle, row by row.
185    ///
186    /// Iterates from top-left to bottom-right, filling each row left-to-right before
187    /// moving to the next row. Total count is `width * height`.
188    ///
189    /// # Example
190    /// ```
191    /// use slt::Rect;
192    /// let r = Rect::new(0, 0, 2, 2);
193    /// let positions: Vec<(u32, u32)> = r.positions().collect();
194    /// assert_eq!(positions, vec![(0, 0), (1, 0), (0, 1), (1, 1)]);
195    /// ```
196    #[inline]
197    pub fn positions(&self) -> impl Iterator<Item = (u32, u32)> {
198        let x_start = self.x;
199        let x_end = self.right();
200        let y_start = self.y;
201        let y_end = self.bottom();
202
203        (y_start..y_end).flat_map(move |y| (x_start..x_end).map(move |x| (x, y)))
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_centered_normal() {
213        let outer = Rect::new(0, 0, 10, 10);
214        let inner = outer.centered(4, 4);
215        assert_eq!(inner, Rect::new(3, 3, 4, 4));
216    }
217
218    #[test]
219    fn test_centered_larger_than_self() {
220        let outer = Rect::new(0, 0, 10, 10);
221        let inner = outer.centered(20, 20);
222        assert_eq!(inner, Rect::new(0, 0, 10, 10));
223    }
224
225    #[test]
226    fn test_centered_zero_size() {
227        let outer = Rect::new(5, 5, 10, 10);
228        let inner = outer.centered(0, 0);
229        assert_eq!(inner, Rect::new(10, 10, 0, 0));
230    }
231
232    #[test]
233    fn test_centered_offset() {
234        let outer = Rect::new(10, 20, 20, 20);
235        let inner = outer.centered(10, 10);
236        assert_eq!(inner, Rect::new(15, 25, 10, 10));
237    }
238
239    #[test]
240    fn test_union_overlapping() {
241        let r1 = Rect::new(0, 0, 5, 5);
242        let r2 = Rect::new(3, 3, 5, 5);
243        let union = r1.union(r2);
244        assert_eq!(union, Rect::new(0, 0, 8, 8));
245    }
246
247    #[test]
248    fn test_union_non_overlapping() {
249        let r1 = Rect::new(0, 0, 5, 5);
250        let r2 = Rect::new(10, 10, 5, 5);
251        let union = r1.union(r2);
252        assert_eq!(union, Rect::new(0, 0, 15, 15));
253    }
254
255    #[test]
256    fn test_union_same_rect() {
257        let r = Rect::new(5, 5, 10, 10);
258        let union = r.union(r);
259        assert_eq!(union, r);
260    }
261
262    #[test]
263    fn test_intersection_overlapping() {
264        let r1 = Rect::new(0, 0, 5, 5);
265        let r2 = Rect::new(3, 3, 5, 5);
266        let overlap = r1.intersection(r2);
267        assert_eq!(overlap, Some(Rect::new(3, 3, 2, 2)));
268    }
269
270    #[test]
271    fn test_intersection_non_overlapping() {
272        let r1 = Rect::new(0, 0, 5, 5);
273        let r2 = Rect::new(10, 10, 5, 5);
274        let overlap = r1.intersection(r2);
275        assert_eq!(overlap, None);
276    }
277
278    #[test]
279    fn test_intersection_adjacent() {
280        let r1 = Rect::new(0, 0, 5, 5);
281        let r2 = Rect::new(5, 0, 5, 5);
282        let overlap = r1.intersection(r2);
283        assert_eq!(overlap, None);
284    }
285
286    #[test]
287    fn test_intersection_same_rect() {
288        let r = Rect::new(5, 5, 10, 10);
289        let overlap = r.intersection(r);
290        assert_eq!(overlap, Some(r));
291    }
292
293    #[test]
294    fn test_contains_inside() {
295        let r = Rect::new(5, 5, 10, 10);
296        assert!(r.contains(5, 5));
297        assert!(r.contains(10, 10));
298        assert!(r.contains(14, 14));
299    }
300
301    #[test]
302    fn test_contains_outside() {
303        let r = Rect::new(5, 5, 10, 10);
304        assert!(!r.contains(4, 5));
305        assert!(!r.contains(5, 4));
306        assert!(!r.contains(15, 15));
307        assert!(!r.contains(15, 10));
308    }
309
310    #[test]
311    fn test_contains_on_edge() {
312        let r = Rect::new(5, 5, 10, 10);
313        assert!(r.contains(5, 5)); // top-left inclusive
314        assert!(!r.contains(15, 5)); // right exclusive
315        assert!(!r.contains(5, 15)); // bottom exclusive
316    }
317
318    #[test]
319    fn test_rows_correct_range() {
320        let r = Rect::new(0, 2, 5, 3);
321        let rows: Vec<u32> = r.rows().collect();
322        assert_eq!(rows, vec![2, 3, 4]);
323    }
324
325    #[test]
326    fn test_rows_single_row() {
327        let r = Rect::new(0, 5, 10, 1);
328        let rows: Vec<u32> = r.rows().collect();
329        assert_eq!(rows, vec![5]);
330    }
331
332    #[test]
333    fn test_rows_empty() {
334        let r = Rect::new(0, 5, 10, 0);
335        let rows: Vec<u32> = r.rows().collect();
336        assert!(rows.is_empty());
337    }
338
339    #[test]
340    fn test_positions_correct_count() {
341        let r = Rect::new(0, 0, 3, 2);
342        let positions: Vec<(u32, u32)> = r.positions().collect();
343        assert_eq!(positions.len(), 6);
344    }
345
346    #[test]
347    fn test_positions_order() {
348        let r = Rect::new(0, 0, 2, 2);
349        let positions: Vec<(u32, u32)> = r.positions().collect();
350        assert_eq!(positions, vec![(0, 0), (1, 0), (0, 1), (1, 1)]);
351    }
352
353    #[test]
354    fn test_positions_offset() {
355        let r = Rect::new(5, 3, 2, 2);
356        let positions: Vec<(u32, u32)> = r.positions().collect();
357        assert_eq!(positions, vec![(5, 3), (6, 3), (5, 4), (6, 4)]);
358    }
359
360    #[test]
361    fn test_positions_empty() {
362        let r = Rect::new(0, 0, 0, 5);
363        let positions: Vec<(u32, u32)> = r.positions().collect();
364        assert!(positions.is_empty());
365    }
366}