1use std::{
2 cell::{Cell, RefCell},
3 cmp::Reverse,
4 collections::{HashMap, HashSet},
5 rc::Rc,
6};
7
8use cranpose_core::{MemoryApplier, NodeId};
9use cranpose_foundation::{PointerEvent, PointerEventKind};
10use cranpose_ui::{LayoutNode, ModifierNodeSlices, SubcomposeLayoutNode};
11use cranpose_ui_graphics::{Point, Rect, RoundedCornerShape};
12
13use crate::{
14 HitTestTarget, RenderScene,
15 graph::{ProjectiveTransform, RenderGraph},
16};
17
18pub struct RenderDiagnostics {
19 reported_warnings: RefCell<HashSet<&'static str>>,
20 live_modifier_slice_lookup_miss_count: Cell<usize>,
21}
22
23impl RenderDiagnostics {
24 pub fn new() -> Self {
25 Self {
26 reported_warnings: RefCell::new(HashSet::new()),
27 live_modifier_slice_lookup_miss_count: Cell::new(0),
28 }
29 }
30
31 pub fn claim_warning_once(&self, key: &'static str) -> bool {
32 self.reported_warnings.borrow_mut().insert(key)
33 }
34
35 pub fn record_live_modifier_slice_lookup_miss(&self) {
36 self.live_modifier_slice_lookup_miss_count.set(
37 self.live_modifier_slice_lookup_miss_count
38 .get()
39 .saturating_add(1),
40 );
41 }
42
43 pub fn live_modifier_slice_lookup_miss_count(&self) -> usize {
44 self.live_modifier_slice_lookup_miss_count.get()
45 }
46}
47
48impl Default for RenderDiagnostics {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54#[derive(Clone)]
55pub enum ClickAction {
56 Simple(Rc<RefCell<dyn FnMut()>>),
57 WithPoint(Rc<dyn Fn(Point)>),
58}
59
60impl ClickAction {
61 fn invoke(&self, local_position: Point) {
62 match self {
63 ClickAction::Simple(handler) => (handler.borrow_mut())(),
64 ClickAction::WithPoint(handler) => handler(local_position),
65 }
66 }
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub struct HitClip {
71 pub quad: [[f32; 2]; 4],
72 pub bounds: Rect,
73}
74
75#[derive(Clone)]
76pub struct HitGeometry {
77 pub rect: Rect,
78 pub quad: [[f32; 2]; 4],
79 pub local_bounds: Rect,
80 pub world_to_local: ProjectiveTransform,
81 pub hit_clip_bounds: Option<Rect>,
82 pub hit_clips: Vec<HitClip>,
83}
84
85#[derive(Clone)]
86pub struct HitRegion {
87 pub node_id: NodeId,
88 pub capture_path: Vec<NodeId>,
89 pub rect: Rect,
90 pub quad: [[f32; 2]; 4],
91 pub local_bounds: Rect,
92 pub world_to_local: ProjectiveTransform,
93 pub shape: Option<RoundedCornerShape>,
94 pub click_actions: Vec<ClickAction>,
95 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
96 pub z_index: usize,
97 pub hit_clip_bounds: Option<Rect>,
98 pub hit_clips: Vec<HitClip>,
99 diagnostics: Rc<RenderDiagnostics>,
100}
101
102struct HitRegionInit {
103 node_id: NodeId,
104 capture_path: Vec<NodeId>,
105 geometry: HitGeometry,
106 shape: Option<RoundedCornerShape>,
107 click_actions: Vec<ClickAction>,
108 pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
109 z_index: usize,
110 diagnostics: Rc<RenderDiagnostics>,
111}
112
113impl Default for HitRegionInit {
114 fn default() -> Self {
115 Self {
116 node_id: 0,
117 capture_path: Vec::new(),
118 geometry: HitGeometry {
119 rect: Rect {
120 x: 0.0,
121 y: 0.0,
122 width: 0.0,
123 height: 0.0,
124 },
125 quad: [[0.0, 0.0]; 4],
126 local_bounds: Rect {
127 x: 0.0,
128 y: 0.0,
129 width: 0.0,
130 height: 0.0,
131 },
132 world_to_local: ProjectiveTransform::identity(),
133 hit_clip_bounds: None,
134 hit_clips: Vec::new(),
135 },
136 shape: None,
137 click_actions: Vec::new(),
138 pointer_inputs: Vec::new(),
139 z_index: 0,
140 diagnostics: Rc::new(RenderDiagnostics::new()),
141 }
142 }
143}
144
145impl HitRegion {
146 fn with_diagnostics(init: HitRegionInit) -> Self {
147 let HitRegionInit {
148 node_id,
149 capture_path,
150 geometry,
151 shape,
152 click_actions,
153 pointer_inputs,
154 z_index,
155 diagnostics,
156 } = init;
157 let HitGeometry {
158 rect,
159 quad,
160 local_bounds,
161 world_to_local,
162 hit_clip_bounds,
163 hit_clips,
164 } = geometry;
165 Self {
166 node_id,
167 capture_path,
168 rect,
169 quad,
170 local_bounds,
171 world_to_local,
172 shape,
173 click_actions,
174 pointer_inputs,
175 z_index,
176 hit_clip_bounds,
177 hit_clips,
178 diagnostics,
179 }
180 }
181
182 fn contains(&self, x: f32, y: f32) -> bool {
183 if !self.rect.contains(x, y) {
184 return false;
185 }
186
187 if let Some(clip_bounds) = self.hit_clip_bounds
188 && !clip_bounds.contains(x, y)
189 {
190 return false;
191 }
192
193 let point = Point { x, y };
194 if !point_in_quad(point, self.quad) {
195 return false;
196 }
197
198 for clip in &self.hit_clips {
199 if !point_in_quad(point, clip.quad) {
200 return false;
201 }
202 }
203
204 let local_point = self.world_to_local.map_point(point);
205 if let Some(shape) = self.shape {
206 point_in_rounded_rect(local_point, self.local_bounds, shape)
207 } else {
208 self.local_bounds.contains(local_point.x, local_point.y)
209 }
210 }
211
212 fn localize_event(&self, event: &PointerEvent) -> (PointerEvent, Point) {
213 let local = self.world_to_local.map_point(event.global_position);
214 let local_position = Point {
215 x: local.x - self.local_bounds.x,
216 y: local.y - self.local_bounds.y,
217 };
218 (
219 event.copy_with_local_position(local_position),
220 local_position,
221 )
222 }
223
224 fn dispatch_pointer_inputs(
225 pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
226 local_event: &PointerEvent,
227 ) {
228 for handler in pointer_inputs {
229 if local_event.is_consumed() && !is_terminal_pointer_event(local_event.kind) {
230 break;
231 }
232 handler(local_event.clone());
233 }
234 }
235
236 fn dispatch_click_actions(&self, local_position: Point) {
237 for action in &self.click_actions {
238 action.invoke(local_position);
239 }
240 }
241
242 fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
243 if should_skip_consumed_event(&event) {
244 return;
245 }
246
247 let (local_event, local_position) = self.localize_event(&event);
248 Self::dispatch_pointer_inputs(modifier_slices.pointer_inputs(), &local_event);
249
250 if event.kind == PointerEventKind::Down && !local_event.is_consumed() {
251 for handler in modifier_slices.click_handlers() {
252 handler(local_position);
253 }
254 }
255 }
256
257 fn dispatch_cached_handlers(&self, event: PointerEvent) {
258 if should_skip_consumed_event(&event) {
259 return;
260 }
261
262 let (local_event, local_position) = self.localize_event(&event);
263 Self::dispatch_pointer_inputs(&self.pointer_inputs, &local_event);
264
265 if event.kind == PointerEventKind::Down && !local_event.is_consumed() {
266 self.dispatch_click_actions(local_position);
267 }
268 }
269
270 fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
271 if let Ok(modifier_slices) =
272 applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
273 {
274 return Some(modifier_slices);
275 }
276
277 applier
278 .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
279 node.modifier_slices_snapshot()
280 })
281 .ok()
282 }
283}
284
285fn is_terminal_pointer_event(kind: PointerEventKind) -> bool {
286 matches!(kind, PointerEventKind::Up | PointerEventKind::Cancel)
287}
288
289fn should_skip_consumed_event(event: &PointerEvent) -> bool {
290 event.is_consumed() && !is_terminal_pointer_event(event.kind)
291}
292
293impl HitTestTarget for HitRegion {
294 fn node_id(&self) -> NodeId {
295 self.node_id
296 }
297
298 fn capture_path(&self) -> Vec<NodeId> {
299 self.capture_path.clone()
300 }
301
302 fn dispatch(&self, event: PointerEvent) {
303 self.dispatch_cached_handlers(event);
304 }
305
306 fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
307 if let Some(modifier_slices) = self.live_modifier_slices(applier) {
308 self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
309 return;
310 }
311
312 self.diagnostics.record_live_modifier_slice_lookup_miss();
313 self.dispatch_cached_handlers(event);
314 }
315}
316
317pub struct Scene {
318 pub graph: Option<RenderGraph>,
319 pub hits: Vec<HitRegion>,
320 pub next_hit_z: usize,
321 pub node_index: HashMap<NodeId, usize>,
322 diagnostics: Rc<RenderDiagnostics>,
323}
324
325impl Scene {
326 pub fn new() -> Self {
327 Self {
328 graph: None,
329 hits: Vec::new(),
330 next_hit_z: 0,
331 node_index: HashMap::new(),
332 diagnostics: Rc::new(RenderDiagnostics::new()),
333 }
334 }
335
336 pub fn diagnostics(&self) -> &RenderDiagnostics {
337 self.diagnostics.as_ref()
338 }
339
340 pub fn push_hit(
341 &mut self,
342 node_id: NodeId,
343 capture_path: Vec<NodeId>,
344 geometry: HitGeometry,
345 shape: Option<RoundedCornerShape>,
346 click_actions: Vec<ClickAction>,
347 pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
348 ) {
349 if click_actions.is_empty() && pointer_inputs.is_empty() {
350 return;
351 }
352
353 let z_index = self.next_hit_z;
354 self.next_hit_z += 1;
355 let hit_index = self.hits.len();
356 self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
357 node_id,
358 capture_path,
359 geometry,
360 shape,
361 click_actions,
362 pointer_inputs,
363 z_index,
364 diagnostics: Rc::clone(&self.diagnostics),
365 }));
366 self.node_index.insert(node_id, hit_index);
367 }
368
369 pub fn replace_graph(&mut self, graph: RenderGraph) {
370 self.graph = Some(graph);
371 }
372}
373
374impl Default for Scene {
375 fn default() -> Self {
376 Self::new()
377 }
378}
379
380impl RenderScene for Scene {
381 type HitTarget = HitRegion;
382
383 fn clear(&mut self) {
384 self.graph = None;
385 self.hits.clear();
386 self.node_index.clear();
387 self.next_hit_z = 0;
388 }
389
390 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
391 let mut hit_indices: Vec<usize> = self
392 .hits
393 .iter()
394 .enumerate()
395 .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
396 .collect();
397
398 hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
399 hit_indices
400 .into_iter()
401 .map(|index| self.hits[index].clone())
402 .collect()
403 }
404
405 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
406 self.node_index
407 .get(&node_id)
408 .and_then(|&index| self.hits.get(index))
409 .cloned()
410 }
411
412 fn retained_visual_observation_nodes(&self) -> Option<HashSet<NodeId>> {
413 Some(
414 self.graph
415 .as_ref()
416 .map_or_else(HashSet::new, RenderGraph::retained_visual_observation_nodes),
417 )
418 }
419}
420
421fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
422 if !rect.contains(point.x, point.y) {
423 return false;
424 }
425
426 let local_x = point.x - rect.x;
427 let local_y = point.y - rect.y;
428 let radii = shape.resolve(rect.width, rect.height);
429 let tl = radii.top_left;
430 let tr = radii.top_right;
431 let bl = radii.bottom_left;
432 let br = radii.bottom_right;
433
434 if local_x < tl && local_y < tl {
435 let dx = tl - local_x;
436 let dy = tl - local_y;
437 return dx * dx + dy * dy <= tl * tl;
438 }
439
440 if local_x > rect.width - tr && local_y < tr {
441 let dx = local_x - (rect.width - tr);
442 let dy = tr - local_y;
443 return dx * dx + dy * dy <= tr * tr;
444 }
445
446 if local_x < bl && local_y > rect.height - bl {
447 let dx = bl - local_x;
448 let dy = local_y - (rect.height - bl);
449 return dx * dx + dy * dy <= bl * bl;
450 }
451
452 if local_x > rect.width - br && local_y > rect.height - br {
453 let dx = local_x - (rect.width - br);
454 let dy = local_y - (rect.height - br);
455 return dx * dx + dy * dy <= br * br;
456 }
457
458 true
459}
460
461fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
462 point_in_triangle(point, quad[0], quad[1], quad[3])
463 || point_in_triangle(point, quad[0], quad[3], quad[2])
464}
465
466fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
467 let d1 = triangle_sign(point, a, b);
468 let d2 = triangle_sign(point, b, c);
469 let d3 = triangle_sign(point, c, a);
470 let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
471 let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
472 !(has_negative && has_positive)
473}
474
475fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
476 (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
477}
478
479#[cfg(test)]
480mod tests {
481 use std::cell::Cell;
482
483 use super::*;
484
485 fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
486 [
487 [rect.x, rect.y],
488 [rect.x + rect.width, rect.y],
489 [rect.x, rect.y + rect.height],
490 [rect.x + rect.width, rect.y + rect.height],
491 ]
492 }
493
494 fn translated_world_to_local(rect: Rect) -> ProjectiveTransform {
495 ProjectiveTransform::translation(-rect.x, -rect.y)
496 }
497
498 fn local_bounds_for_rect(rect: Rect) -> Rect {
499 Rect {
500 x: 0.0,
501 y: 0.0,
502 width: rect.width,
503 height: rect.height,
504 }
505 }
506
507 fn hit_geometry_for_rect(rect: Rect) -> HitGeometry {
508 HitGeometry {
509 rect,
510 quad: rect_to_quad(rect),
511 local_bounds: local_bounds_for_rect(rect),
512 world_to_local: translated_world_to_local(rect),
513 hit_clip_bounds: None,
514 hit_clips: Vec::new(),
515 }
516 }
517
518 fn test_diagnostics() -> Rc<RenderDiagnostics> {
519 Rc::new(RenderDiagnostics::new())
520 }
521
522 fn make_handler(counter: Rc<Cell<u32>>, consume: bool) -> Rc<dyn Fn(PointerEvent)> {
523 Rc::new(move |event: PointerEvent| {
524 counter.set(counter.get() + 1);
525 if consume {
526 event.consume();
527 }
528 })
529 }
530
531 #[test]
532 fn hit_test_respects_hit_clip() {
533 let mut scene = Scene::new();
534 let rect = Rect {
535 x: 0.0,
536 y: 0.0,
537 width: 100.0,
538 height: 100.0,
539 };
540 let clip = Rect {
541 x: 0.0,
542 y: 0.0,
543 width: 40.0,
544 height: 40.0,
545 };
546 scene.push_hit(
547 1,
548 vec![1],
549 HitGeometry {
550 hit_clip_bounds: Some(clip),
551 hit_clips: vec![HitClip {
552 quad: rect_to_quad(clip),
553 bounds: clip,
554 }],
555 ..hit_geometry_for_rect(rect)
556 },
557 None,
558 Vec::new(),
559 vec![Rc::new(|_event: PointerEvent| {})],
560 );
561
562 assert!(scene.hit_test(60.0, 20.0).is_empty());
563 assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
564 }
565
566 #[test]
567 fn hit_test_sorts_by_z_without_duplicating_hit_storage() {
568 let mut scene = Scene::new();
569 let rect = Rect {
570 x: 0.0,
571 y: 0.0,
572 width: 50.0,
573 height: 50.0,
574 };
575
576 scene.push_hit(
577 1,
578 vec![1],
579 hit_geometry_for_rect(rect),
580 None,
581 Vec::new(),
582 vec![Rc::new(|_event: PointerEvent| {})],
583 );
584 scene.push_hit(
585 2,
586 vec![2],
587 hit_geometry_for_rect(rect),
588 None,
589 Vec::new(),
590 vec![Rc::new(|_event: PointerEvent| {})],
591 );
592
593 assert_eq!(scene.node_index.get(&1), Some(&0));
594 assert_eq!(scene.node_index.get(&2), Some(&1));
595
596 let hits = scene.hit_test(10.0, 10.0);
597 assert_eq!(
598 hits.iter().map(|hit| hit.node_id).collect::<Vec<_>>(),
599 vec![2, 1]
600 );
601 assert_eq!(scene.find_target(1).map(|hit| hit.node_id), Some(1));
602 assert_eq!(scene.find_target(2).map(|hit| hit.node_id), Some(2));
603 }
604
605 #[test]
606 fn hit_test_rejects_points_in_rounded_corner_cutout() {
607 let mut scene = Scene::new();
608 let rect = Rect {
609 x: 0.0,
610 y: 0.0,
611 width: 40.0,
612 height: 40.0,
613 };
614 scene.push_hit(
615 1,
616 vec![1],
617 hit_geometry_for_rect(rect),
618 Some(RoundedCornerShape::uniform(20.0)),
619 Vec::new(),
620 vec![Rc::new(|_event: PointerEvent| {})],
621 );
622
623 assert!(scene.hit_test(1.0, 1.0).is_empty());
624 assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
625 }
626
627 #[test]
628 fn render_diagnostics_claim_each_warning_key_once() {
629 let diagnostics = RenderDiagnostics::new();
630
631 assert!(diagnostics.claim_warning_once("pixels.effect-fallback"));
632 assert!(!diagnostics.claim_warning_once("pixels.effect-fallback"));
633 assert!(diagnostics.claim_warning_once("pixels.blend-fallback"));
634 }
635
636 #[test]
637 fn dispatch_stops_after_event_consumed() {
638 let count_first = Rc::new(Cell::new(0));
639 let count_second = Rc::new(Cell::new(0));
640
641 let hit = HitRegion::with_diagnostics(HitRegionInit {
642 node_id: 1,
643 capture_path: vec![1],
644 geometry: hit_geometry_for_rect(Rect {
645 x: 0.0,
646 y: 0.0,
647 width: 50.0,
648 height: 50.0,
649 }),
650 pointer_inputs: vec![
651 make_handler(count_first.clone(), true),
652 make_handler(count_second.clone(), false),
653 ],
654 ..Default::default()
655 });
656
657 let event = PointerEvent::new(
658 PointerEventKind::Down,
659 Point { x: 10.0, y: 10.0 },
660 Point { x: 10.0, y: 10.0 },
661 );
662 hit.dispatch(event);
663
664 assert_eq!(count_first.get(), 1);
665 assert_eq!(count_second.get(), 0);
666 }
667
668 #[test]
669 fn dispatch_delivers_terminal_events_after_consumption_for_cleanup() {
670 let count_first = Rc::new(Cell::new(0));
671 let count_second = Rc::new(Cell::new(0));
672
673 let hit = HitRegion::with_diagnostics(HitRegionInit {
674 node_id: 1,
675 capture_path: vec![1],
676 geometry: hit_geometry_for_rect(Rect {
677 x: 0.0,
678 y: 0.0,
679 width: 50.0,
680 height: 50.0,
681 }),
682 pointer_inputs: vec![
683 make_handler(count_first.clone(), true),
684 make_handler(count_second.clone(), false),
685 ],
686 ..Default::default()
687 });
688
689 for kind in [PointerEventKind::Up, PointerEventKind::Cancel] {
690 let event =
691 PointerEvent::new(kind, Point { x: 10.0, y: 10.0 }, Point { x: 10.0, y: 10.0 });
692 hit.dispatch(event);
693 }
694
695 assert_eq!(count_first.get(), 2);
696 assert_eq!(count_second.get(), 2);
697 }
698
699 #[test]
700 fn dispatch_delivers_terminal_events_to_later_captured_targets_after_consumption() {
701 let child_count = Rc::new(Cell::new(0));
702 let parent_count = Rc::new(Cell::new(0));
703
704 let child_hit = HitRegion::with_diagnostics(HitRegionInit {
705 node_id: 2,
706 capture_path: vec![2, 1],
707 geometry: hit_geometry_for_rect(Rect {
708 x: 8.0,
709 y: 8.0,
710 width: 20.0,
711 height: 20.0,
712 }),
713 pointer_inputs: vec![make_handler(child_count.clone(), true)],
714 z_index: 1,
715 ..Default::default()
716 });
717 let parent_hit = HitRegion::with_diagnostics(HitRegionInit {
718 node_id: 1,
719 capture_path: vec![1],
720 geometry: hit_geometry_for_rect(Rect {
721 x: 0.0,
722 y: 0.0,
723 width: 50.0,
724 height: 50.0,
725 }),
726 pointer_inputs: vec![make_handler(parent_count.clone(), false)],
727 ..Default::default()
728 });
729
730 let event = PointerEvent::new(
731 PointerEventKind::Up,
732 Point { x: 12.0, y: 12.0 },
733 Point { x: 12.0, y: 12.0 },
734 );
735 child_hit.dispatch(event.clone());
736 parent_hit.dispatch(event);
737
738 assert_eq!(child_count.get(), 1);
739 assert_eq!(parent_count.get(), 1);
740 }
741
742 #[test]
743 fn dispatch_triggers_click_action_on_down() {
744 let click_count = Rc::new(Cell::new(0));
745 let click_count_for_handler = Rc::clone(&click_count);
746 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
747 click_count_for_handler.set(click_count_for_handler.get() + 1);
748 })));
749
750 let hit = HitRegion::with_diagnostics(HitRegionInit {
751 node_id: 1,
752 capture_path: vec![1],
753 geometry: hit_geometry_for_rect(Rect {
754 x: 0.0,
755 y: 0.0,
756 width: 50.0,
757 height: 50.0,
758 }),
759 click_actions: vec![click_action],
760 ..Default::default()
761 });
762
763 hit.dispatch(PointerEvent::new(
764 PointerEventKind::Down,
765 Point { x: 10.0, y: 10.0 },
766 Point { x: 10.0, y: 10.0 },
767 ));
768 hit.dispatch(PointerEvent::new(
769 PointerEventKind::Move,
770 Point { x: 10.0, y: 10.0 },
771 Point { x: 12.0, y: 12.0 },
772 ));
773
774 assert_eq!(click_count.get(), 1);
775 }
776
777 #[test]
778 fn dispatch_passes_local_position_to_click_action() {
779 let local_positions = Rc::new(RefCell::new(Vec::new()));
780 let local_positions_for_handler = Rc::clone(&local_positions);
781 let click_action = ClickAction::WithPoint(Rc::new(move |point| {
782 local_positions_for_handler.borrow_mut().push(point);
783 }));
784
785 let hit = HitRegion::with_diagnostics(HitRegionInit {
786 node_id: 1,
787 capture_path: vec![1],
788 geometry: hit_geometry_for_rect(Rect {
789 x: 10.0,
790 y: 12.0,
791 width: 50.0,
792 height: 50.0,
793 }),
794 click_actions: vec![click_action],
795 ..Default::default()
796 });
797
798 hit.dispatch(PointerEvent::new(
799 PointerEventKind::Down,
800 Point { x: 15.0, y: 17.0 },
801 Point { x: 15.0, y: 17.0 },
802 ));
803
804 assert_eq!(*local_positions.borrow(), vec![Point { x: 5.0, y: 5.0 }]);
805 }
806
807 #[test]
808 fn dispatch_does_not_trigger_click_action_when_consumed() {
809 let click_count = Rc::new(Cell::new(0));
810 let click_count_for_handler = Rc::clone(&click_count);
811 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
812 click_count_for_handler.set(click_count_for_handler.get() + 1);
813 })));
814
815 let hit = HitRegion::with_diagnostics(HitRegionInit {
816 node_id: 1,
817 capture_path: vec![1],
818 geometry: hit_geometry_for_rect(Rect {
819 x: 0.0,
820 y: 0.0,
821 width: 50.0,
822 height: 50.0,
823 }),
824 click_actions: vec![click_action],
825 pointer_inputs: vec![Rc::new(|event: PointerEvent| event.consume())],
826 ..Default::default()
827 });
828
829 hit.dispatch(PointerEvent::new(
830 PointerEventKind::Down,
831 Point { x: 10.0, y: 10.0 },
832 Point { x: 10.0, y: 10.0 },
833 ));
834
835 assert_eq!(click_count.get(), 0);
836 }
837
838 #[test]
839 fn hit_test_uses_exact_quad_for_transformed_region() {
840 let mut scene = Scene::new();
841 let rect = Rect {
842 x: 0.0,
843 y: 0.0,
844 width: 40.0,
845 height: 20.0,
846 };
847 let quad = [[10.0, 10.0], [50.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
848 let world_to_local = ProjectiveTransform::from_rect_to_quad(rect, quad)
849 .inverse()
850 .expect("transformed hit region should be invertible");
851 scene.push_hit(
852 1,
853 vec![1],
854 HitGeometry {
855 rect: Rect {
856 x: 10.0,
857 y: 10.0,
858 width: 50.0,
859 height: 20.0,
860 },
861 quad,
862 local_bounds: rect,
863 world_to_local,
864 hit_clip_bounds: None,
865 hit_clips: Vec::new(),
866 },
867 None,
868 Vec::new(),
869 vec![Rc::new(|_event: PointerEvent| {})],
870 );
871
872 assert!(
873 scene.hit_test(15.0, 28.0).is_empty(),
874 "point inside the quad bounds but outside the transformed quad must not hit"
875 );
876 assert_eq!(scene.hit_test(30.0, 20.0).len(), 1);
877 }
878
879 #[test]
880 fn dispatch_uses_inverse_transform_for_local_position() {
881 let local_positions = Rc::new(RefCell::new(Vec::new()));
882 let local_positions_for_handler = Rc::clone(&local_positions);
883 let click_action = ClickAction::WithPoint(Rc::new(move |point| {
884 local_positions_for_handler.borrow_mut().push(point);
885 }));
886 let local_bounds = Rect {
887 x: 0.0,
888 y: 0.0,
889 width: 20.0,
890 height: 10.0,
891 };
892 let quad = [[20.0, 10.0], [60.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
893 let world_to_local = ProjectiveTransform::from_rect_to_quad(local_bounds, quad)
894 .inverse()
895 .expect("translated quad should be invertible");
896 let hit = HitRegion::with_diagnostics(HitRegionInit {
897 node_id: 1,
898 capture_path: vec![1],
899 geometry: HitGeometry {
900 rect: Rect {
901 x: 20.0,
902 y: 10.0,
903 width: 40.0,
904 height: 20.0,
905 },
906 quad,
907 local_bounds,
908 world_to_local,
909 hit_clip_bounds: None,
910 hit_clips: Vec::new(),
911 },
912 click_actions: vec![click_action],
913 ..Default::default()
914 });
915
916 hit.dispatch(PointerEvent::new(
917 PointerEventKind::Down,
918 Point { x: 25.0, y: 17.0 },
919 Point { x: 25.0, y: 17.0 },
920 ));
921
922 assert_eq!(*local_positions.borrow(), vec![Point { x: 2.5, y: 3.5 }]);
923 }
924
925 #[test]
926 fn dispatch_with_applier_counts_live_modifier_slice_lookup_misses() {
927 let handler_calls = Rc::new(Cell::new(0));
928 let handler_calls_for_handler = Rc::clone(&handler_calls);
929 let diagnostics = test_diagnostics();
930 let hit = HitRegion::with_diagnostics(HitRegionInit {
931 node_id: 42,
932 capture_path: vec![42],
933 geometry: hit_geometry_for_rect(Rect {
934 x: 0.0,
935 y: 0.0,
936 width: 50.0,
937 height: 50.0,
938 }),
939 pointer_inputs: vec![Rc::new(move |_event: PointerEvent| {
940 handler_calls_for_handler.set(handler_calls_for_handler.get() + 1);
941 })],
942 diagnostics: Rc::clone(&diagnostics),
943 ..Default::default()
944 });
945 let misses_before = diagnostics.live_modifier_slice_lookup_miss_count();
946 let mut applier = MemoryApplier::new();
947
948 hit.dispatch_with_applier(
949 &mut applier,
950 PointerEvent::new(
951 PointerEventKind::Down,
952 Point { x: 10.0, y: 10.0 },
953 Point { x: 10.0, y: 10.0 },
954 ),
955 );
956
957 assert_eq!(handler_calls.get(), 1);
958 assert_eq!(
959 diagnostics.live_modifier_slice_lookup_miss_count(),
960 misses_before + 1
961 );
962 }
963}