Skip to main content

abstracttui/base/
geom.rs

1//! Integer cell geometry. All coordinates are `i32` so compositor math
2//! (negative offsets mid-animation, off-screen layers) never underflows;
3//! buffer indexing clamps at the edge of a `Rect`.
4
5#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
6pub struct Point {
7    pub x: i32,
8    pub y: i32,
9}
10
11impl Point {
12    pub const ZERO: Point = Point { x: 0, y: 0 };
13
14    pub const fn new(x: i32, y: i32) -> Self {
15        Point { x, y }
16    }
17
18    pub const fn translate(self, dx: i32, dy: i32) -> Self {
19        Point::new(self.x + dx, self.y + dy)
20    }
21}
22
23#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
24pub struct Size {
25    pub w: i32,
26    pub h: i32,
27}
28
29impl Size {
30    pub const ZERO: Size = Size { w: 0, h: 0 };
31
32    pub const fn new(w: i32, h: i32) -> Self {
33        Size { w, h }
34    }
35
36    pub const fn is_empty(self) -> bool {
37        self.w <= 0 || self.h <= 0
38    }
39
40    pub const fn area(self) -> i64 {
41        if self.is_empty() {
42            0
43        } else {
44            self.w as i64 * self.h as i64
45        }
46    }
47}
48
49/// A size in device pixels (terminal cell-pixel geometry from
50/// TIOCGWINSZ / `CSI 14 t` / `CSI 16 t`). Distinct from `Size`, which is
51/// always cells — mixing the two units is a classic gfx-scaling bug, so
52/// the type system keeps them apart (KERNEL/GFX3D request, cycle 1).
53#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
54pub struct PixelSize {
55    pub w: u16,
56    pub h: u16,
57}
58
59impl PixelSize {
60    pub const fn new(w: u16, h: u16) -> Self {
61        PixelSize { w, h }
62    }
63
64    pub const fn is_empty(self) -> bool {
65        self.w == 0 || self.h == 0
66    }
67}
68
69#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
70pub struct Rect {
71    pub x: i32,
72    pub y: i32,
73    pub w: i32,
74    pub h: i32,
75}
76
77impl Rect {
78    pub const ZERO: Rect = Rect {
79        x: 0,
80        y: 0,
81        w: 0,
82        h: 0,
83    };
84
85    pub const fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
86        Rect { x, y, w, h }
87    }
88
89    pub const fn from_size(size: Size) -> Self {
90        Rect::new(0, 0, size.w, size.h)
91    }
92
93    pub const fn origin(self) -> Point {
94        Point::new(self.x, self.y)
95    }
96
97    pub const fn size(self) -> Size {
98        Size::new(self.w, self.h)
99    }
100
101    pub const fn right(self) -> i32 {
102        self.x + self.w
103    }
104
105    pub const fn bottom(self) -> i32 {
106        self.y + self.h
107    }
108
109    pub const fn is_empty(self) -> bool {
110        self.w <= 0 || self.h <= 0
111    }
112
113    pub const fn area(self) -> i64 {
114        self.size().area()
115    }
116
117    pub const fn contains(self, p: Point) -> bool {
118        p.x >= self.x && p.y >= self.y && p.x < self.right() && p.y < self.bottom()
119    }
120
121    pub fn intersect(self, other: Rect) -> Rect {
122        let x = self.x.max(other.x);
123        let y = self.y.max(other.y);
124        let r = self.right().min(other.right());
125        let b = self.bottom().min(other.bottom());
126        if r <= x || b <= y {
127            Rect::ZERO
128        } else {
129            Rect::new(x, y, r - x, b - y)
130        }
131    }
132
133    pub fn intersects(self, other: Rect) -> bool {
134        !self.intersect(other).is_empty()
135    }
136
137    /// Smallest rect covering both. Empty rects are identity elements.
138    pub fn union(self, other: Rect) -> Rect {
139        if self.is_empty() {
140            return other;
141        }
142        if other.is_empty() {
143            return self;
144        }
145        let x = self.x.min(other.x);
146        let y = self.y.min(other.y);
147        let r = self.right().max(other.right());
148        let b = self.bottom().max(other.bottom());
149        Rect::new(x, y, r - x, b - y)
150    }
151
152    pub const fn translate(self, dx: i32, dy: i32) -> Rect {
153        Rect::new(self.x + dx, self.y + dy, self.w, self.h)
154    }
155
156    /// Shrink by `n` on every side (clamping at empty).
157    pub fn inset(self, n: i32) -> Rect {
158        let w = (self.w - 2 * n).max(0);
159        let h = (self.h - 2 * n).max(0);
160        if w == 0 || h == 0 {
161            Rect::new(self.x + n, self.y + n, 0, 0)
162        } else {
163            Rect::new(self.x + n, self.y + n, w, h)
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn intersect_and_union() {
174        let a = Rect::new(0, 0, 10, 10);
175        let b = Rect::new(5, 5, 10, 10);
176        assert_eq!(a.intersect(b), Rect::new(5, 5, 5, 5));
177        assert_eq!(a.union(b), Rect::new(0, 0, 15, 15));
178        assert!(a.intersect(Rect::new(20, 20, 5, 5)).is_empty());
179        assert_eq!(Rect::ZERO.union(a), a);
180    }
181
182    #[test]
183    fn contains_edges() {
184        let r = Rect::new(1, 1, 2, 2);
185        assert!(r.contains(Point::new(1, 1)));
186        assert!(r.contains(Point::new(2, 2)));
187        assert!(!r.contains(Point::new(3, 3)));
188    }
189}