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