1use crate::rect::{LayoutRect, Quadrant};
2
3pub fn hit_test_leaf<Id: Copy + Eq + Ord>(
5 regions: &[(Id, LayoutRect)],
6 col: u16,
7 row: u16,
8) -> Option<Id> {
9 for (id, rect) in regions.iter().rev() {
10 if rect.contains(col, row) {
11 return Some(*id);
12 }
13 }
14 None
15}
16
17pub fn detect_quadrant(cursor_col: u16, cursor_row: u16, target: &LayoutRect) -> Quadrant {
26 let (cx, cy) = target.center();
27
28 let dx = i32::from(cursor_col).saturating_sub(cx);
29 let dy = i32::from(cursor_row).saturating_sub(cy);
30
31 if dx == 0 && dy == 0 {
32 return Quadrant::East;
33 }
34
35 let adx = dx.unsigned_abs();
36 let ady = dy.unsigned_abs();
37 let w = u32::from(target.width);
38 let h = u32::from(target.height);
39
40 let scaled_dx = adx.saturating_mul(h);
41 let scaled_dy = ady.saturating_mul(w);
42
43 if scaled_dx > scaled_dy || (scaled_dx == scaled_dy && dx >= 0) {
44 if dx >= 0 {
45 Quadrant::East
46 } else {
47 Quadrant::West
48 }
49 } else {
50 if dy < 0 {
51 Quadrant::North
52 } else {
53 Quadrant::South
54 }
55 }
56}
57
58pub fn find_closest_region<Id: Copy>(
61 cx: i32,
62 cy: i32,
63 regions: &[(Id, LayoutRect)],
64 aspect_ratio_weight: u32,
65) -> Option<(Id, LayoutRect)> {
66 if regions.is_empty() {
67 return None;
68 }
69 let weight = aspect_ratio_weight as i64;
70 regions
71 .iter()
72 .map(|(id, rect)| {
73 let (rcx, rcy) = rect.center();
74 let dx = (cx as i64) - (rcx as i64);
75 let dy = ((cy as i64) - (rcy as i64)) * weight;
76 let dist = dx * dx + dy * dy;
77 (*id, *rect, dist)
78 })
79 .min_by_key(|(_, _, d)| *d)
80 .map(|(id, rect, _)| (id, rect))
81}
82
83pub fn resolve_target<Id: Copy>(
87 cx: i32,
88 cy: i32,
89 regions: &[(Id, LayoutRect)],
90 aspect_ratio_weight: u32,
91) -> Option<(Id, LayoutRect)> {
92 if let Some(found) = regions
94 .iter()
95 .find(|(_, r)| r.contains(cx as u16, cy as u16))
96 {
97 return Some(*found);
98 }
99 find_closest_region(cx, cy, regions, aspect_ratio_weight)
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 fn rect() -> LayoutRect {
108 LayoutRect {
109 x: 0,
110 y: 0,
111 width: 100,
112 height: 100,
113 }
114 }
115
116 #[test]
117 fn hit_test_finds_topmost() {
118 let regions = vec![
119 (
120 1u8,
121 LayoutRect {
122 x: 0,
123 y: 0,
124 width: 50,
125 height: 50,
126 },
127 ),
128 (
129 2u8,
130 LayoutRect {
131 x: 0,
132 y: 0,
133 width: 10,
134 height: 10,
135 },
136 ),
137 ];
138 assert_eq!(hit_test_leaf(®ions, 5, 5), Some(2));
139 }
140
141 #[test]
142 fn hit_test_miss() {
143 let regions = vec![(
144 1u8,
145 LayoutRect {
146 x: 0,
147 y: 0,
148 width: 10,
149 height: 10,
150 },
151 )];
152 assert_eq!(hit_test_leaf(®ions, 20, 20), None);
153 }
154
155 #[test]
156 fn quadrant_east() {
157 assert_eq!(detect_quadrant(75, 50, &rect()), Quadrant::East);
158 }
159
160 #[test]
161 fn quadrant_west() {
162 assert_eq!(detect_quadrant(25, 50, &rect()), Quadrant::West);
163 }
164
165 #[test]
166 fn quadrant_south() {
167 assert_eq!(detect_quadrant(50, 75, &rect()), Quadrant::South);
168 }
169
170 #[test]
171 fn quadrant_north() {
172 assert_eq!(detect_quadrant(50, 25, &rect()), Quadrant::North);
173 }
174
175 #[test]
176 fn quadrant_on_center_defaults_to_east() {
177 assert_eq!(detect_quadrant(50, 50, &rect()), Quadrant::East);
178 }
179
180 #[test]
181 fn quadrant_non_square_wide_target() {
182 let wide = LayoutRect {
183 x: 0,
184 y: 0,
185 width: 100,
186 height: 20,
187 };
188 assert_eq!(detect_quadrant(80, 5, &wide), Quadrant::East);
189 assert_eq!(detect_quadrant(60, 2, &wide), Quadrant::North);
190 assert_eq!(detect_quadrant(55, 0, &wide), Quadrant::North);
191 }
192
193 #[test]
194 fn quadrant_non_square_tall_target() {
195 let tall = LayoutRect {
196 x: 0,
197 y: 0,
198 width: 20,
199 height: 60,
200 };
201 assert_eq!(detect_quadrant(18, 5, &tall), Quadrant::North);
202 assert_eq!(detect_quadrant(19, 10, &tall), Quadrant::East);
203 }
204
205 #[test]
208 fn quadrant_to_insert_position_mappings() {
209 use crate::snap::InsertPosition;
210 assert_eq!(Quadrant::North.to_insert_position(), InsertPosition::Top);
211 assert_eq!(Quadrant::South.to_insert_position(), InsertPosition::Bottom);
212 assert_eq!(Quadrant::West.to_insert_position(), InsertPosition::Left);
213 assert_eq!(Quadrant::East.to_insert_position(), InsertPosition::Right);
214 }
215
216 #[test]
219 fn resolve_target_exact_hit() {
220 let left = LayoutRect {
222 x: 0,
223 y: 0,
224 width: 50,
225 height: 100,
226 };
227 let right = LayoutRect {
228 x: 51,
229 y: 0,
230 width: 50,
231 height: 100,
232 };
233 let regions = vec![(1u8, left), (2u8, right)];
234 let result = resolve_target(25, 50, ®ions, 2);
236 assert_eq!(result, Some((1u8, left)));
237 }
238
239 #[test]
240 fn resolve_target_gap_nearest_left() {
241 let left = LayoutRect {
243 x: 0,
244 y: 0,
245 width: 50,
246 height: 100,
247 };
248 let right = LayoutRect {
249 x: 51,
250 y: 0,
251 width: 50,
252 height: 100,
253 };
254 let regions = vec![(1u8, left), (2u8, right)];
255 let result = resolve_target(50, 50, ®ions, 2);
258 assert_eq!(result, Some((1u8, left)));
259 }
260
261 #[test]
262 fn resolve_target_gap_nearest_right() {
263 let left = LayoutRect {
264 x: 0,
265 y: 0,
266 width: 50,
267 height: 100,
268 };
269 let right = LayoutRect {
270 x: 51,
271 y: 0,
272 width: 50,
273 height: 100,
274 };
275 let regions = vec![(1u8, left), (2u8, right)];
276 let result = resolve_target(50, 75, ®ions, 2);
278 assert_eq!(result, Some((1u8, left)));
281 }
282
283 #[test]
284 fn resolve_target_aspect_ratio_selects_horizontal() {
285 let above = LayoutRect {
289 x: 50,
290 y: 0,
291 width: 100,
292 height: 48,
293 };
294 let right = LayoutRect {
295 x: 101,
296 y: 49,
297 width: 100,
298 height: 48,
299 };
300 let regions = vec![(1u8, above), (2u8, right)];
301 let result = resolve_target(100, 50, ®ions, 2);
305 assert_eq!(result, Some((1u8, above)));
306 }
307
308 #[test]
309 fn resolve_target_empty() {
310 let regions: Vec<(u8, LayoutRect)> = vec![];
311 assert!(resolve_target(50, 50, ®ions, 2).is_none());
312 }
313}