Skip to main content

smelt_term/
geometry.rs

1//! Viewport geometry primitive shared by `grid` and `layout`.
2
3#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4pub struct Rect {
5    pub top: u16,
6    pub left: u16,
7    pub width: u16,
8    pub height: u16,
9}
10
11impl Rect {
12    pub fn new(top: u16, left: u16, width: u16, height: u16) -> Self {
13        Self {
14            top,
15            left,
16            width,
17            height,
18        }
19    }
20
21    pub fn bottom(&self) -> u16 {
22        self.top.saturating_add(self.height)
23    }
24
25    pub fn right(&self) -> u16 {
26        self.left.saturating_add(self.width)
27    }
28
29    pub fn contains(&self, row: u16, col: u16) -> bool {
30        row >= self.top && row < self.bottom() && col >= self.left && col < self.right()
31    }
32
33    pub fn contains_xy(&self, x: u16, y: u16) -> bool {
34        self.contains(y, x)
35    }
36
37    pub fn area(&self) -> u32 {
38        self.width as u32 * self.height as u32
39    }
40
41    pub fn is_empty(&self) -> bool {
42        self.width == 0 || self.height == 0
43    }
44
45    pub fn inset(self, amount: u16) -> Rect {
46        self.inset_by(Insets::uniform(amount))
47    }
48
49    pub fn inset_by(self, insets: Insets) -> Rect {
50        let dx = insets.left.saturating_add(insets.right);
51        let dy = insets.top.saturating_add(insets.bottom);
52        Rect::new(
53            self.top.saturating_add(insets.top),
54            self.left.saturating_add(insets.left),
55            self.width.saturating_sub(dx),
56            self.height.saturating_sub(dy),
57        )
58    }
59
60    pub fn translate(self, rows: i32, cols: i32) -> Rect {
61        Rect::new(
62            translate_u16(self.top, rows),
63            translate_u16(self.left, cols),
64            self.width,
65            self.height,
66        )
67    }
68
69    pub fn to_grid(self, origin: Rect) -> Rect {
70        Rect::new(
71            origin.top.saturating_add(self.top),
72            origin.left.saturating_add(self.left),
73            self.width,
74            self.height,
75        )
76    }
77
78    pub fn to_local(self, origin: Rect) -> Rect {
79        Rect::new(
80            self.top.saturating_sub(origin.top),
81            self.left.saturating_sub(origin.left),
82            self.width,
83            self.height,
84        )
85    }
86
87    pub fn intersection(self, other: Rect) -> Rect {
88        let top = self.top.max(other.top);
89        let left = self.left.max(other.left);
90        let bottom = self.bottom().min(other.bottom());
91        let right = self.right().min(other.right());
92        Rect::new(
93            top.min(bottom),
94            left.min(right),
95            right.saturating_sub(left),
96            bottom.saturating_sub(top),
97        )
98    }
99
100    pub fn clip_to(self, bounds: Rect) -> Rect {
101        self.intersection(bounds)
102    }
103
104    pub fn split_top(self, height: u16) -> (Rect, Rect) {
105        let top_height = height.min(self.height);
106        let top = Rect::new(self.top, self.left, self.width, top_height);
107        let rest = Rect::new(
108            self.top.saturating_add(top_height),
109            self.left,
110            self.width,
111            self.height.saturating_sub(top_height),
112        );
113        (top, rest)
114    }
115
116    pub fn split_bottom(self, height: u16) -> (Rect, Rect) {
117        let bottom_height = height.min(self.height);
118        let rest_height = self.height.saturating_sub(bottom_height);
119        let rest = Rect::new(self.top, self.left, self.width, rest_height);
120        let bottom = Rect::new(
121            self.top.saturating_add(rest_height),
122            self.left,
123            self.width,
124            bottom_height,
125        );
126        (rest, bottom)
127    }
128
129    pub fn split_left(self, width: u16) -> (Rect, Rect) {
130        let left_width = width.min(self.width);
131        let left = Rect::new(self.top, self.left, left_width, self.height);
132        let rest = Rect::new(
133            self.top,
134            self.left.saturating_add(left_width),
135            self.width.saturating_sub(left_width),
136            self.height,
137        );
138        (left, rest)
139    }
140
141    pub fn split_right(self, width: u16) -> (Rect, Rect) {
142        let right_width = width.min(self.width);
143        let rest_width = self.width.saturating_sub(right_width);
144        let rest = Rect::new(self.top, self.left, rest_width, self.height);
145        let right = Rect::new(
146            self.top,
147            self.left.saturating_add(rest_width),
148            right_width,
149            self.height,
150        );
151        (rest, right)
152    }
153
154    pub fn split_y(self, top_height: u16, gap: u16) -> (Rect, Rect) {
155        let top_height = top_height.min(self.height);
156        let bottom_top = self.top.saturating_add(top_height).saturating_add(gap);
157        let used = top_height.saturating_add(gap).min(self.height);
158        let top = Rect::new(self.top, self.left, self.width, top_height);
159        let bottom = Rect::new(
160            bottom_top.min(self.bottom()),
161            self.left,
162            self.width,
163            self.height.saturating_sub(used),
164        );
165        (top, bottom)
166    }
167
168    pub fn split_x(self, left_width: u16, gap: u16) -> (Rect, Rect) {
169        let left_width = left_width.min(self.width);
170        let right_left = self.left.saturating_add(left_width).saturating_add(gap);
171        let used = left_width.saturating_add(gap).min(self.width);
172        let left = Rect::new(self.top, self.left, left_width, self.height);
173        let right = Rect::new(
174            self.top,
175            right_left.min(self.right()),
176            self.width.saturating_sub(used),
177            self.height,
178        );
179        (left, right)
180    }
181
182    pub fn centered(self, width: u16, height: u16) -> Rect {
183        let width = width.min(self.width);
184        let height = height.min(self.height);
185        Rect::new(
186            self.top.saturating_add((self.height - height) / 2),
187            self.left.saturating_add((self.width - width) / 2),
188            width,
189            height,
190        )
191    }
192}
193
194#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
195pub struct Insets {
196    pub top: u16,
197    pub right: u16,
198    pub bottom: u16,
199    pub left: u16,
200}
201
202impl Insets {
203    pub fn new(top: u16, right: u16, bottom: u16, left: u16) -> Self {
204        Self {
205            top,
206            right,
207            bottom,
208            left,
209        }
210    }
211
212    pub fn uniform(amount: u16) -> Self {
213        Self::new(amount, amount, amount, amount)
214    }
215}
216
217fn translate_u16(value: u16, delta: i32) -> u16 {
218    if delta.is_negative() {
219        value.saturating_sub(delta.unsigned_abs().min(u16::MAX as u32) as u16)
220    } else {
221        value.saturating_add(delta.min(u16::MAX as i32) as u16)
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn bottom_is_top_plus_height() {
231        let r = Rect::new(3, 0, 10, 7);
232        assert_eq!(r.bottom(), 10);
233    }
234
235    #[test]
236    fn right_is_left_plus_width() {
237        let r = Rect::new(0, 5, 12, 4);
238        assert_eq!(r.right(), 17);
239    }
240
241    #[test]
242    fn area_multiplies_width_by_height() {
243        assert_eq!(Rect::new(0, 0, 80, 24).area(), 1920);
244        assert_eq!(Rect::new(0, 0, 0, 99).area(), 0);
245    }
246
247    #[test]
248    fn contains_includes_top_left_excludes_bottom_right() {
249        // Half-open ranges: [top, bottom) × [left, right). The contains
250        // signature is (row, col), not (x, y).
251        let r = Rect::new(5, 10, 4, 3); // rows 5..8, cols 10..14
252        assert!(r.contains(5, 10), "top-left corner should be inside");
253        assert!(r.contains(7, 13), "last inclusive cell should be inside");
254        assert!(!r.contains(8, 10), "bottom edge is exclusive");
255        assert!(!r.contains(5, 14), "right edge is exclusive");
256    }
257
258    #[test]
259    fn zero_sized_rect_contains_nothing() {
260        let r = Rect::new(5, 5, 0, 0);
261        assert!(!r.contains(5, 5));
262    }
263
264    #[test]
265    fn inset_clamps_tiny_rects() {
266        assert_eq!(Rect::new(1, 2, 3, 2).inset(2), Rect::new(3, 4, 0, 0));
267        assert_eq!(
268            Rect::new(0, 0, 10, 8).inset_by(Insets::new(1, 2, 3, 4)),
269            Rect::new(1, 4, 4, 4)
270        );
271    }
272
273    #[test]
274    fn splits_with_gap_do_not_underflow() {
275        let r = Rect::new(2, 3, 5, 4);
276        assert_eq!(
277            r.split_y(3, 10),
278            (Rect::new(2, 3, 5, 3), Rect::new(6, 3, 5, 0))
279        );
280        assert_eq!(
281            r.split_x(4, 10),
282            (Rect::new(2, 3, 4, 4), Rect::new(2, 8, 0, 4))
283        );
284    }
285
286    #[test]
287    fn centered_clamps_oversized_child() {
288        let r = Rect::new(10, 20, 6, 4);
289        assert_eq!(r.centered(2, 2), Rect::new(11, 22, 2, 2));
290        assert_eq!(r.centered(99, 99), r);
291        assert_eq!(
292            Rect::new(u16::MAX - 1, u16::MAX - 1, 4, 4)
293                .centered(2, 2)
294                .top,
295            u16::MAX
296        );
297    }
298
299    #[test]
300    fn intersection_returns_overlap_or_empty_rect() {
301        assert_eq!(
302            Rect::new(2, 3, 10, 8).intersection(Rect::new(5, 1, 6, 4)),
303            Rect::new(5, 3, 4, 4)
304        );
305        assert_eq!(
306            Rect::new(0, 0, 2, 2).intersection(Rect::new(5, 5, 2, 2)),
307            Rect::new(2, 2, 0, 0)
308        );
309    }
310
311    #[test]
312    fn translate_saturates_at_zero() {
313        assert_eq!(
314            Rect::new(2, 3, 4, 5).translate(-10, -1),
315            Rect::new(0, 2, 4, 5)
316        );
317        assert_eq!(
318            Rect::new(60_000, 3, 4, 5).translate(-60_000, 4),
319            Rect::new(0, 7, 4, 5)
320        );
321        assert_eq!(Rect::new(2, 3, 4, 5).translate(2, 4), Rect::new(4, 7, 4, 5));
322    }
323
324    #[test]
325    fn grid_and_local_convert_coordinate_spaces() {
326        let origin = Rect::new(10, 20, 30, 40);
327        let local = Rect::new(2, 3, 4, 5);
328        let grid = Rect::new(12, 23, 4, 5);
329        assert_eq!(local.to_grid(origin), grid);
330        assert_eq!(grid.to_local(origin), local);
331    }
332}