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.saturating_mul(self.height)
40    }
41
42    /// Total area in cells without narrowing to `u32`.
43    #[inline]
44    pub const fn area_u64(&self) -> u64 {
45        self.width as u64 * self.height as u64
46    }
47
48    /// Exclusive right edge (`x + width`).
49    ///
50    /// This is one column past the last column in the rectangle.
51    #[inline]
52    pub const fn right(&self) -> u32 {
53        self.x.saturating_add(self.width)
54    }
55
56    /// Checked exclusive right edge, or `None` when it is not representable.
57    #[inline]
58    pub const fn checked_right(&self) -> Option<u32> {
59        self.x.checked_add(self.width)
60    }
61
62    /// Exclusive bottom edge (`y + height`).
63    ///
64    /// This is one row past the last row in the rectangle.
65    #[inline]
66    pub const fn bottom(&self) -> u32 {
67        self.y.saturating_add(self.height)
68    }
69
70    /// Checked exclusive bottom edge, or `None` when it is not representable.
71    #[inline]
72    pub const fn checked_bottom(&self) -> Option<u32> {
73        self.y.checked_add(self.height)
74    }
75
76    /// Return whether both exclusive edges are representable as `u32`.
77    #[inline]
78    pub const fn has_valid_edges(&self) -> bool {
79        self.checked_right().is_some() && self.checked_bottom().is_some()
80    }
81
82    /// Returns `true` if the rectangle has zero area (width or height is zero).
83    #[inline]
84    pub const fn is_empty(&self) -> bool {
85        self.width == 0 || self.height == 0
86    }
87
88    /// Returns a smaller Rect centered within self.
89    ///
90    /// If the inner dimensions exceed self's dimensions, they are clamped to self's size.
91    /// The returned rectangle is positioned such that it is centered both horizontally
92    /// and vertically within self.
93    ///
94    /// # Example
95    /// ```
96    /// use slt::Rect;
97    /// let outer = Rect::new(0, 0, 10, 10);
98    /// let inner = outer.centered(4, 4);
99    /// assert_eq!(inner, Rect::new(3, 3, 4, 4));
100    /// ```
101    #[inline]
102    pub fn centered(&self, inner_w: u32, inner_h: u32) -> Rect {
103        let w = inner_w.min(self.width);
104        let h = inner_h.min(self.height);
105        let x = self.x.saturating_add((self.width.saturating_sub(w)) / 2);
106        let y = self.y.saturating_add((self.height.saturating_sub(h)) / 2);
107        Rect {
108            x,
109            y,
110            width: w,
111            height: h,
112        }
113    }
114
115    /// Returns the smallest Rect containing both self and other.
116    ///
117    /// The union encompasses all cells in both rectangles. If either rectangle is empty,
118    /// the result may have unexpected dimensions; use `is_empty()` to check.
119    ///
120    /// # Example
121    /// ```
122    /// use slt::Rect;
123    /// let r1 = Rect::new(0, 0, 5, 5);
124    /// let r2 = Rect::new(3, 3, 5, 5);
125    /// let union = r1.union(r2);
126    /// assert_eq!(union, Rect::new(0, 0, 8, 8));
127    /// ```
128    #[inline]
129    pub fn union(&self, other: Rect) -> Rect {
130        let x = self.x.min(other.x);
131        let y = self.y.min(other.y);
132        let right = self.right().max(other.right());
133        let bottom = self.bottom().max(other.bottom());
134        Rect {
135            x,
136            y,
137            width: right.saturating_sub(x),
138            height: bottom.saturating_sub(y),
139        }
140    }
141
142    /// Returns the overlapping region between self and other, or None if they don't overlap.
143    ///
144    /// Two rectangles overlap if they share at least one cell. Adjacent rectangles
145    /// (touching at an edge but not overlapping) return None.
146    ///
147    /// # Example
148    /// ```
149    /// use slt::Rect;
150    /// let r1 = Rect::new(0, 0, 5, 5);
151    /// let r2 = Rect::new(3, 3, 5, 5);
152    /// let overlap = r1.intersection(r2);
153    /// assert_eq!(overlap, Some(Rect::new(3, 3, 2, 2)));
154    /// ```
155    #[inline]
156    pub fn intersection(&self, other: Rect) -> Option<Rect> {
157        let x = self.x.max(other.x);
158        let y = self.y.max(other.y);
159        let right = self.right().min(other.right());
160        let bottom = self.bottom().min(other.bottom());
161
162        if x < right && y < bottom {
163            Some(Rect {
164                x,
165                y,
166                width: right.saturating_sub(x),
167                height: bottom.saturating_sub(y),
168            })
169        } else {
170            None
171        }
172    }
173
174    /// Returns true if the point (x, y) is inside the rectangle.
175    ///
176    /// A point is considered inside if it is within the inclusive left/top bounds
177    /// and exclusive right/bottom bounds (consistent with Rust range conventions).
178    ///
179    /// # Example
180    /// ```
181    /// use slt::Rect;
182    /// let r = Rect::new(5, 5, 10, 10);
183    /// assert!(r.contains(5, 5));   // top-left corner
184    /// assert!(r.contains(14, 14)); // inside
185    /// assert!(!r.contains(15, 15)); // outside (exclusive right/bottom)
186    /// ```
187    #[inline]
188    pub fn contains(&self, x: u32, y: u32) -> bool {
189        x >= self.x && x < self.right() && y >= self.y && y < self.bottom()
190    }
191
192    /// Returns an iterator over row y-coordinates in this rectangle.
193    ///
194    /// Yields values from `self.y` to `self.bottom() - 1` (inclusive).
195    ///
196    /// # Example
197    /// ```
198    /// use slt::Rect;
199    /// let r = Rect::new(0, 2, 5, 3);
200    /// let rows: Vec<u32> = r.rows().collect();
201    /// assert_eq!(rows, vec![2, 3, 4]);
202    /// ```
203    #[inline]
204    pub fn rows(&self) -> impl Iterator<Item = u32> + use<> {
205        self.y..self.bottom()
206    }
207
208    /// Returns an iterator over all (x, y) positions in this rectangle, row by row.
209    ///
210    /// Iterates from top-left to bottom-right, filling each row left-to-right before
211    /// moving to the next row. Total count is `width * height`.
212    ///
213    /// # Example
214    /// ```
215    /// use slt::Rect;
216    /// let r = Rect::new(0, 0, 2, 2);
217    /// let positions: Vec<(u32, u32)> = r.positions().collect();
218    /// assert_eq!(positions, vec![(0, 0), (1, 0), (0, 1), (1, 1)]);
219    /// ```
220    #[inline]
221    pub fn positions(&self) -> impl Iterator<Item = (u32, u32)> + use<> {
222        let x_start = self.x;
223        let x_end = self.right();
224        let y_start = self.y;
225        let y_end = self.bottom();
226
227        (y_start..y_end).flat_map(move |y| (x_start..x_end).map(move |x| (x, y)))
228    }
229
230    /// Position `self` centered both horizontally and vertically inside `parent`.
231    ///
232    /// Returns a [`Rect`] with the same `width`/`height` as `self`, but with
233    /// `x`/`y` adjusted so the result is centered within `parent`. If `self`
234    /// is wider or taller than `parent` on either axis, the corresponding
235    /// dimension is clamped to `parent`'s extent on that axis (matching
236    /// [`Rect::centered`]'s clamp policy). Self's existing `x`/`y` are
237    /// ignored — only its dimensions matter.
238    ///
239    /// This is the inverse of [`Rect::centered`]: `centered` answers "give
240    /// me an inner rect of size W×H centered in me," whereas `center_in`
241    /// answers "position me centered inside parent."
242    ///
243    /// # Example
244    /// ```
245    /// use slt::Rect;
246    /// let dialog = Rect::new(0, 0, 40, 10);
247    /// let screen = Rect::new(0, 0, 120, 40);
248    /// let r = dialog.center_in(screen);
249    /// assert_eq!(r, Rect::new(40, 15, 40, 10));
250    /// ```
251    #[inline]
252    pub const fn center_in(self, parent: Rect) -> Rect {
253        let w = if self.width < parent.width {
254            self.width
255        } else {
256            parent.width
257        };
258        let h = if self.height < parent.height {
259            self.height
260        } else {
261            parent.height
262        };
263        let x = parent.x.saturating_add(parent.width.saturating_sub(w) / 2);
264        let y = parent.y.saturating_add(parent.height.saturating_sub(h) / 2);
265        Rect {
266            x,
267            y,
268            width: w,
269            height: h,
270        }
271    }
272
273    /// Position `self` centered horizontally inside `parent`; preserve `self.y` and `self.height`.
274    ///
275    /// If `self.width` exceeds `parent.width`, the returned rect's width is
276    /// clamped to `parent.width` (matching [`Rect::centered`]).
277    ///
278    /// # Example
279    /// ```
280    /// use slt::Rect;
281    /// let banner = Rect::new(0, 5, 30, 3);
282    /// let screen = Rect::new(0, 0, 120, 40);
283    /// let r = banner.center_horizontally_in(screen);
284    /// assert_eq!(r, Rect::new(45, 5, 30, 3));
285    /// ```
286    #[inline]
287    pub const fn center_horizontally_in(self, parent: Rect) -> Rect {
288        let w = if self.width < parent.width {
289            self.width
290        } else {
291            parent.width
292        };
293        let x = parent.x.saturating_add(parent.width.saturating_sub(w) / 2);
294        Rect {
295            x,
296            y: self.y,
297            width: w,
298            height: self.height,
299        }
300    }
301
302    /// Position `self` centered vertically inside `parent`; preserve `self.x` and `self.width`.
303    ///
304    /// If `self.height` exceeds `parent.height`, the returned rect's height
305    /// is clamped to `parent.height` (matching [`Rect::centered`]).
306    ///
307    /// # Example
308    /// ```
309    /// use slt::Rect;
310    /// let sidebar = Rect::new(2, 0, 20, 10);
311    /// let screen = Rect::new(0, 0, 120, 40);
312    /// let r = sidebar.center_vertically_in(screen);
313    /// assert_eq!(r, Rect::new(2, 15, 20, 10));
314    /// ```
315    #[inline]
316    pub const fn center_vertically_in(self, parent: Rect) -> Rect {
317        let h = if self.height < parent.height {
318            self.height
319        } else {
320            parent.height
321        };
322        let y = parent.y.saturating_add(parent.height.saturating_sub(h) / 2);
323        Rect {
324            x: self.x,
325            y,
326            width: self.width,
327            height: h,
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn test_centered_normal() {
338        let outer = Rect::new(0, 0, 10, 10);
339        let inner = outer.centered(4, 4);
340        assert_eq!(inner, Rect::new(3, 3, 4, 4));
341    }
342
343    #[test]
344    fn test_centered_larger_than_self() {
345        let outer = Rect::new(0, 0, 10, 10);
346        let inner = outer.centered(20, 20);
347        assert_eq!(inner, Rect::new(0, 0, 10, 10));
348    }
349
350    #[test]
351    fn test_centered_zero_size() {
352        let outer = Rect::new(5, 5, 10, 10);
353        let inner = outer.centered(0, 0);
354        assert_eq!(inner, Rect::new(10, 10, 0, 0));
355    }
356
357    #[test]
358    fn test_centered_offset() {
359        let outer = Rect::new(10, 20, 20, 20);
360        let inner = outer.centered(10, 10);
361        assert_eq!(inner, Rect::new(15, 25, 10, 10));
362    }
363
364    #[test]
365    fn test_union_overlapping() {
366        let r1 = Rect::new(0, 0, 5, 5);
367        let r2 = Rect::new(3, 3, 5, 5);
368        let union = r1.union(r2);
369        assert_eq!(union, Rect::new(0, 0, 8, 8));
370    }
371
372    #[test]
373    fn test_union_non_overlapping() {
374        let r1 = Rect::new(0, 0, 5, 5);
375        let r2 = Rect::new(10, 10, 5, 5);
376        let union = r1.union(r2);
377        assert_eq!(union, Rect::new(0, 0, 15, 15));
378    }
379
380    #[test]
381    fn test_union_same_rect() {
382        let r = Rect::new(5, 5, 10, 10);
383        let union = r.union(r);
384        assert_eq!(union, r);
385    }
386
387    #[test]
388    fn test_intersection_overlapping() {
389        let r1 = Rect::new(0, 0, 5, 5);
390        let r2 = Rect::new(3, 3, 5, 5);
391        let overlap = r1.intersection(r2);
392        assert_eq!(overlap, Some(Rect::new(3, 3, 2, 2)));
393    }
394
395    #[test]
396    fn test_intersection_non_overlapping() {
397        let r1 = Rect::new(0, 0, 5, 5);
398        let r2 = Rect::new(10, 10, 5, 5);
399        let overlap = r1.intersection(r2);
400        assert_eq!(overlap, None);
401    }
402
403    #[test]
404    fn test_intersection_adjacent() {
405        let r1 = Rect::new(0, 0, 5, 5);
406        let r2 = Rect::new(5, 0, 5, 5);
407        let overlap = r1.intersection(r2);
408        assert_eq!(overlap, None);
409    }
410
411    #[test]
412    fn test_intersection_same_rect() {
413        let r = Rect::new(5, 5, 10, 10);
414        let overlap = r.intersection(r);
415        assert_eq!(overlap, Some(r));
416    }
417
418    #[test]
419    fn test_contains_inside() {
420        let r = Rect::new(5, 5, 10, 10);
421        assert!(r.contains(5, 5));
422        assert!(r.contains(10, 10));
423        assert!(r.contains(14, 14));
424    }
425
426    #[test]
427    fn test_contains_outside() {
428        let r = Rect::new(5, 5, 10, 10);
429        assert!(!r.contains(4, 5));
430        assert!(!r.contains(5, 4));
431        assert!(!r.contains(15, 15));
432        assert!(!r.contains(15, 10));
433    }
434
435    #[test]
436    fn test_contains_on_edge() {
437        let r = Rect::new(5, 5, 10, 10);
438        assert!(r.contains(5, 5)); // top-left inclusive
439        assert!(!r.contains(15, 5)); // right exclusive
440        assert!(!r.contains(5, 15)); // bottom exclusive
441    }
442
443    #[test]
444    fn test_rows_correct_range() {
445        let r = Rect::new(0, 2, 5, 3);
446        let rows: Vec<u32> = r.rows().collect();
447        assert_eq!(rows, vec![2, 3, 4]);
448    }
449
450    #[test]
451    fn test_rows_single_row() {
452        let r = Rect::new(0, 5, 10, 1);
453        let rows: Vec<u32> = r.rows().collect();
454        assert_eq!(rows, vec![5]);
455    }
456
457    #[test]
458    fn test_rows_empty() {
459        let r = Rect::new(0, 5, 10, 0);
460        let rows: Vec<u32> = r.rows().collect();
461        assert!(rows.is_empty());
462    }
463
464    #[test]
465    fn test_positions_correct_count() {
466        let r = Rect::new(0, 0, 3, 2);
467        let positions: Vec<(u32, u32)> = r.positions().collect();
468        assert_eq!(positions.len(), 6);
469    }
470
471    #[test]
472    fn test_positions_order() {
473        let r = Rect::new(0, 0, 2, 2);
474        let positions: Vec<(u32, u32)> = r.positions().collect();
475        assert_eq!(positions, vec![(0, 0), (1, 0), (0, 1), (1, 1)]);
476    }
477
478    #[test]
479    fn test_positions_offset() {
480        let r = Rect::new(5, 3, 2, 2);
481        let positions: Vec<(u32, u32)> = r.positions().collect();
482        assert_eq!(positions, vec![(5, 3), (6, 3), (5, 4), (6, 4)]);
483    }
484
485    #[test]
486    fn test_positions_empty() {
487        let r = Rect::new(0, 0, 0, 5);
488        let positions: Vec<(u32, u32)> = r.positions().collect();
489        assert!(positions.is_empty());
490    }
491
492    #[test]
493    fn rect_area_no_overflow() {
494        // u32::MAX * u32::MAX would wrap to 0 without saturating_mul
495        let r = Rect::new(0, 0, u32::MAX, u32::MAX);
496        assert_eq!(r.area(), u32::MAX);
497        // Concrete case from issue #166: 65536 * 65536 wraps to 0 without fix
498        let r2 = Rect::new(0, 0, 65536, 65536);
499        assert_eq!(r2.area(), u32::MAX);
500        assert_eq!(r2.area_u64(), 4_294_967_296);
501    }
502
503    #[test]
504    fn checked_edges_report_unrepresentable_origins() {
505        let invalid = Rect::new(u32::MAX, u32::MAX, 1, 1);
506        assert_eq!(invalid.checked_right(), None);
507        assert_eq!(invalid.checked_bottom(), None);
508        assert!(!invalid.has_valid_edges());
509
510        let valid = Rect::new(u32::MAX - 1, u32::MAX - 1, 1, 1);
511        assert_eq!(valid.checked_right(), Some(u32::MAX));
512        assert_eq!(valid.checked_bottom(), Some(u32::MAX));
513        assert!(valid.has_valid_edges());
514    }
515
516    #[test]
517    fn rect_edges_saturate_instead_of_wrapping() {
518        let r = Rect::new(u32::MAX, u32::MAX - 1, 10, 10);
519        assert_eq!(r.right(), u32::MAX);
520        assert_eq!(r.bottom(), u32::MAX);
521        assert!(!r.contains(0, 0), "saturated edge must not wrap to origin");
522    }
523
524    #[test]
525    fn rect_union_and_intersection_do_not_wrap_at_u32_max() {
526        let edge = Rect::new(u32::MAX - 1, u32::MAX - 1, 10, 10);
527        let origin = Rect::new(0, 0, 1, 1);
528
529        assert_eq!(edge.union(origin), Rect::new(0, 0, u32::MAX, u32::MAX));
530        assert_eq!(edge.intersection(origin), None);
531    }
532
533    #[test]
534    fn rect_centering_saturates_large_offsets() {
535        let parent = Rect::new(u32::MAX - 2, u32::MAX - 2, 10, 10);
536        let child = Rect::new(0, 0, 2, 2).center_in(parent);
537        assert_eq!(child.x, u32::MAX);
538        assert_eq!(child.y, u32::MAX);
539    }
540
541    #[test]
542    fn test_center_in_basic() {
543        let dialog = Rect::new(0, 0, 40, 10);
544        let screen = Rect::new(0, 0, 120, 40);
545        assert_eq!(dialog.center_in(screen), Rect::new(40, 15, 40, 10));
546    }
547
548    #[test]
549    fn test_center_in_self_bigger_clamps() {
550        // self larger than parent on both axes -> clamp to parent extent.
551        let oversize = Rect::new(0, 0, 200, 80);
552        let screen = Rect::new(0, 0, 120, 40);
553        assert_eq!(oversize.center_in(screen), Rect::new(0, 0, 120, 40));
554    }
555
556    #[test]
557    fn test_center_in_offset_parent() {
558        // Parent at (10, 5) with size 100 x 30; centering 40 x 10 ->
559        // x = 10 + (100 - 40) / 2 = 40, y = 5 + (30 - 10) / 2 = 15
560        let dialog = Rect::new(999, 999, 40, 10); // self.x/self.y ignored
561        let parent = Rect::new(10, 5, 100, 30);
562        assert_eq!(dialog.center_in(parent), Rect::new(40, 15, 40, 10));
563    }
564
565    #[test]
566    fn test_center_in_self_position_ignored() {
567        // self.x/self.y must NOT influence the result — only dimensions.
568        let a = Rect::new(0, 0, 10, 4).center_in(Rect::new(0, 0, 20, 10));
569        let b = Rect::new(99, 99, 10, 4).center_in(Rect::new(0, 0, 20, 10));
570        assert_eq!(a, b);
571    }
572
573    #[test]
574    fn test_center_horizontally_in_preserves_y_height() {
575        let banner = Rect::new(0, 5, 30, 3);
576        let screen = Rect::new(0, 0, 120, 40);
577        assert_eq!(
578            banner.center_horizontally_in(screen),
579            Rect::new(45, 5, 30, 3)
580        );
581    }
582
583    #[test]
584    fn test_center_horizontally_in_clamps_width() {
585        let wide = Rect::new(0, 4, 200, 3);
586        let screen = Rect::new(0, 0, 120, 40);
587        // width clamped, x = 0 (saturating_sub(120, 120) = 0)
588        assert_eq!(wide.center_horizontally_in(screen), Rect::new(0, 4, 120, 3));
589    }
590
591    #[test]
592    fn test_center_vertically_in_preserves_x_width() {
593        let sidebar = Rect::new(2, 0, 20, 10);
594        let screen = Rect::new(0, 0, 120, 40);
595        assert_eq!(
596            sidebar.center_vertically_in(screen),
597            Rect::new(2, 15, 20, 10)
598        );
599    }
600
601    #[test]
602    fn test_center_vertically_in_clamps_height() {
603        let tall = Rect::new(3, 0, 8, 200);
604        let screen = Rect::new(0, 0, 120, 40);
605        assert_eq!(tall.center_vertically_in(screen), Rect::new(3, 0, 8, 40));
606    }
607}