Skip to main content

qframe/
geometry.rs

1//! Cell geometry: rectangles, sizes and padding.
2//!
3//! Rectangles use signed coordinates so content scrolled above or left of its viewport can be
4//! laid out normally and clipped when drawn.
5
6/// A width and height in terminal cells.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
8pub struct Size {
9    /// Columns.
10    pub width: u16,
11    /// Rows.
12    pub height: u16,
13}
14
15impl Size {
16    /// Creates a size.
17    #[must_use]
18    pub const fn new(width: u16, height: u16) -> Self {
19        Self { width, height }
20    }
21
22    /// The component-wise minimum of two sizes.
23    #[must_use]
24    pub fn min(self, other: Self) -> Self {
25        Self::new(self.width.min(other.width), self.height.min(other.height))
26    }
27}
28
29/// Space kept free inside an area, in cells.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31pub struct Padding {
32    /// Rows above.
33    pub top: u16,
34    /// Columns to the right.
35    pub right: u16,
36    /// Rows below.
37    pub bottom: u16,
38    /// Columns to the left.
39    pub left: u16,
40}
41
42impl Padding {
43    /// The same padding on every side.
44    #[must_use]
45    pub const fn all(cells: u16) -> Self {
46        Self { top: cells, right: cells, bottom: cells, left: cells }
47    }
48
49    /// `vertical` rows above and below, `horizontal` columns left and right.
50    #[must_use]
51    pub const fn symmetric(vertical: u16, horizontal: u16) -> Self {
52        Self { top: vertical, right: horizontal, bottom: vertical, left: horizontal }
53    }
54
55    /// Total columns taken.
56    #[must_use]
57    pub fn horizontal(self) -> u16 {
58        self.left.saturating_add(self.right)
59    }
60
61    /// Total rows taken.
62    #[must_use]
63    pub fn vertical(self) -> u16 {
64        self.top.saturating_add(self.bottom)
65    }
66}
67
68/// A rectangle of cells. `x` and `y` may be negative or beyond the screen.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
70pub struct Rect {
71    /// Left column.
72    pub x: i32,
73    /// Top row.
74    pub y: i32,
75    /// Columns.
76    pub width: u16,
77    /// Rows.
78    pub height: u16,
79}
80
81impl Rect {
82    /// Creates a rectangle.
83    #[must_use]
84    pub const fn new(x: i32, y: i32, width: u16, height: u16) -> Self {
85        Self { x, y, width, height }
86    }
87
88    /// The column just past the right edge, saturating at `i32::MAX`.
89    #[must_use]
90    pub fn right(self) -> i32 {
91        self.x.saturating_add(i32::from(self.width))
92    }
93
94    /// The row just past the bottom edge, saturating at `i32::MAX`.
95    #[must_use]
96    pub fn bottom(self) -> i32 {
97        self.y.saturating_add(i32::from(self.height))
98    }
99
100    /// The size of the rectangle.
101    #[must_use]
102    pub fn size(self) -> Size {
103        Size::new(self.width, self.height)
104    }
105
106    /// Whether the rectangle covers no cell.
107    #[must_use]
108    pub fn is_empty(self) -> bool {
109        self.width == 0 || self.height == 0
110    }
111
112    /// Whether the cell at `(x, y)` lies inside.
113    #[must_use]
114    pub fn contains(self, x: i32, y: i32) -> bool {
115        x >= self.x && x < self.right() && y >= self.y && y < self.bottom()
116    }
117
118    /// The overlapping part of two rectangles; empty (at `self`'s origin) when they do not overlap.
119    #[must_use]
120    pub fn intersect(self, other: Self) -> Self {
121        let x = self.x.max(other.x);
122        let y = self.y.max(other.y);
123        let right = self.right().min(other.right());
124        let bottom = self.bottom().min(other.bottom());
125        if right <= x || bottom <= y {
126            return Self::new(self.x, self.y, 0, 0);
127        }
128        Self::new(x, y, clamp_u16(right - x), clamp_u16(bottom - y))
129    }
130
131    /// The rectangle shrunk by `padding`, never below zero size.
132    #[must_use]
133    pub fn inset(self, padding: Padding) -> Self {
134        Self::new(
135            self.x.saturating_add(i32::from(padding.left)),
136            self.y.saturating_add(i32::from(padding.top)),
137            self.width.saturating_sub(padding.horizontal()),
138            self.height.saturating_sub(padding.vertical()),
139        )
140    }
141
142    /// One row of the rectangle, relative to its top.
143    #[must_use]
144    pub fn row(self, offset: u16) -> Self {
145        Self::new(self.x, self.y.saturating_add(i32::from(offset)), self.width, u16::from(offset < self.height))
146    }
147
148    /// A rectangle of `size` centred inside this one, clamped to fit.
149    #[must_use]
150    pub fn centered(self, size: Size) -> Self {
151        let size = size.min(self.size());
152        Self::new(
153            self.x.saturating_add(i32::from((self.width - size.width) / 2)),
154            self.y.saturating_add(i32::from((self.height - size.height) / 2)),
155            size.width,
156            size.height,
157        )
158    }
159}
160
161/// Converts a non-negative `i32` cell count to `u16`, saturating.
162pub(crate) fn clamp_u16(value: i32) -> u16 {
163    u16::try_from(value.max(0)).unwrap_or(u16::MAX)
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn intersection_clips_and_handles_disjoint() {
172        let a = Rect::new(0, 0, 10, 5);
173        assert_eq!(a.intersect(Rect::new(5, -2, 10, 4)), Rect::new(5, 0, 5, 2));
174        assert!(a.intersect(Rect::new(20, 20, 3, 3)).is_empty());
175    }
176
177    #[test]
178    fn inset_never_underflows() {
179        let r = Rect::new(2, 3, 4, 2).inset(Padding::symmetric(1, 3));
180        assert_eq!(r, Rect::new(5, 4, 0, 0));
181        assert_eq!(Padding::all(2).horizontal(), 4);
182    }
183
184    #[test]
185    fn contains_uses_half_open_edges() {
186        let r = Rect::new(-1, -1, 2, 2);
187        assert!(r.contains(-1, -1) && r.contains(0, 0));
188        assert!(!r.contains(1, 0));
189    }
190
191    #[test]
192    fn edges_saturate_far_from_the_origin() {
193        let far = Rect::new(i32::MAX - 1, i32::MAX - 1, 10, 10);
194        assert_eq!((far.right(), far.bottom()), (i32::MAX, i32::MAX));
195        assert_eq!(far.inset(Padding::all(4)), Rect::new(i32::MAX, i32::MAX, 2, 2));
196        assert_eq!(far.row(3).y, i32::MAX);
197        assert!(far.contains(i32::MAX - 1, i32::MAX - 1));
198        assert_eq!(far.centered(Size::new(4, 4)).x, i32::MAX);
199    }
200
201    #[test]
202    fn centered_and_rows() {
203        let r = Rect::new(0, 0, 10, 6);
204        assert_eq!(r.centered(Size::new(4, 2)), Rect::new(3, 2, 4, 2));
205        assert_eq!(r.centered(Size::new(40, 2)), Rect::new(0, 2, 10, 2));
206        assert_eq!(r.row(2), Rect::new(0, 2, 10, 1));
207        assert_eq!(r.row(9).height, 0);
208    }
209}