Skip to main content

embedded_gui/
geometry.rs

1use heapless::Vec;
2
3#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4pub struct Rect {
5    pub x: i32,
6    pub y: i32,
7    pub w: u32,
8    pub h: u32,
9}
10
11impl Rect {
12    pub const fn new(x: i32, y: i32, w: u32, h: u32) -> Self {
13        Self { x, y, w, h }
14    }
15
16    pub const fn empty() -> Self {
17        Self::new(0, 0, 0, 0)
18    }
19
20    pub const fn right(self) -> i32 {
21        self.x + self.w as i32
22    }
23
24    pub const fn bottom(self) -> i32 {
25        self.y + self.h as i32
26    }
27
28    pub const fn is_empty(self) -> bool {
29        self.w == 0 || self.h == 0
30    }
31
32    pub fn contains(self, x: i32, y: i32) -> bool {
33        x >= self.x && y >= self.y && x < self.right() && y < self.bottom()
34    }
35
36    pub fn intersects(self, other: Self) -> bool {
37        !self.intersection(other).is_empty()
38    }
39
40    pub fn intersection(self, other: Self) -> Self {
41        let x0 = self.x.max(other.x);
42        let y0 = self.y.max(other.y);
43        let x1 = self.right().min(other.right());
44        let y1 = self.bottom().min(other.bottom());
45
46        if x1 <= x0 || y1 <= y0 {
47            Self::empty()
48        } else {
49            Self::new(x0, y0, (x1 - x0) as u32, (y1 - y0) as u32)
50        }
51    }
52
53    pub fn union(self, other: Self) -> Self {
54        if self.is_empty() {
55            return other;
56        }
57        if other.is_empty() {
58            return self;
59        }
60
61        let x0 = self.x.min(other.x);
62        let y0 = self.y.min(other.y);
63        let x1 = self.right().max(other.right());
64        let y1 = self.bottom().max(other.bottom());
65        Self::new(x0, y0, (x1 - x0) as u32, (y1 - y0) as u32)
66    }
67
68    pub fn inset(self, edges: EdgeInsets) -> Self {
69        let left = edges.left.max(0) as u32;
70        let right = edges.right.max(0) as u32;
71        let top = edges.top.max(0) as u32;
72        let bottom = edges.bottom.max(0) as u32;
73        let shrink_w = left.saturating_add(right).min(self.w);
74        let shrink_h = top.saturating_add(bottom).min(self.h);
75
76        Self::new(
77            self.x + left as i32,
78            self.y + top as i32,
79            self.w - shrink_w,
80            self.h - shrink_h,
81        )
82    }
83}
84
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
86pub struct EdgeInsets {
87    pub left: i16,
88    pub right: i16,
89    pub top: i16,
90    pub bottom: i16,
91}
92
93impl EdgeInsets {
94    pub const fn all(v: i16) -> Self {
95        Self {
96            left: v,
97            right: v,
98            top: v,
99            bottom: v,
100        }
101    }
102
103    pub const fn symmetric(horizontal: i16, vertical: i16) -> Self {
104        Self {
105            left: horizontal,
106            right: horizontal,
107            top: vertical,
108            bottom: vertical,
109        }
110    }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum DirtyError {
115    Full,
116}
117
118pub struct DirtyTracker<const N: usize> {
119    regions: Vec<Rect, N>,
120}
121
122impl<const N: usize> DirtyTracker<N> {
123    pub const fn new() -> Self {
124        Self {
125            regions: Vec::new(),
126        }
127    }
128
129    pub fn clear(&mut self) {
130        self.regions.clear();
131    }
132
133    pub fn add(&mut self, rect: Rect) -> Result<(), DirtyError> {
134        if rect.is_empty() {
135            return Ok(());
136        }
137
138        if self.regions.iter().any(|r| r.intersects(rect)) {
139            let mut merged = rect;
140            let mut i = 0;
141            while i < self.regions.len() {
142                if self.regions[i].intersects(merged) {
143                    merged = merged.union(self.regions.swap_remove(i));
144                } else {
145                    i += 1;
146                }
147            }
148            return self.regions.push(merged).map_err(|_| DirtyError::Full);
149        }
150
151        self.regions.push(rect).map_err(|_| DirtyError::Full)
152    }
153
154    pub fn mark_all(&mut self, rect: Rect) -> Result<(), DirtyError> {
155        self.regions.clear();
156        self.add(rect)
157    }
158
159    pub fn as_slice(&self) -> &[Rect] {
160        self.regions.as_slice()
161    }
162
163    pub fn bounding_rect(&self) -> Option<Rect> {
164        let mut iter = self.regions.iter().copied();
165        let first = iter.next()?;
166        Some(iter.fold(first, Rect::union))
167    }
168
169    pub fn is_empty(&self) -> bool {
170        self.regions.is_empty()
171    }
172}
173
174impl<const N: usize> Default for DirtyTracker<N> {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_rect_empty_and_contains() {
186        let r = Rect::new(10, 20, 30, 40);
187        assert!(!r.is_empty());
188        assert_eq!(r.right(), 40);
189        assert_eq!(r.bottom(), 60);
190
191        assert!(r.contains(10, 20));
192        assert!(r.contains(39, 59));
193        assert!(!r.contains(40, 60));
194        assert!(!r.contains(9, 20));
195
196        let empty = Rect::empty();
197        assert!(empty.is_empty());
198        assert!(!empty.contains(0, 0));
199    }
200
201    #[test]
202    fn test_rect_intersection_and_union() {
203        let r1 = Rect::new(0, 0, 20, 20);
204        let r2 = Rect::new(10, 10, 20, 20);
205
206        assert!(r1.intersects(r2));
207        assert_eq!(r1.intersection(r2), Rect::new(10, 10, 10, 10));
208        assert_eq!(r1.union(r2), Rect::new(0, 0, 30, 30));
209
210        let r3 = Rect::new(50, 50, 10, 10);
211        assert!(!r1.intersects(r3));
212        assert!(r1.intersection(r3).is_empty());
213    }
214
215    #[test]
216    fn test_rect_inset() {
217        let r = Rect::new(10, 10, 40, 40);
218        let inset = r.inset(EdgeInsets::all(5));
219        assert_eq!(inset, Rect::new(15, 15, 30, 30));
220
221        // Excess inset saturates width and height to 0
222        let over_inset = r.inset(EdgeInsets::all(30));
223        assert_eq!(over_inset.w, 0);
224        assert_eq!(over_inset.h, 0);
225    }
226
227    #[test]
228    fn test_dirty_tracker() {
229        let mut dt: DirtyTracker<4> = DirtyTracker::new();
230        assert!(dt.is_empty());
231
232        dt.add(Rect::new(0, 0, 10, 10)).unwrap();
233        assert_eq!(dt.as_slice().len(), 1);
234
235        // Add non-overlapping rect
236        dt.add(Rect::new(20, 20, 10, 10)).unwrap();
237        assert_eq!(dt.as_slice().len(), 2);
238
239        // Add overlapping rect to trigger merge
240        dt.add(Rect::new(5, 5, 10, 10)).unwrap();
241        // The overlapping rects merge into one larger region
242        assert_eq!(dt.as_slice().len(), 2);
243
244        assert_eq!(dt.bounding_rect(), Some(Rect::new(0, 0, 30, 30)));
245
246        dt.clear();
247        assert!(dt.is_empty());
248        assert_eq!(dt.bounding_rect(), None);
249    }
250}