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