Skip to main content

louie/core/
rect.rs

1use serde::{Deserialize, Serialize};
2use std::ops::{Add, Sub};
3
4/// A rectangular area in terminal coordinates.
5///
6/// All positions are zero-indexed. The top-left of the terminal is (0, 0).
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
8pub struct Rect {
9    pub x: u16,
10    pub y: u16,
11    pub width: u16,
12    pub height: u16,
13}
14
15impl Rect {
16    pub const ZERO: Rect = Rect {
17        x: 0,
18        y: 0,
19        width: 0,
20        height: 0,
21    };
22
23    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
24        Self {
25            x,
26            y,
27            width,
28            height,
29        }
30    }
31
32    /// Total number of cells in this rectangle.
33    pub const fn area(&self) -> u32 {
34        self.width as u32 * self.height as u32
35    }
36
37    /// Whether this rectangle has zero area.
38    pub const fn is_empty(&self) -> bool {
39        self.width == 0 || self.height == 0
40    }
41
42    /// The leftmost column.
43    pub const fn left(&self) -> u16 {
44        self.x
45    }
46
47    /// The rightmost column (exclusive).
48    pub const fn right(&self) -> u16 {
49        self.x.saturating_add(self.width)
50    }
51
52    /// The topmost row.
53    pub const fn top(&self) -> u16 {
54        self.y
55    }
56
57    /// The bottommost row (exclusive).
58    pub const fn bottom(&self) -> u16 {
59        self.y.saturating_add(self.height)
60    }
61
62    /// The center position.
63    pub const fn center(&self) -> Position {
64        Position {
65            x: self.x.saturating_add(self.width / 2),
66            y: self.y.saturating_add(self.height / 2),
67        }
68    }
69
70    /// Returns the intersection of two rectangles.
71    pub fn intersection(&self, other: Rect) -> Rect {
72        let x = self.x.max(other.x);
73        let y = self.y.max(other.y);
74        let right = self.right().min(other.right());
75        let bottom = self.bottom().min(other.bottom());
76        Rect {
77            x,
78            y,
79            width: right.saturating_sub(x),
80            height: bottom.saturating_sub(y),
81        }
82    }
83
84    /// Returns the smallest rectangle that contains both.
85    pub fn union(&self, other: Rect) -> Rect {
86        let x = self.x.min(other.x);
87        let y = self.y.min(other.y);
88        let right = self.right().max(other.right());
89        let bottom = self.bottom().max(other.bottom());
90        Rect {
91            x,
92            y,
93            width: right.saturating_sub(x),
94            height: bottom.saturating_sub(y),
95        }
96    }
97
98    /// Whether this rectangle contains a position.
99    pub fn contains(&self, pos: Position) -> bool {
100        pos.x >= self.x && pos.x < self.right() && pos.y >= self.y && pos.y < self.bottom()
101    }
102
103    /// Create a new rect with uniform inner margin applied.
104    pub fn inner(&self, margin: Margin) -> Rect {
105        let x = self.x.saturating_add(margin.left);
106        let y = self.y.saturating_add(margin.top);
107        let width = self
108            .width
109            .saturating_sub(margin.left.saturating_add(margin.right));
110        let height = self
111            .height
112            .saturating_sub(margin.top.saturating_add(margin.bottom));
113        Rect {
114            x,
115            y,
116            width,
117            height,
118        }
119    }
120
121    /// Iterator over all positions in this rectangle, row by row.
122    pub fn positions(&self) -> impl Iterator<Item = Position> + '_ {
123        (self.y..self.bottom())
124            .flat_map(move |y| (self.x..self.right()).map(move |x| Position { x, y }))
125    }
126
127    /// Iterator over row indices.
128    pub fn rows(&self) -> impl Iterator<Item = u16> {
129        self.y..self.bottom()
130    }
131
132    /// Iterator over column indices.
133    pub fn columns(&self) -> impl Iterator<Item = u16> {
134        self.x..self.right()
135    }
136}
137
138/// A position in terminal coordinates.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
140pub struct Position {
141    pub x: u16,
142    pub y: u16,
143}
144
145impl Position {
146    pub const fn new(x: u16, y: u16) -> Self {
147        Self { x, y }
148    }
149}
150
151impl Add<Offset> for Position {
152    type Output = Position;
153    fn add(self, rhs: Offset) -> Self::Output {
154        Position {
155            x: (self.x as i32 + rhs.x as i32).max(0) as u16,
156            y: (self.y as i32 + rhs.y as i32).max(0) as u16,
157        }
158    }
159}
160
161impl Sub for Position {
162    type Output = Offset;
163    fn sub(self, rhs: Self) -> Self::Output {
164        Offset {
165            x: self.x as i16 - rhs.x as i16,
166            y: self.y as i16 - rhs.y as i16,
167        }
168    }
169}
170
171/// A displacement in terminal coordinates.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
173pub struct Offset {
174    pub x: i16,
175    pub y: i16,
176}
177
178/// Margin/padding specification.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
180pub struct Margin {
181    pub top: u16,
182    pub right: u16,
183    pub bottom: u16,
184    pub left: u16,
185}
186
187impl Margin {
188    pub const ZERO: Margin = Margin {
189        top: 0,
190        right: 0,
191        bottom: 0,
192        left: 0,
193    };
194
195    /// Uniform margin on all sides.
196    pub const fn uniform(value: u16) -> Self {
197        Self {
198            top: value,
199            right: value,
200            bottom: value,
201            left: value,
202        }
203    }
204
205    /// Symmetric margin (vertical, horizontal).
206    pub const fn symmetric(vertical: u16, horizontal: u16) -> Self {
207        Self {
208            top: vertical,
209            right: horizontal,
210            bottom: vertical,
211            left: horizontal,
212        }
213    }
214
215    /// Individual sides.
216    pub const fn new(top: u16, right: u16, bottom: u16, left: u16) -> Self {
217        Self {
218            top,
219            right,
220            bottom,
221            left,
222        }
223    }
224}
225
226/// Size in columns and rows.
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
228pub struct Size {
229    pub width: u16,
230    pub height: u16,
231}
232
233impl Size {
234    pub const fn new(width: u16, height: u16) -> Self {
235        Self { width, height }
236    }
237}
238
239impl From<Rect> for Size {
240    fn from(rect: Rect) -> Self {
241        Size {
242            width: rect.width,
243            height: rect.height,
244        }
245    }
246}