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