tuxtui_core/
geometry.rs

1//! Geometric primitives for terminal layout.
2
3use core::fmt;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8/// A position in the terminal grid.
9#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
10#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11pub struct Position {
12    /// X coordinate (column)
13    pub x: u16,
14    /// Y coordinate (row)
15    pub y: u16,
16}
17
18impl Position {
19    /// Create a new position.
20    #[inline]
21    #[must_use]
22    pub const fn new(x: u16, y: u16) -> Self {
23        Self { x, y }
24    }
25
26    /// Calculate the distance to another position.
27    #[inline]
28    #[must_use]
29    pub fn distance_to(self, other: Self) -> f64 {
30        let dx = (other.x as f64) - (self.x as f64);
31        let dy = (other.y as f64) - (self.y as f64);
32        (dx * dx + dy * dy).sqrt()
33    }
34}
35
36impl core::ops::Add for Position {
37    type Output = Self;
38
39    fn add(self, rhs: Self) -> Self::Output {
40        Self {
41            x: self.x.saturating_add(rhs.x),
42            y: self.y.saturating_add(rhs.y),
43        }
44    }
45}
46
47impl core::ops::Sub for Position {
48    type Output = Self;
49
50    fn sub(self, rhs: Self) -> Self::Output {
51        Self {
52            x: self.x.saturating_sub(rhs.x),
53            y: self.y.saturating_sub(rhs.y),
54        }
55    }
56}
57
58impl core::ops::AddAssign for Position {
59    fn add_assign(&mut self, rhs: Self) {
60        self.x = self.x.saturating_add(rhs.x);
61        self.y = self.y.saturating_add(rhs.y);
62    }
63}
64
65impl core::ops::SubAssign for Position {
66    fn sub_assign(&mut self, rhs: Self) {
67        self.x = self.x.saturating_sub(rhs.x);
68        self.y = self.y.saturating_sub(rhs.y);
69    }
70}
71
72/// A rectangular region in the terminal.
73///
74/// Represents a region with position (x, y) and dimensions (width, height).
75/// All coordinates use zero-based indexing.
76#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
77#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
78pub struct Rect {
79    /// X coordinate (column) of the top-left corner
80    pub x: u16,
81    /// Y coordinate (row) of the top-left corner
82    pub y: u16,
83    /// Width in columns
84    pub width: u16,
85    /// Height in rows
86    pub height: u16,
87}
88
89impl Rect {
90    /// Create a new rectangle.
91    ///
92    /// # Example
93    ///
94    /// ```
95    /// use tuxtui_core::geometry::Rect;
96    ///
97    /// let rect = Rect::new(0, 0, 80, 24);
98    /// assert_eq!(rect.width, 80);
99    /// assert_eq!(rect.height, 24);
100    /// ```
101    #[inline]
102    #[must_use]
103    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
104        Self {
105            x,
106            y,
107            width,
108            height,
109        }
110    }
111
112    /// Create a zero-sized rectangle at the origin.
113    #[inline]
114    #[must_use]
115    pub const fn zero() -> Self {
116        Self::new(0, 0, 0, 0)
117    }
118
119    /// Get the area (width × height) of the rectangle.
120    ///
121    /// # Example
122    ///
123    /// ```
124    /// use tuxtui_core::geometry::Rect;
125    ///
126    /// let rect = Rect::new(0, 0, 10, 5);
127    /// assert_eq!(rect.area(), 50);
128    /// ```
129    #[inline]
130    #[must_use]
131    pub const fn area(self) -> u32 {
132        self.width as u32 * self.height as u32
133    }
134
135    /// Check if the rectangle is empty (zero width or height).
136    #[inline]
137    #[must_use]
138    pub const fn is_empty(self) -> bool {
139        self.width == 0 || self.height == 0
140    }
141
142    /// Get the left edge x coordinate.
143    #[inline]
144    #[must_use]
145    pub const fn left(self) -> u16 {
146        self.x
147    }
148
149    /// Get the right edge x coordinate (exclusive).
150    #[inline]
151    #[must_use]
152    pub const fn right(self) -> u16 {
153        self.x.saturating_add(self.width)
154    }
155
156    /// Get the top edge y coordinate.
157    #[inline]
158    #[must_use]
159    pub const fn top(self) -> u16 {
160        self.y
161    }
162
163    /// Get the bottom edge y coordinate (exclusive).
164    #[inline]
165    #[must_use]
166    pub const fn bottom(self) -> u16 {
167        self.y.saturating_add(self.height)
168    }
169
170    /// Get the position of the top-left corner.
171    #[inline]
172    #[must_use]
173    pub const fn position(self) -> Position {
174        Position::new(self.x, self.y)
175    }
176
177    /// Check if this rectangle contains a position.
178    ///
179    /// # Example
180    ///
181    /// ```
182    /// use tuxtui_core::geometry::{Rect, Position};
183    ///
184    /// let rect = Rect::new(0, 0, 10, 10);
185    /// assert!(rect.contains(Position::new(5, 5)));
186    /// assert!(!rect.contains(Position::new(15, 5)));
187    /// ```
188    #[must_use]
189    pub const fn contains(self, pos: Position) -> bool {
190        pos.x >= self.x && pos.x < self.right() && pos.y >= self.y && pos.y < self.bottom()
191    }
192
193    /// Compute the intersection of two rectangles.
194    ///
195    /// # Example
196    ///
197    /// ```
198    /// use tuxtui_core::geometry::Rect;
199    ///
200    /// let a = Rect::new(0, 0, 10, 10);
201    /// let b = Rect::new(5, 5, 10, 10);
202    /// let intersection = a.intersection(b);
203    /// assert_eq!(intersection, Rect::new(5, 5, 5, 5));
204    /// ```
205    #[must_use]
206    pub const fn intersection(self, other: Self) -> Self {
207        let x1 = if self.x > other.x { self.x } else { other.x };
208        let y1 = if self.y > other.y { self.y } else { other.y };
209        let x2 = if self.right() < other.right() {
210            self.right()
211        } else {
212            other.right()
213        };
214        let y2 = if self.bottom() < other.bottom() {
215            self.bottom()
216        } else {
217            other.bottom()
218        };
219        Self::new(x1, y1, x2.saturating_sub(x1), y2.saturating_sub(y1))
220    }
221
222    /// Check if this rectangle fully contains another rectangle.
223    ///
224    /// # Example
225    ///
226    /// ```
227    /// use tuxtui_core::geometry::Rect;
228    ///
229    /// let outer = Rect::new(0, 0, 10, 10);
230    /// let inner = Rect::new(2, 2, 5, 5);
231    /// assert!(outer.contains_rect(inner));
232    /// ```
233    #[must_use]
234    pub const fn contains_rect(self, other: Self) -> bool {
235        other.x >= self.x
236            && other.y >= self.y
237            && other.right() <= self.right()
238            && other.bottom() <= self.bottom()
239    }
240
241    /// Compute the union of two rectangles.
242    #[must_use]
243    pub const fn union(self, other: Self) -> Self {
244        let x1 = if self.x < other.x { self.x } else { other.x };
245        let y1 = if self.y < other.y { self.y } else { other.y };
246        let x2 = if self.right() > other.right() {
247            self.right()
248        } else {
249            other.right()
250        };
251        let y2 = if self.bottom() > other.bottom() {
252            self.bottom()
253        } else {
254            other.bottom()
255        };
256        Self::new(x1, y1, x2 - x1, y2 - y1)
257    }
258
259    /// Apply a margin (padding) inset to the rectangle.
260    ///
261    /// # Example
262    ///
263    /// ```
264    /// use tuxtui_core::geometry::{Rect, Margin};
265    ///
266    /// let rect = Rect::new(0, 0, 10, 10);
267    /// let inner = rect.inner(Margin::new(1, 1));
268    /// assert_eq!(inner, Rect::new(1, 1, 8, 8));
269    /// ```
270    #[must_use]
271    pub const fn inner(self, margin: Margin) -> Self {
272        let doubled_horizontal = margin.horizontal.saturating_mul(2);
273        let doubled_vertical = margin.vertical.saturating_mul(2);
274        Self::new(
275            self.x.saturating_add(margin.horizontal),
276            self.y.saturating_add(margin.vertical),
277            self.width.saturating_sub(doubled_horizontal),
278            self.height.saturating_sub(doubled_vertical),
279        )
280    }
281
282    /// Clamp this rectangle to fit within another rectangle.
283    #[must_use]
284    pub const fn clamp(self, other: Self) -> Self {
285        self.intersection(other)
286    }
287}
288
289impl fmt::Display for Rect {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
292    }
293}
294
295/// Margin (padding) specification for rectangular insets.
296#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
297#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
298pub struct Margin {
299    /// Horizontal margin (left and right)
300    pub horizontal: u16,
301    /// Vertical margin (top and bottom)
302    pub vertical: u16,
303}
304
305impl Margin {
306    /// Create a new margin.
307    ///
308    /// # Example
309    ///
310    /// ```
311    /// use tuxtui_core::geometry::Margin;
312    ///
313    /// let margin = Margin::new(1, 2);
314    /// assert_eq!(margin.horizontal, 1);
315    /// assert_eq!(margin.vertical, 2);
316    /// ```
317    #[inline]
318    #[must_use]
319    pub const fn new(horizontal: u16, vertical: u16) -> Self {
320        Self {
321            horizontal,
322            vertical,
323        }
324    }
325
326    /// Create a uniform margin.
327    #[inline]
328    #[must_use]
329    pub const fn uniform(value: u16) -> Self {
330        Self::new(value, value)
331    }
332}
333
334/// Alignment along an axis.
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
336#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
337pub enum Alignment {
338    /// Align to the start (left/top)
339    Start,
340    /// Center alignment
341    Center,
342    /// Align to the end (right/bottom)
343    End,
344}
345
346impl Default for Alignment {
347    fn default() -> Self {
348        Self::Start
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn rect_area() {
358        assert_eq!(Rect::new(0, 0, 10, 10).area(), 100);
359        assert_eq!(Rect::new(0, 0, 0, 10).area(), 0);
360    }
361
362    #[test]
363    fn rect_contains() {
364        let rect = Rect::new(5, 5, 10, 10);
365        assert!(rect.contains(Position::new(5, 5)));
366        assert!(rect.contains(Position::new(14, 14)));
367        assert!(!rect.contains(Position::new(4, 5)));
368        assert!(!rect.contains(Position::new(15, 5)));
369    }
370
371    #[test]
372    fn rect_intersection() {
373        let a = Rect::new(0, 0, 10, 10);
374        let b = Rect::new(5, 5, 10, 10);
375        assert_eq!(a.intersection(b), Rect::new(5, 5, 5, 5));
376
377        let c = Rect::new(20, 20, 10, 10);
378        assert_eq!(a.intersection(c), Rect::new(20, 20, 0, 0));
379    }
380
381    #[test]
382    fn rect_inner() {
383        let rect = Rect::new(0, 0, 10, 10);
384        let inner = rect.inner(Margin::new(1, 1));
385        assert_eq!(inner, Rect::new(1, 1, 8, 8));
386    }
387}