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