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    /// Positions `self` relative to `reference` along horizontal and vertical alignment axes.
85    pub fn align_to(self, reference: Rect, h: HorizontalAlign, v: VerticalAlign) -> Rect {
86        let x = match h {
87            HorizontalAlign::Left => reference.x,
88            HorizontalAlign::Center => reference.x + (reference.w as i32 - self.w as i32) / 2,
89            HorizontalAlign::Right => reference.right() - self.w as i32,
90            HorizontalAlign::LeftToRight => reference.right(),
91            HorizontalAlign::RightToLeft => reference.x - self.w as i32,
92        };
93        let y = match v {
94            VerticalAlign::Top => reference.y,
95            VerticalAlign::Center => reference.y + (reference.h as i32 - self.h as i32) / 2,
96            VerticalAlign::Bottom => reference.bottom() - self.h as i32,
97            VerticalAlign::TopToBottom => reference.bottom(),
98            VerticalAlign::BottomToTop => reference.y - self.h as i32,
99        };
100        Rect::new(x, y, self.w, self.h)
101    }
102
103    /// Positions `self` relative to `reference` using a compound 2D anchor preset.
104    pub fn anchor_to(self, reference: Rect, anchor: Anchor) -> Rect {
105        match anchor {
106            Anchor::TopLeft => self.align_to(reference, HorizontalAlign::Left, VerticalAlign::Top),
107            Anchor::TopCenter => {
108                self.align_to(reference, HorizontalAlign::Center, VerticalAlign::Top)
109            }
110            Anchor::TopRight => {
111                self.align_to(reference, HorizontalAlign::Right, VerticalAlign::Top)
112            }
113            Anchor::CenterLeft => {
114                self.align_to(reference, HorizontalAlign::Left, VerticalAlign::Center)
115            }
116            Anchor::Center => {
117                self.align_to(reference, HorizontalAlign::Center, VerticalAlign::Center)
118            }
119            Anchor::CenterRight => {
120                self.align_to(reference, HorizontalAlign::Right, VerticalAlign::Center)
121            }
122            Anchor::BottomLeft => {
123                self.align_to(reference, HorizontalAlign::Left, VerticalAlign::Bottom)
124            }
125            Anchor::BottomCenter => {
126                self.align_to(reference, HorizontalAlign::Center, VerticalAlign::Bottom)
127            }
128            Anchor::BottomRight => {
129                self.align_to(reference, HorizontalAlign::Right, VerticalAlign::Bottom)
130            }
131            Anchor::OutsideTop => self.align_to(
132                reference,
133                HorizontalAlign::Center,
134                VerticalAlign::BottomToTop,
135            ),
136            Anchor::OutsideBottom => self.align_to(
137                reference,
138                HorizontalAlign::Center,
139                VerticalAlign::TopToBottom,
140            ),
141            Anchor::OutsideLeft => self.align_to(
142                reference,
143                HorizontalAlign::RightToLeft,
144                VerticalAlign::Center,
145            ),
146            Anchor::OutsideRight => self.align_to(
147                reference,
148                HorizontalAlign::LeftToRight,
149                VerticalAlign::Center,
150            ),
151        }
152    }
153}
154
155/// Horizontal alignment policy for relative positioning.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub enum HorizontalAlign {
158    Left,
159    Center,
160    Right,
161    /// Place directly adjacent to the right outer edge of reference.
162    LeftToRight,
163    /// Place directly adjacent to the left outer edge of reference.
164    RightToLeft,
165}
166
167/// Vertical alignment policy for relative positioning.
168#[derive(Clone, Copy, Debug, PartialEq, Eq)]
169pub enum VerticalAlign {
170    Top,
171    Center,
172    Bottom,
173    /// Place directly below the bottom outer edge of reference.
174    TopToBottom,
175    /// Place directly above the top outer edge of reference.
176    BottomToTop,
177}
178
179/// Compound 2D anchor presets for positioning UI elements relative to parents or siblings.
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181pub enum Anchor {
182    TopLeft,
183    TopCenter,
184    TopRight,
185    CenterLeft,
186    Center,
187    CenterRight,
188    BottomLeft,
189    BottomCenter,
190    BottomRight,
191    OutsideTop,
192    OutsideBottom,
193    OutsideLeft,
194    OutsideRight,
195}
196
197#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
198pub struct EdgeInsets {
199    pub left: i16,
200    pub right: i16,
201    pub top: i16,
202    pub bottom: i16,
203}
204
205impl EdgeInsets {
206    pub const fn all(v: i16) -> Self {
207        Self {
208            left: v,
209            right: v,
210            top: v,
211            bottom: v,
212        }
213    }
214
215    pub const fn symmetric(horizontal: i16, vertical: i16) -> Self {
216        Self {
217            left: horizontal,
218            right: horizontal,
219            top: vertical,
220            bottom: vertical,
221        }
222    }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum DirtyError {
227    Full,
228}
229
230pub struct DirtyTracker<const N: usize> {
231    regions: Vec<Rect, N>,
232}
233
234impl<const N: usize> DirtyTracker<N> {
235    pub const fn new() -> Self {
236        Self {
237            regions: Vec::new(),
238        }
239    }
240
241    pub fn clear(&mut self) {
242        self.regions.clear();
243    }
244
245    pub fn add(&mut self, rect: Rect) -> Result<(), DirtyError> {
246        if rect.is_empty() {
247            return Ok(());
248        }
249
250        if self.regions.iter().any(|r| r.intersects(rect)) {
251            let mut merged = rect;
252            let mut i = 0;
253            while i < self.regions.len() {
254                if self.regions[i].intersects(merged) {
255                    merged = merged.union(self.regions.swap_remove(i));
256                } else {
257                    i += 1;
258                }
259            }
260            return self.regions.push(merged).map_err(|_| DirtyError::Full);
261        }
262
263        self.regions.push(rect).map_err(|_| DirtyError::Full)
264    }
265
266    pub fn mark_all(&mut self, rect: Rect) -> Result<(), DirtyError> {
267        self.regions.clear();
268        self.add(rect)
269    }
270
271    pub fn as_slice(&self) -> &[Rect] {
272        self.regions.as_slice()
273    }
274
275    pub fn bounding_rect(&self) -> Option<Rect> {
276        let mut iter = self.regions.iter().copied();
277        let first = iter.next()?;
278        Some(iter.fold(first, Rect::union))
279    }
280
281    pub fn is_empty(&self) -> bool {
282        self.regions.is_empty()
283    }
284}
285
286impl<const N: usize> Default for DirtyTracker<N> {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_rect_empty_and_contains() {
298        let r = Rect::new(10, 20, 30, 40);
299        assert!(!r.is_empty());
300        assert_eq!(r.right(), 40);
301        assert_eq!(r.bottom(), 60);
302
303        assert!(r.contains(10, 20));
304        assert!(r.contains(39, 59));
305        assert!(!r.contains(40, 60));
306        assert!(!r.contains(9, 20));
307
308        let empty = Rect::empty();
309        assert!(empty.is_empty());
310        assert!(!empty.contains(0, 0));
311    }
312
313    #[test]
314    fn test_rect_intersection_and_union() {
315        let r1 = Rect::new(0, 0, 20, 20);
316        let r2 = Rect::new(10, 10, 20, 20);
317
318        assert!(r1.intersects(r2));
319        assert_eq!(r1.intersection(r2), Rect::new(10, 10, 10, 10));
320        assert_eq!(r1.union(r2), Rect::new(0, 0, 30, 30));
321
322        let r3 = Rect::new(50, 50, 10, 10);
323        assert!(!r1.intersects(r3));
324        assert!(r1.intersection(r3).is_empty());
325    }
326
327    #[test]
328    fn test_rect_inset() {
329        let r = Rect::new(10, 10, 40, 40);
330        let inset = r.inset(EdgeInsets::all(5));
331        assert_eq!(inset, Rect::new(15, 15, 30, 30));
332
333        // Excess inset saturates width and height to 0
334        let over_inset = r.inset(EdgeInsets::all(30));
335        assert_eq!(over_inset.w, 0);
336        assert_eq!(over_inset.h, 0);
337    }
338
339    #[test]
340    fn test_dirty_tracker() {
341        let mut dt: DirtyTracker<4> = DirtyTracker::new();
342        assert!(dt.is_empty());
343
344        dt.add(Rect::new(0, 0, 10, 10)).unwrap();
345        assert_eq!(dt.as_slice().len(), 1);
346
347        // Add non-overlapping rect
348        dt.add(Rect::new(20, 20, 10, 10)).unwrap();
349        assert_eq!(dt.as_slice().len(), 2);
350
351        // Add overlapping rect to trigger merge
352        dt.add(Rect::new(5, 5, 10, 10)).unwrap();
353        // The overlapping rects merge into one larger region
354        assert_eq!(dt.as_slice().len(), 2);
355
356        assert_eq!(dt.bounding_rect(), Some(Rect::new(0, 0, 30, 30)));
357
358        dt.clear();
359        assert!(dt.is_empty());
360        assert_eq!(dt.bounding_rect(), None);
361    }
362
363    #[test]
364    fn test_rect_align_to_and_anchor_to() {
365        let parent = Rect::new(0, 0, 100, 100);
366        let child = Rect::new(0, 0, 20, 20);
367
368        // Center alignment
369        let centered = child.align_to(parent, HorizontalAlign::Center, VerticalAlign::Center);
370        assert_eq!(centered, Rect::new(40, 40, 20, 20));
371
372        // Top-right anchor
373        let top_right = child.anchor_to(parent, Anchor::TopRight);
374        assert_eq!(top_right, Rect::new(80, 0, 20, 20));
375
376        // Outside-bottom anchor (dropdown/tooltip)
377        let dropdown = child.anchor_to(parent, Anchor::OutsideBottom);
378        assert_eq!(dropdown, Rect::new(40, 100, 20, 20));
379
380        // Outside-right (badge/adjacent icon)
381        let badge = child.align_to(parent, HorizontalAlign::LeftToRight, VerticalAlign::Top);
382        assert_eq!(badge, Rect::new(100, 0, 20, 20));
383    }
384}