Skip to main content

photon_ui/layout/
rect.rs

1use std::{
2    cmp::{
3        max,
4        min,
5    },
6    fmt,
7};
8
9use super::{
10    Margin,
11    Offset,
12    Position,
13    Size,
14};
15
16/// A rectangular area in the terminal.
17#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
18pub struct Rect {
19    pub x: u16,
20    pub y: u16,
21    pub width: u16,
22    pub height: u16,
23}
24
25impl Rect {
26    pub const MAX: Self = Self::new(0, 0, u16::MAX, u16::MAX);
27    pub const MIN: Self = Self::ZERO;
28    pub const ZERO: Self = Self::new(0, 0, 0, 0);
29
30    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
31        let width = x.saturating_add(width).saturating_sub(x);
32        let height = y.saturating_add(height).saturating_sub(y);
33        Self {
34            x,
35            y,
36            width,
37            height,
38        }
39    }
40
41    pub const fn area(self) -> u32 {
42        self.width as u32 * self.height as u32
43    }
44
45    pub const fn is_empty(self) -> bool {
46        self.width == 0 || self.height == 0
47    }
48
49    pub const fn left(self) -> u16 {
50        self.x
51    }
52
53    pub const fn right(self) -> u16 {
54        self.x.saturating_add(self.width)
55    }
56
57    pub const fn top(self) -> u16 {
58        self.y
59    }
60
61    pub const fn bottom(self) -> u16 {
62        self.y.saturating_add(self.height)
63    }
64
65    pub const fn row(self) -> u16 {
66        self.y
67    }
68
69    pub const fn col(self) -> u16 {
70        self.x
71    }
72
73    pub const fn inner(self, margin: Margin) -> Self {
74        let doubled_h = margin.horizontal.saturating_mul(2);
75        let doubled_v = margin.vertical.saturating_mul(2);
76        if self.width < doubled_h || self.height < doubled_v {
77            Self::ZERO
78        } else {
79            Self {
80                x: self.x.saturating_add(margin.horizontal),
81                y: self.y.saturating_add(margin.vertical),
82                width: self.width.saturating_sub(doubled_h),
83                height: self.height.saturating_sub(doubled_v),
84            }
85        }
86    }
87
88    pub const fn outer(self, margin: Margin) -> Self {
89        let x = self.x.saturating_sub(margin.horizontal);
90        let y = self.y.saturating_sub(margin.vertical);
91        let width = self
92            .right()
93            .saturating_add(margin.horizontal)
94            .saturating_sub(x);
95        let height = self
96            .bottom()
97            .saturating_add(margin.vertical)
98            .saturating_sub(y);
99        Self {
100            x,
101            y,
102            width,
103            height,
104        }
105    }
106
107    pub fn offset(self, offset: Offset) -> Self {
108        self + offset
109    }
110
111    pub const fn resize(self, size: Size) -> Self {
112        Self {
113            width: self.x.saturating_add(size.width).saturating_sub(self.x),
114            height: self.y.saturating_add(size.height).saturating_sub(self.y),
115            ..self
116        }
117    }
118
119    pub fn union(self, other: Self) -> Self {
120        let x1 = min(self.x, other.x);
121        let y1 = min(self.y, other.y);
122        let x2 = max(self.right(), other.right());
123        let y2 = max(self.bottom(), other.bottom());
124        Self {
125            x: x1,
126            y: y1,
127            width: x2.saturating_sub(x1),
128            height: y2.saturating_sub(y1),
129        }
130    }
131
132    pub fn intersection(self, other: Self) -> Self {
133        let x1 = max(self.x, other.x);
134        let y1 = max(self.y, other.y);
135        let x2 = min(self.right(), other.right());
136        let y2 = min(self.bottom(), other.bottom());
137        Self {
138            x: x1,
139            y: y1,
140            width: x2.saturating_sub(x1),
141            height: y2.saturating_sub(y1),
142        }
143    }
144
145    pub const fn intersects(self, other: Self) -> bool {
146        self.x < other.right() &&
147            self.right() > other.x &&
148            self.y < other.bottom() &&
149            self.bottom() > other.y
150    }
151
152    pub const fn contains(self, position: Position) -> bool {
153        position.x >= self.x &&
154            position.x < self.right() &&
155            position.y >= self.y &&
156            position.y < self.bottom()
157    }
158
159    pub fn clamp(self, other: Self) -> Self {
160        let width = self.width.min(other.width);
161        let height = self.height.min(other.height);
162        let x = self.x.clamp(other.x, other.right().saturating_sub(width));
163        let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
164        Self::new(x, y, width, height)
165    }
166
167    pub const fn rows(self) -> Rows {
168        Rows::new(self)
169    }
170
171    pub const fn columns(self) -> Columns {
172        Columns::new(self)
173    }
174
175    pub const fn positions(self) -> Positions {
176        Positions::new(self)
177    }
178
179    pub const fn as_position(self) -> Position {
180        Position {
181            x: self.x,
182            y: self.y,
183        }
184    }
185
186    pub const fn as_size(self) -> Size {
187        Size {
188            width: self.width,
189            height: self.height,
190        }
191    }
192}
193
194impl fmt::Display for Rect {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
197    }
198}
199
200impl From<(Position, Size)> for Rect {
201    fn from((position, size): (Position, Size)) -> Self {
202        Self {
203            x: position.x,
204            y: position.y,
205            width: size.width,
206            height: size.height,
207        }
208    }
209}
210
211impl From<Size> for Rect {
212    fn from(size: Size) -> Self {
213        Self {
214            x: 0,
215            y: 0,
216            width: size.width,
217            height: size.height,
218        }
219    }
220}
221
222impl std::ops::Add<Offset> for Rect {
223    type Output = Self;
224
225    fn add(self, offset: Offset) -> Self::Output {
226        let max = i32::from(u16::MAX);
227        let x = i32::from(self.x)
228            .saturating_add(i32::from(offset.x))
229            .clamp(0, max) as u16;
230        let y = i32::from(self.y)
231            .saturating_add(i32::from(offset.y))
232            .clamp(0, max) as u16;
233        Self { x, y, ..self }
234    }
235}
236
237impl std::ops::Sub<Offset> for Rect {
238    type Output = Self;
239
240    fn sub(self, offset: Offset) -> Self::Output {
241        let max = i32::from(u16::MAX);
242        let x = i32::from(self.x)
243            .saturating_sub(i32::from(offset.x))
244            .clamp(0, max) as u16;
245        let y = i32::from(self.y)
246            .saturating_sub(i32::from(offset.y))
247            .clamp(0, max) as u16;
248        Self { x, y, ..self }
249    }
250}
251
252/// Iterator over rows within a Rect.
253#[derive(Debug, Clone)]
254pub struct Rows {
255    rect: Rect,
256    current: u16,
257}
258
259impl Rows {
260    pub const fn new(rect: Rect) -> Self {
261        Self { rect, current: 0 }
262    }
263}
264
265impl Iterator for Rows {
266    type Item = Rect;
267
268    fn next(&mut self) -> Option<Self::Item> {
269        if self.current >= self.rect.height {
270            return None;
271        }
272        let row = Rect {
273            x: self.rect.x,
274            y: self.rect.y + self.current,
275            width: self.rect.width,
276            height: 1,
277        };
278        self.current += 1;
279        Some(row)
280    }
281}
282
283/// Iterator over columns within a Rect.
284#[derive(Debug, Clone)]
285pub struct Columns {
286    rect: Rect,
287    current: u16,
288}
289
290impl Columns {
291    pub const fn new(rect: Rect) -> Self {
292        Self { rect, current: 0 }
293    }
294}
295
296impl Iterator for Columns {
297    type Item = Rect;
298
299    fn next(&mut self) -> Option<Self::Item> {
300        if self.current >= self.rect.width {
301            return None;
302        }
303        let col = Rect {
304            x: self.rect.x + self.current,
305            y: self.rect.y,
306            width: 1,
307            height: self.rect.height,
308        };
309        self.current += 1;
310        Some(col)
311    }
312}
313
314/// Iterator over all positions within a Rect.
315#[derive(Debug, Clone)]
316pub struct Positions {
317    rect: Rect,
318    current: u16,
319}
320
321impl Positions {
322    pub const fn new(rect: Rect) -> Self {
323        Self { rect, current: 0 }
324    }
325}
326
327impl Iterator for Positions {
328    type Item = Position;
329
330    fn next(&mut self) -> Option<Self::Item> {
331        let area = self.rect.area();
332        if self.current as u32 >= area {
333            return None;
334        }
335        let x = self.rect.x + (self.current % self.rect.width);
336        let y = self.rect.y + (self.current / self.rect.width);
337        self.current += 1;
338        Some(Position { x, y })
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn rect_new() {
348        let r = Rect::new(1, 2, 3, 4);
349        assert_eq!(r.x, 1);
350        assert_eq!(r.y, 2);
351        assert_eq!(r.width, 3);
352        assert_eq!(r.height, 4);
353    }
354
355    #[test]
356    fn rect_new_clamps_overflow() {
357        let r = Rect::new(u16::MAX - 5, u16::MAX - 3, 100, 100);
358        assert_eq!(r.width, 5);
359        assert_eq!(r.height, 3);
360    }
361
362    #[test]
363    fn rect_area() {
364        assert_eq!(Rect::new(0, 0, 3, 4).area(), 12);
365        assert_eq!(Rect::ZERO.area(), 0);
366    }
367
368    #[test]
369    fn rect_is_empty() {
370        assert!(Rect::new(0, 0, 0, 5).is_empty());
371        assert!(Rect::new(0, 0, 5, 0).is_empty());
372        assert!(!Rect::new(0, 0, 1, 1).is_empty());
373    }
374
375    #[test]
376    fn rect_edges() {
377        let r = Rect::new(1, 2, 3, 4);
378        assert_eq!(r.left(), 1);
379        assert_eq!(r.right(), 4);
380        assert_eq!(r.top(), 2);
381        assert_eq!(r.bottom(), 6);
382    }
383
384    #[test]
385    fn rect_row_col_compat() {
386        let r = Rect::new(5, 10, 1, 1);
387        assert_eq!(r.row(), 10);
388        assert_eq!(r.col(), 5);
389    }
390
391    #[test]
392    fn rect_inner() {
393        let r = Rect::new(0, 0, 10, 10).inner(Margin::new(2, 3));
394        assert_eq!(r, Rect::new(2, 3, 6, 4));
395    }
396
397    #[test]
398    fn rect_inner_zero_when_margin_too_large() {
399        let r = Rect::new(0, 0, 3, 3).inner(Margin::new(2, 2));
400        assert_eq!(r, Rect::ZERO);
401    }
402
403    #[test]
404    fn rect_outer() {
405        let r = Rect::new(10, 20, 5, 5).outer(Margin::new(2, 3));
406        assert_eq!(r, Rect::new(8, 17, 9, 11));
407    }
408
409    #[test]
410    fn rect_outer_saturates() {
411        let r = Rect::new(0, 0, 5, 5).outer(Margin::new(10, 10));
412        assert_eq!(r.x, 0);
413        assert_eq!(r.y, 0);
414    }
415
416    #[test]
417    fn rect_offset() {
418        let r = Rect::new(5, 5, 10, 10).offset(Offset::new(3, -2));
419        assert_eq!(r, Rect::new(8, 3, 10, 10));
420    }
421
422    #[test]
423    fn rect_offset_clamps() {
424        let r = Rect::new(0, 0, 1, 1).offset(Offset::new(-5, -5));
425        assert_eq!(r, Rect::new(0, 0, 1, 1));
426    }
427
428    #[test]
429    fn rect_resize() {
430        let r = Rect::new(1, 1, 5, 5).resize(Size::new(3, 3));
431        assert_eq!(r, Rect::new(1, 1, 3, 3));
432    }
433
434    #[test]
435    fn rect_resize_clamps() {
436        let r = Rect::new(u16::MAX - 2, u16::MAX - 1, 1, 1).resize(Size::new(10, 10));
437        assert_eq!(r.width, 2);
438        assert_eq!(r.height, 1);
439    }
440
441    #[test]
442    fn rect_union() {
443        let a = Rect::new(0, 0, 5, 5);
444        let b = Rect::new(3, 3, 5, 5);
445        assert_eq!(a.union(b), Rect::new(0, 0, 8, 8));
446    }
447
448    #[test]
449    fn rect_intersection() {
450        let a = Rect::new(0, 0, 5, 5);
451        let b = Rect::new(3, 3, 5, 5);
452        assert_eq!(a.intersection(b), Rect::new(3, 3, 2, 2));
453    }
454
455    #[test]
456    fn rect_intersection_no_overlap() {
457        let a = Rect::new(0, 0, 2, 2);
458        let b = Rect::new(5, 5, 2, 2);
459        assert_eq!(a.intersection(b), Rect::new(5, 5, 0, 0));
460    }
461
462    #[test]
463    fn rect_intersects() {
464        assert!(Rect::new(0, 0, 5, 5).intersects(Rect::new(3, 3, 5, 5)));
465        assert!(!Rect::new(0, 0, 2, 2).intersects(Rect::new(5, 5, 2, 2)));
466    }
467
468    #[test]
469    fn rect_contains() {
470        let r = Rect::new(1, 1, 3, 3);
471        assert!(r.contains(Position::new(1, 1)));
472        assert!(r.contains(Position::new(3, 3)));
473        assert!(!r.contains(Position::new(0, 1)));
474        assert!(!r.contains(Position::new(4, 4)));
475    }
476
477    #[test]
478    fn rect_clamp() {
479        let area = Rect::new(0, 0, 100, 100);
480        let r = Rect::new(80, 80, 30, 30).clamp(area);
481        assert_eq!(r, Rect::new(70, 70, 30, 30));
482    }
483
484    #[test]
485    fn rect_rows() {
486        let rows: Vec<_> = Rect::new(0, 0, 3, 2).rows().collect();
487        assert_eq!(rows, vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)]);
488    }
489
490    #[test]
491    fn rect_columns() {
492        let cols: Vec<_> = Rect::new(0, 0, 2, 3).columns().collect();
493        assert_eq!(cols, vec![Rect::new(0, 0, 1, 3), Rect::new(1, 0, 1, 3)]);
494    }
495
496    #[test]
497    fn rect_positions() {
498        let positions: Vec<_> = Rect::new(1, 1, 2, 2).positions().collect();
499        assert_eq!(
500            positions,
501            vec![
502                Position::new(1, 1),
503                Position::new(2, 1),
504                Position::new(1, 2),
505                Position::new(2, 2),
506            ]
507        );
508    }
509
510    #[test]
511    fn rect_as_position() {
512        assert_eq!(Rect::new(5, 10, 1, 1).as_position(), Position::new(5, 10));
513    }
514
515    #[test]
516    fn rect_as_size() {
517        assert_eq!(Rect::new(0, 0, 5, 7).as_size(), Size::new(5, 7));
518    }
519
520    #[test]
521    fn rect_display() {
522        assert_eq!(Rect::new(1, 2, 3, 4).to_string(), "3x4+1+2");
523    }
524
525    #[test]
526    fn rect_from_position_and_size() {
527        let p = Position::new(1, 2);
528        let s = Size::new(3, 4);
529        let r: Rect = (p, s).into();
530        assert_eq!(r, Rect::new(1, 2, 3, 4));
531    }
532
533    #[test]
534    fn rect_from_size() {
535        let r: Rect = Size::new(5, 7).into();
536        assert_eq!(r, Rect::new(0, 0, 5, 7));
537    }
538
539    #[test]
540    fn rect_add_offset() {
541        let r = Rect::new(1, 2, 3, 4) + Offset::new(5, -1);
542        assert_eq!(r, Rect::new(6, 1, 3, 4));
543    }
544
545    #[test]
546    fn rect_sub_offset() {
547        let r = Rect::new(5, 5, 3, 4) - Offset::new(2, 3);
548        assert_eq!(r, Rect::new(3, 2, 3, 4));
549    }
550}