Skip to main content

droidrun_core/ui/
geometry.rs

1/// Geometry utilities for UI element bounds and tap point calculation.
2
3/// A rectangle defined by (left, top, right, bottom).
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct Bounds {
6    pub left: i32,
7    pub top: i32,
8    pub right: i32,
9    pub bottom: i32,
10}
11
12impl Bounds {
13    pub fn new(left: i32, top: i32, right: i32, bottom: i32) -> Self {
14        Self {
15            left,
16            top,
17            right,
18            bottom,
19        }
20    }
21
22    /// Parse from "left,top,right,bottom" string.
23    pub fn from_str(s: &str) -> Option<Self> {
24        let parts: Vec<i32> = s.split(',').filter_map(|p| p.trim().parse().ok()).collect();
25        if parts.len() == 4 {
26            Some(Self::new(parts[0], parts[1], parts[2], parts[3]))
27        } else {
28            None
29        }
30    }
31
32    /// Center point of the bounds.
33    pub fn center(&self) -> (i32, i32) {
34        ((self.left + self.right) / 2, (self.top + self.bottom) / 2)
35    }
36
37    /// Width of the bounds.
38    pub fn width(&self) -> i32 {
39        self.right - self.left
40    }
41
42    /// Height of the bounds.
43    pub fn height(&self) -> i32 {
44        self.bottom - self.top
45    }
46
47    /// Area of the bounds.
48    pub fn area(&self) -> i32 {
49        self.width() * self.height()
50    }
51
52    /// Check if two rectangles overlap.
53    pub fn overlaps(&self, other: &Bounds) -> bool {
54        !(self.right <= other.left
55            || other.right <= self.left
56            || self.bottom <= other.top
57            || other.bottom <= self.top)
58    }
59
60    /// Check if a point is inside the bounds.
61    pub fn contains_point(&self, x: i32, y: i32) -> bool {
62        x >= self.left && x < self.right && y >= self.top && y < self.bottom
63    }
64
65    /// Convert to "left,top,right,bottom" string.
66    pub fn to_string(&self) -> String {
67        format!("{},{},{},{}", self.left, self.top, self.right, self.bottom)
68    }
69}
70
71/// Find a clear tap point within bounds, avoiding blocker rectangles.
72///
73/// Uses quadrant subdivision (up to depth 4) to find an unblocked center point.
74pub fn find_clear_point(bounds: &Bounds, blockers: &[Bounds]) -> Option<(i32, i32)> {
75    find_clear_point_recursive(bounds, blockers, 0)
76}
77
78fn find_clear_point_recursive(bounds: &Bounds, blockers: &[Bounds], depth: u32) -> Option<(i32, i32)> {
79    let (cx, cy) = bounds.center();
80
81    // Check if center is blocked
82    let blocked = blockers.iter().any(|b| b.contains_point(cx, cy));
83
84    if !blocked {
85        return Some((cx, cy));
86    }
87
88    // Max recursion depth or too-small area
89    if depth > 4 || bounds.area() < 100 {
90        return None;
91    }
92
93    // Try quadrants
94    let quadrants = [
95        Bounds::new(bounds.left, bounds.top, cx, cy),
96        Bounds::new(cx, bounds.top, bounds.right, cy),
97        Bounds::new(bounds.left, cy, cx, bounds.bottom),
98        Bounds::new(cx, cy, bounds.right, bounds.bottom),
99    ];
100
101    let mut best_point = None;
102    let mut best_area = 0;
103
104    for q in &quadrants {
105        let area = q.area();
106        if area <= 0 {
107            continue;
108        }
109        if let Some(point) = find_clear_point_recursive(q, blockers, depth + 1) {
110            if area > best_area {
111                best_point = Some(point);
112                best_area = area;
113            }
114        }
115    }
116
117    best_point
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_bounds_center() {
126        let b = Bounds::new(0, 0, 100, 200);
127        assert_eq!(b.center(), (50, 100));
128    }
129
130    #[test]
131    fn test_bounds_from_str() {
132        let b = Bounds::from_str("10,20,30,40").unwrap();
133        assert_eq!(b, Bounds::new(10, 20, 30, 40));
134    }
135
136    #[test]
137    fn test_bounds_from_str_invalid() {
138        assert!(Bounds::from_str("10,20").is_none());
139        assert!(Bounds::from_str("abc").is_none());
140    }
141
142    #[test]
143    fn test_overlaps() {
144        let a = Bounds::new(0, 0, 100, 100);
145        let b = Bounds::new(50, 50, 150, 150);
146        assert!(a.overlaps(&b));
147        assert!(b.overlaps(&a));
148    }
149
150    #[test]
151    fn test_no_overlap() {
152        let a = Bounds::new(0, 0, 100, 100);
153        let b = Bounds::new(100, 100, 200, 200);
154        assert!(!a.overlaps(&b));
155    }
156
157    #[test]
158    fn test_contains_point() {
159        let b = Bounds::new(0, 0, 100, 100);
160        assert!(b.contains_point(50, 50));
161        assert!(!b.contains_point(100, 100));
162        assert!(!b.contains_point(-1, 50));
163    }
164
165    #[test]
166    fn test_find_clear_point_no_blockers() {
167        let bounds = Bounds::new(0, 0, 200, 200);
168        let point = find_clear_point(&bounds, &[]).unwrap();
169        assert_eq!(point, (100, 100));
170    }
171
172    #[test]
173    fn test_find_clear_point_blocked_center() {
174        let bounds = Bounds::new(0, 0, 200, 200);
175        let blocker = Bounds::new(90, 90, 110, 110); // blocks center
176        let point = find_clear_point(&bounds, &[blocker]).unwrap();
177        // Should find a point in one of the quadrants
178        assert!(!blocker.contains_point(point.0, point.1));
179    }
180
181    #[test]
182    fn test_find_clear_point_fully_blocked() {
183        let bounds = Bounds::new(0, 0, 10, 10);
184        let blocker = Bounds::new(0, 0, 10, 10); // covers entire area
185        let point = find_clear_point(&bounds, &[blocker]);
186        assert!(point.is_none());
187    }
188
189    #[test]
190    fn test_bounds_dimensions() {
191        let b = Bounds::new(10, 20, 110, 220);
192        assert_eq!(b.width(), 100);
193        assert_eq!(b.height(), 200);
194        assert_eq!(b.area(), 20000);
195    }
196}