1use dioxus::prelude::Callback;
4
5use super::{Point, Rect, ZoneId};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8#[non_exhaustive]
9pub enum CollisionStrategy {
10 #[default]
11 PointerWithin,
12 ClosestCenter,
13 ClosestCorners,
14 RectIntersection,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18#[non_exhaustive]
19pub struct ZoneCandidate {
20 pub id: ZoneId,
21 pub rect: Rect,
22 pub order: usize,
23}
24
25impl ZoneCandidate {
26 pub fn new(id: ZoneId, rect: Rect, order: usize) -> Self {
27 Self { id, rect, order }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq)]
32#[non_exhaustive]
33pub struct Collision {
34 pub zone: ZoneId,
35 pub score: f64,
37}
38
39impl Collision {
40 pub fn new(zone: ZoneId, score: f64) -> Self {
41 Self { zone, score }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq)]
46#[non_exhaustive]
47pub struct CollisionRequest<T> {
48 pub pointer: Point,
49 pub active_rect: Option<Rect>,
50 pub payload: T,
51 pub candidates: Vec<ZoneCandidate>,
52 pub max_distance: f64,
53}
54
55impl<T> CollisionRequest<T> {
56 pub fn new(pointer: Point, payload: T, candidates: Vec<ZoneCandidate>) -> Self {
57 Self {
58 pointer,
59 active_rect: None,
60 payload,
61 candidates,
62 max_distance: 0.0,
63 }
64 }
65}
66
67#[non_exhaustive]
68pub enum CollisionDetector<T: 'static> {
69 BuiltIn(CollisionStrategy),
70 Custom(Callback<CollisionRequest<T>, Vec<Collision>>),
71}
72
73impl<T> Copy for CollisionDetector<T> {}
74impl<T> Clone for CollisionDetector<T> {
75 fn clone(&self) -> Self {
76 *self
77 }
78}
79impl<T> PartialEq for CollisionDetector<T> {
80 fn eq(&self, other: &Self) -> bool {
81 match (*self, *other) {
82 (Self::BuiltIn(a), Self::BuiltIn(b)) => a == b,
83 (Self::Custom(a), Self::Custom(b)) => a == b,
84 _ => false,
85 }
86 }
87}
88
89impl<T> std::fmt::Debug for CollisionDetector<T> {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Self::BuiltIn(strategy) => f.debug_tuple("BuiltIn").field(strategy).finish(),
93 Self::Custom(_) => f.write_str("Custom(..)"),
94 }
95 }
96}
97
98impl<T> Default for CollisionDetector<T> {
99 fn default() -> Self {
100 Self::BuiltIn(CollisionStrategy::PointerWithin)
101 }
102}
103
104#[derive(Debug)]
105#[non_exhaustive]
106pub struct ReleasePolicy<T: 'static> {
107 pub collision: CollisionDetector<T>,
108 pub recovery_radius: f64,
109 pub sticky: bool,
110}
111
112impl<T> Copy for ReleasePolicy<T> {}
113impl<T> Clone for ReleasePolicy<T> {
114 fn clone(&self) -> Self {
115 *self
116 }
117}
118impl<T> PartialEq for ReleasePolicy<T> {
119 fn eq(&self, other: &Self) -> bool {
120 self.collision == other.collision
121 && self.recovery_radius == other.recovery_radius
122 && self.sticky == other.sticky
123 }
124}
125
126impl<T> Default for ReleasePolicy<T> {
127 fn default() -> Self {
128 Self {
129 collision: CollisionDetector::default(),
130 recovery_radius: 48.0,
131 sticky: false,
132 }
133 }
134}
135
136impl<T> ReleasePolicy<T> {
137 pub fn strategy(strategy: CollisionStrategy) -> Self {
138 Self {
139 collision: CollisionDetector::BuiltIn(strategy),
140 ..Self::default()
141 }
142 }
143
144 pub fn with_recovery_radius(mut self, recovery_radius: f64) -> Self {
145 self.recovery_radius = recovery_radius.max(0.0);
146 self
147 }
148
149 pub fn with_collision(mut self, collision: CollisionDetector<T>) -> Self {
150 self.collision = collision;
151 self
152 }
153
154 pub fn with_sticky(mut self, sticky: bool) -> Self {
155 self.sticky = sticky;
156 self
157 }
158}
159
160pub fn rank_collisions<T: Clone + 'static>(
161 detector: CollisionDetector<T>,
162 request: CollisionRequest<T>,
163) -> Vec<Collision> {
164 match detector {
165 CollisionDetector::BuiltIn(strategy) => rank_builtin(strategy, &request),
166 CollisionDetector::Custom(callback) => {
167 let orders: Vec<_> = request
168 .candidates
169 .iter()
170 .map(|candidate| (candidate.id, candidate.order))
171 .collect();
172 let order = |zone| {
173 orders
174 .iter()
175 .find(|(candidate, _)| *candidate == zone)
176 .map(|(_, order)| *order)
177 .unwrap_or_default()
178 };
179 let mut ranked = callback.call(request);
180 ranked.sort_by(|a, b| {
181 a.score
182 .total_cmp(&b.score)
183 .then_with(|| order(b.zone).cmp(&order(a.zone)))
184 });
185 ranked
186 }
187 }
188}
189
190pub(crate) fn rank_builtin_candidates(
191 strategy: CollisionStrategy,
192 pointer: Point,
193 active_rect: Option<Rect>,
194 candidates: Vec<ZoneCandidate>,
195 max_distance: f64,
196) -> Vec<Collision> {
197 rank_builtin(
198 strategy,
199 &CollisionRequest {
200 pointer,
201 active_rect,
202 payload: (),
203 candidates,
204 max_distance,
205 },
206 )
207}
208
209fn rank_builtin<T>(strategy: CollisionStrategy, request: &CollisionRequest<T>) -> Vec<Collision> {
210 let mut ranked = Vec::new();
211 for candidate in &request.candidates {
212 let edge_distance = point_rect_distance(request.pointer, candidate.rect);
213 let overlap = request
214 .active_rect
215 .map(|active| intersection_area(active, candidate.rect))
216 .unwrap_or(0.0);
217 let eligible = match (strategy, request.active_rect) {
218 (CollisionStrategy::RectIntersection, Some(_)) => {
219 overlap > 0.0
220 || (request.max_distance > 0.0 && edge_distance <= request.max_distance)
221 }
222 (CollisionStrategy::ClosestCenter | CollisionStrategy::ClosestCorners, Some(_)) => {
223 overlap > 0.0 || edge_distance <= request.max_distance
224 }
225 _ => edge_distance <= request.max_distance,
226 };
227 if !eligible {
228 continue;
229 }
230 let score = match strategy {
231 CollisionStrategy::PointerWithin => edge_distance,
232 CollisionStrategy::ClosestCenter => distance(
233 request
234 .active_rect
235 .map(|rect| rect.center())
236 .unwrap_or(request.pointer),
237 candidate.rect.center(),
238 ),
239 CollisionStrategy::ClosestCorners => match request.active_rect {
240 Some(active) => closest_corner_distance(active, candidate.rect),
241 None => point_corner_distance(request.pointer, candidate.rect),
242 },
243 CollisionStrategy::RectIntersection => {
244 if overlap > 0.0 {
245 -overlap
246 } else {
247 edge_distance
248 }
249 }
250 };
251 ranked.push((
252 Collision {
253 zone: candidate.id,
254 score,
255 },
256 candidate.order,
257 ));
258 }
259 ranked.sort_by(|(a, ao), (b, bo)| {
260 a.score
261 .total_cmp(&b.score)
262 .then_with(|| bo.cmp(ao))
265 });
266 ranked.into_iter().map(|(collision, _)| collision).collect()
267}
268
269fn distance(a: Point, b: Point) -> f64 {
270 let d = a - b;
271 (d.x * d.x + d.y * d.y).sqrt()
272}
273
274pub fn point_rect_distance(point: Point, rect: Rect) -> f64 {
275 let dx = (rect.x - point.x)
276 .max(point.x - (rect.x + rect.width))
277 .max(0.0);
278 let dy = (rect.y - point.y)
279 .max(point.y - (rect.y + rect.height))
280 .max(0.0);
281 (dx * dx + dy * dy).sqrt()
282}
283
284fn corners(rect: Rect) -> [Point; 4] {
285 [
286 Point::new(rect.x, rect.y),
287 Point::new(rect.x + rect.width, rect.y),
288 Point::new(rect.x + rect.width, rect.y + rect.height),
289 Point::new(rect.x, rect.y + rect.height),
290 ]
291}
292
293fn point_corner_distance(point: Point, rect: Rect) -> f64 {
294 corners(rect)
295 .into_iter()
296 .map(|corner| distance(point, corner))
297 .min_by(f64::total_cmp)
298 .unwrap_or(f64::INFINITY)
299}
300
301fn closest_corner_distance(a: Rect, b: Rect) -> f64 {
302 corners(a)
303 .into_iter()
304 .flat_map(|left| {
305 corners(b)
306 .into_iter()
307 .map(move |right| distance(left, right))
308 })
309 .min_by(f64::total_cmp)
310 .unwrap_or(f64::INFINITY)
311}
312
313fn intersection_area(a: Rect, b: Rect) -> f64 {
314 let width = (a.x + a.width).min(b.x + b.width) - a.x.max(b.x);
315 let height = (a.y + a.height).min(b.y + b.height) - a.y.max(b.y);
316 width.max(0.0) * height.max(0.0)
317}
318
319#[cfg(test)]
320mod tests {
321 use dioxus::prelude::*;
322
323 use super::*;
324
325 fn request(point: Point) -> CollisionRequest<()> {
326 CollisionRequest {
327 pointer: point,
328 active_rect: None,
329 payload: (),
330 candidates: vec![
331 ZoneCandidate {
332 id: ZoneId(1),
333 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
334 order: 0,
335 },
336 ZoneCandidate {
337 id: ZoneId(2),
338 rect: Rect::new(50.0, 0.0, 100.0, 100.0),
339 order: 1,
340 },
341 ],
342 max_distance: 0.0,
343 }
344 }
345
346 #[test]
347 fn pointer_within_preserves_later_overlap_precedence() {
348 let ranked = rank_collisions(
349 CollisionDetector::BuiltIn(CollisionStrategy::PointerWithin),
350 request(Point::new(75.0, 50.0)),
351 );
352 assert_eq!(ranked[0].zone, ZoneId(2));
353 }
354
355 #[test]
356 fn recovery_radius_uses_distance_to_rect_edge() {
357 let mut request = request(Point::new(160.0, 50.0));
358 request.max_distance = 12.0;
359 let ranked = rank_collisions(CollisionDetector::default(), request);
360 assert_eq!(ranked[0].zone, ZoneId(2));
361 }
362
363 #[test]
364 fn rect_intersection_does_not_require_pointer_inside_target() {
365 let ranked = rank_collisions(
366 CollisionDetector::BuiltIn(CollisionStrategy::RectIntersection),
367 CollisionRequest {
368 pointer: Point::new(25.0, 50.0),
369 active_rect: Some(Rect::new(25.0, 25.0, 50.0, 50.0)),
370 payload: (),
371 candidates: vec![ZoneCandidate {
372 id: ZoneId(1),
373 rect: Rect::new(60.0, 25.0, 50.0, 50.0),
374 order: 0,
375 }],
376 max_distance: 0.0,
377 },
378 );
379 assert_eq!(ranked[0].zone, ZoneId(1));
380 }
381
382 #[test]
383 fn rect_intersection_exact_hover_requires_shape_overlap() {
384 let ranked = rank_collisions(
385 CollisionDetector::BuiltIn(CollisionStrategy::RectIntersection),
386 CollisionRequest {
387 pointer: Point::new(75.0, 50.0),
388 active_rect: Some(Rect::new(0.0, 0.0, 20.0, 20.0)),
389 payload: (),
390 candidates: vec![ZoneCandidate {
391 id: ZoneId(1),
392 rect: Rect::new(50.0, 0.0, 100.0, 100.0),
393 order: 0,
394 }],
395 max_distance: 0.0,
396 },
397 );
398 assert!(ranked.is_empty());
399 }
400
401 #[test]
402 fn closest_center_uses_the_active_shape_and_can_rank_pointer_outside() {
403 let ranked = rank_collisions(
404 CollisionDetector::BuiltIn(CollisionStrategy::ClosestCenter),
405 CollisionRequest {
406 pointer: Point::new(10.0, 10.0),
407 active_rect: Some(Rect::new(40.0, 0.0, 40.0, 40.0)),
408 payload: (),
409 candidates: vec![
410 ZoneCandidate {
411 id: ZoneId(1),
412 rect: Rect::new(60.0, 0.0, 40.0, 40.0),
413 order: 0,
414 },
415 ZoneCandidate {
416 id: ZoneId(2),
417 rect: Rect::new(500.0, 0.0, 40.0, 40.0),
418 order: 1,
419 },
420 ],
421 max_distance: 0.0,
422 },
423 );
424 assert_eq!(ranked[0].zone, ZoneId(1));
425 }
426
427 fn custom_ranking_probe() -> Element {
428 let ranked = rank_collisions(
429 CollisionDetector::Custom(Callback::new(|_| {
430 vec![
431 Collision::new(ZoneId(1), 10.0),
432 Collision::new(ZoneId(2), 1.0),
433 Collision::new(ZoneId(3), 1.0),
434 ]
435 })),
436 CollisionRequest {
437 pointer: Point::default(),
438 active_rect: None,
439 payload: (),
440 candidates: vec![
441 ZoneCandidate::new(ZoneId(1), Rect::default(), 0),
442 ZoneCandidate::new(ZoneId(2), Rect::default(), 1),
443 ZoneCandidate::new(ZoneId(3), Rect::default(), 2),
444 ],
445 max_distance: 0.0,
446 },
447 );
448 assert_eq!(
449 ranked
450 .iter()
451 .map(|collision| collision.zone)
452 .collect::<Vec<_>>(),
453 [ZoneId(3), ZoneId(2), ZoneId(1)]
454 );
455 rsx! {}
456 }
457
458 #[test]
459 fn custom_results_are_sorted_by_score_then_registration_order() {
460 let mut dom = VirtualDom::new(custom_ranking_probe);
461 dom.rebuild_in_place();
462 }
463}