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() {
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 event.is_consumed() {
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 event.is_consumed() {
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
249impl HitTestTarget for HitRegion {
250 fn node_id(&self) -> NodeId {
251 self.node_id
252 }
253
254 fn capture_path(&self) -> Vec<NodeId> {
255 self.capture_path.clone()
256 }
257
258 fn dispatch(&self, event: PointerEvent) {
259 self.dispatch_cached_handlers(event);
260 }
261
262 fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
263 if let Some(modifier_slices) = self.live_modifier_slices(applier) {
264 self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
265 return;
266 }
267
268 self.diagnostics.record_live_modifier_slice_lookup_miss();
269 self.dispatch_cached_handlers(event);
270 }
271}
272
273pub struct Scene {
274 pub graph: Option<RenderGraph>,
275 pub hits: Vec<HitRegion>,
276 pub next_hit_z: usize,
277 pub node_index: HashMap<NodeId, usize>,
278 diagnostics: Rc<RenderDiagnostics>,
279}
280
281impl Scene {
282 pub fn new() -> Self {
283 Self {
284 graph: None,
285 hits: Vec::new(),
286 next_hit_z: 0,
287 node_index: HashMap::new(),
288 diagnostics: Rc::new(RenderDiagnostics::new()),
289 }
290 }
291
292 pub fn diagnostics(&self) -> &RenderDiagnostics {
293 self.diagnostics.as_ref()
294 }
295
296 pub fn push_hit(
297 &mut self,
298 node_id: NodeId,
299 capture_path: Vec<NodeId>,
300 geometry: HitGeometry,
301 shape: Option<RoundedCornerShape>,
302 click_actions: Vec<ClickAction>,
303 pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
304 ) {
305 if click_actions.is_empty() && pointer_inputs.is_empty() {
306 return;
307 }
308
309 let z_index = self.next_hit_z;
310 self.next_hit_z += 1;
311 let hit_index = self.hits.len();
312 self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
313 node_id,
314 capture_path,
315 geometry,
316 shape,
317 click_actions,
318 pointer_inputs,
319 z_index,
320 diagnostics: Rc::clone(&self.diagnostics),
321 }));
322 self.node_index.insert(node_id, hit_index);
323 }
324
325 pub fn replace_graph(&mut self, graph: RenderGraph) {
326 self.graph = Some(graph);
327 }
328}
329
330impl Default for Scene {
331 fn default() -> Self {
332 Self::new()
333 }
334}
335
336impl RenderScene for Scene {
337 type HitTarget = HitRegion;
338
339 fn clear(&mut self) {
340 self.graph = None;
341 self.hits.clear();
342 self.node_index.clear();
343 self.next_hit_z = 0;
344 }
345
346 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
347 let mut hit_indices: Vec<usize> = self
348 .hits
349 .iter()
350 .enumerate()
351 .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
352 .collect();
353
354 hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
355 hit_indices
356 .into_iter()
357 .map(|index| self.hits[index].clone())
358 .collect()
359 }
360
361 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
362 self.node_index
363 .get(&node_id)
364 .and_then(|&index| self.hits.get(index))
365 .cloned()
366 }
367}
368
369fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
370 if !rect.contains(point.x, point.y) {
371 return false;
372 }
373
374 let local_x = point.x - rect.x;
375 let local_y = point.y - rect.y;
376 let radii = shape.resolve(rect.width, rect.height);
377 let tl = radii.top_left;
378 let tr = radii.top_right;
379 let bl = radii.bottom_left;
380 let br = radii.bottom_right;
381
382 if local_x < tl && local_y < tl {
383 let dx = tl - local_x;
384 let dy = tl - local_y;
385 return dx * dx + dy * dy <= tl * tl;
386 }
387
388 if local_x > rect.width - tr && local_y < tr {
389 let dx = local_x - (rect.width - tr);
390 let dy = tr - local_y;
391 return dx * dx + dy * dy <= tr * tr;
392 }
393
394 if local_x < bl && local_y > rect.height - bl {
395 let dx = bl - local_x;
396 let dy = local_y - (rect.height - bl);
397 return dx * dx + dy * dy <= bl * bl;
398 }
399
400 if local_x > rect.width - br && local_y > rect.height - br {
401 let dx = local_x - (rect.width - br);
402 let dy = local_y - (rect.height - br);
403 return dx * dx + dy * dy <= br * br;
404 }
405
406 true
407}
408
409fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
410 point_in_triangle(point, quad[0], quad[1], quad[3])
411 || point_in_triangle(point, quad[0], quad[3], quad[2])
412}
413
414fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
415 let d1 = triangle_sign(point, a, b);
416 let d2 = triangle_sign(point, b, c);
417 let d3 = triangle_sign(point, c, a);
418 let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
419 let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
420 !(has_negative && has_positive)
421}
422
423fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
424 (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use std::cell::Cell;
431
432 fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
433 [
434 [rect.x, rect.y],
435 [rect.x + rect.width, rect.y],
436 [rect.x, rect.y + rect.height],
437 [rect.x + rect.width, rect.y + rect.height],
438 ]
439 }
440
441 fn translated_world_to_local(rect: Rect) -> ProjectiveTransform {
442 ProjectiveTransform::translation(-rect.x, -rect.y)
443 }
444
445 fn local_bounds_for_rect(rect: Rect) -> Rect {
446 Rect {
447 x: 0.0,
448 y: 0.0,
449 width: rect.width,
450 height: rect.height,
451 }
452 }
453
454 fn hit_geometry_for_rect(rect: Rect) -> HitGeometry {
455 HitGeometry {
456 rect,
457 quad: rect_to_quad(rect),
458 local_bounds: local_bounds_for_rect(rect),
459 world_to_local: translated_world_to_local(rect),
460 hit_clip_bounds: None,
461 hit_clips: Vec::new(),
462 }
463 }
464
465 fn test_diagnostics() -> Rc<RenderDiagnostics> {
466 Rc::new(RenderDiagnostics::new())
467 }
468
469 fn make_handler(counter: Rc<Cell<u32>>, consume: bool) -> Rc<dyn Fn(PointerEvent)> {
470 Rc::new(move |event: PointerEvent| {
471 counter.set(counter.get() + 1);
472 if consume {
473 event.consume();
474 }
475 })
476 }
477
478 #[test]
479 fn hit_test_respects_hit_clip() {
480 let mut scene = Scene::new();
481 let rect = Rect {
482 x: 0.0,
483 y: 0.0,
484 width: 100.0,
485 height: 100.0,
486 };
487 let clip = Rect {
488 x: 0.0,
489 y: 0.0,
490 width: 40.0,
491 height: 40.0,
492 };
493 scene.push_hit(
494 1,
495 vec![1],
496 HitGeometry {
497 hit_clip_bounds: Some(clip),
498 hit_clips: vec![HitClip {
499 quad: rect_to_quad(clip),
500 bounds: clip,
501 }],
502 ..hit_geometry_for_rect(rect)
503 },
504 None,
505 Vec::new(),
506 vec![Rc::new(|_event: PointerEvent| {})],
507 );
508
509 assert!(scene.hit_test(60.0, 20.0).is_empty());
510 assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
511 }
512
513 #[test]
514 fn hit_test_sorts_by_z_without_duplicating_hit_storage() {
515 let mut scene = Scene::new();
516 let rect = Rect {
517 x: 0.0,
518 y: 0.0,
519 width: 50.0,
520 height: 50.0,
521 };
522
523 scene.push_hit(
524 1,
525 vec![1],
526 hit_geometry_for_rect(rect),
527 None,
528 Vec::new(),
529 vec![Rc::new(|_event: PointerEvent| {})],
530 );
531 scene.push_hit(
532 2,
533 vec![2],
534 hit_geometry_for_rect(rect),
535 None,
536 Vec::new(),
537 vec![Rc::new(|_event: PointerEvent| {})],
538 );
539
540 assert_eq!(scene.node_index.get(&1), Some(&0));
541 assert_eq!(scene.node_index.get(&2), Some(&1));
542
543 let hits = scene.hit_test(10.0, 10.0);
544 assert_eq!(
545 hits.iter().map(|hit| hit.node_id).collect::<Vec<_>>(),
546 vec![2, 1]
547 );
548 assert_eq!(scene.find_target(1).map(|hit| hit.node_id), Some(1));
549 assert_eq!(scene.find_target(2).map(|hit| hit.node_id), Some(2));
550 }
551
552 #[test]
553 fn hit_test_rejects_points_in_rounded_corner_cutout() {
554 let mut scene = Scene::new();
555 let rect = Rect {
556 x: 0.0,
557 y: 0.0,
558 width: 40.0,
559 height: 40.0,
560 };
561 scene.push_hit(
562 1,
563 vec![1],
564 hit_geometry_for_rect(rect),
565 Some(RoundedCornerShape::uniform(20.0)),
566 Vec::new(),
567 vec![Rc::new(|_event: PointerEvent| {})],
568 );
569
570 assert!(scene.hit_test(1.0, 1.0).is_empty());
571 assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
572 }
573
574 #[test]
575 fn render_diagnostics_claim_each_warning_key_once() {
576 let diagnostics = RenderDiagnostics::new();
577
578 assert!(diagnostics.claim_warning_once("pixels.effect-fallback"));
579 assert!(!diagnostics.claim_warning_once("pixels.effect-fallback"));
580 assert!(diagnostics.claim_warning_once("pixels.blend-fallback"));
581 }
582
583 #[test]
584 fn dispatch_stops_after_event_consumed() {
585 let count_first = Rc::new(Cell::new(0));
586 let count_second = Rc::new(Cell::new(0));
587
588 let hit = HitRegion::with_diagnostics(HitRegionInit {
589 node_id: 1,
590 capture_path: vec![1],
591 geometry: hit_geometry_for_rect(Rect {
592 x: 0.0,
593 y: 0.0,
594 width: 50.0,
595 height: 50.0,
596 }),
597 shape: None,
598 click_actions: Vec::new(),
599 pointer_inputs: vec![
600 make_handler(count_first.clone(), true),
601 make_handler(count_second.clone(), false),
602 ],
603 z_index: 0,
604 diagnostics: test_diagnostics(),
605 });
606
607 let event = PointerEvent::new(
608 PointerEventKind::Down,
609 Point { x: 10.0, y: 10.0 },
610 Point { x: 10.0, y: 10.0 },
611 );
612 hit.dispatch(event);
613
614 assert_eq!(count_first.get(), 1);
615 assert_eq!(count_second.get(), 0);
616 }
617
618 #[test]
619 fn dispatch_triggers_click_action_on_down() {
620 let click_count = Rc::new(Cell::new(0));
621 let click_count_for_handler = Rc::clone(&click_count);
622 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
623 click_count_for_handler.set(click_count_for_handler.get() + 1);
624 })));
625
626 let hit = HitRegion::with_diagnostics(HitRegionInit {
627 node_id: 1,
628 capture_path: vec![1],
629 geometry: hit_geometry_for_rect(Rect {
630 x: 0.0,
631 y: 0.0,
632 width: 50.0,
633 height: 50.0,
634 }),
635 shape: None,
636 click_actions: vec![click_action],
637 pointer_inputs: Vec::new(),
638 z_index: 0,
639 diagnostics: test_diagnostics(),
640 });
641
642 hit.dispatch(PointerEvent::new(
643 PointerEventKind::Down,
644 Point { x: 10.0, y: 10.0 },
645 Point { x: 10.0, y: 10.0 },
646 ));
647 hit.dispatch(PointerEvent::new(
648 PointerEventKind::Move,
649 Point { x: 10.0, y: 10.0 },
650 Point { x: 12.0, y: 12.0 },
651 ));
652
653 assert_eq!(click_count.get(), 1);
654 }
655
656 #[test]
657 fn dispatch_passes_local_position_to_click_action() {
658 let local_positions = Rc::new(RefCell::new(Vec::new()));
659 let local_positions_for_handler = Rc::clone(&local_positions);
660 let click_action = ClickAction::WithPoint(Rc::new(move |point| {
661 local_positions_for_handler.borrow_mut().push(point);
662 }));
663
664 let hit = HitRegion::with_diagnostics(HitRegionInit {
665 node_id: 1,
666 capture_path: vec![1],
667 geometry: hit_geometry_for_rect(Rect {
668 x: 10.0,
669 y: 12.0,
670 width: 50.0,
671 height: 50.0,
672 }),
673 shape: None,
674 click_actions: vec![click_action],
675 pointer_inputs: Vec::new(),
676 z_index: 0,
677 diagnostics: test_diagnostics(),
678 });
679
680 hit.dispatch(PointerEvent::new(
681 PointerEventKind::Down,
682 Point { x: 15.0, y: 17.0 },
683 Point { x: 15.0, y: 17.0 },
684 ));
685
686 assert_eq!(*local_positions.borrow(), vec![Point { x: 5.0, y: 5.0 }]);
687 }
688
689 #[test]
690 fn dispatch_does_not_trigger_click_action_when_consumed() {
691 let click_count = Rc::new(Cell::new(0));
692 let click_count_for_handler = Rc::clone(&click_count);
693 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
694 click_count_for_handler.set(click_count_for_handler.get() + 1);
695 })));
696
697 let hit = HitRegion::with_diagnostics(HitRegionInit {
698 node_id: 1,
699 capture_path: vec![1],
700 geometry: hit_geometry_for_rect(Rect {
701 x: 0.0,
702 y: 0.0,
703 width: 50.0,
704 height: 50.0,
705 }),
706 shape: None,
707 click_actions: vec![click_action],
708 pointer_inputs: vec![Rc::new(|event: PointerEvent| event.consume())],
709 z_index: 0,
710 diagnostics: test_diagnostics(),
711 });
712
713 hit.dispatch(PointerEvent::new(
714 PointerEventKind::Down,
715 Point { x: 10.0, y: 10.0 },
716 Point { x: 10.0, y: 10.0 },
717 ));
718
719 assert_eq!(click_count.get(), 0);
720 }
721
722 #[test]
723 fn hit_test_uses_exact_quad_for_transformed_region() {
724 let mut scene = Scene::new();
725 let rect = Rect {
726 x: 0.0,
727 y: 0.0,
728 width: 40.0,
729 height: 20.0,
730 };
731 let quad = [[10.0, 10.0], [50.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
732 let world_to_local = ProjectiveTransform::from_rect_to_quad(rect, quad)
733 .inverse()
734 .expect("transformed hit region should be invertible");
735 scene.push_hit(
736 1,
737 vec![1],
738 HitGeometry {
739 rect: Rect {
740 x: 10.0,
741 y: 10.0,
742 width: 50.0,
743 height: 20.0,
744 },
745 quad,
746 local_bounds: rect,
747 world_to_local,
748 hit_clip_bounds: None,
749 hit_clips: Vec::new(),
750 },
751 None,
752 Vec::new(),
753 vec![Rc::new(|_event: PointerEvent| {})],
754 );
755
756 assert!(
757 scene.hit_test(15.0, 28.0).is_empty(),
758 "point inside the quad bounds but outside the transformed quad must not hit"
759 );
760 assert_eq!(scene.hit_test(30.0, 20.0).len(), 1);
761 }
762
763 #[test]
764 fn dispatch_uses_inverse_transform_for_local_position() {
765 let local_positions = Rc::new(RefCell::new(Vec::new()));
766 let local_positions_for_handler = Rc::clone(&local_positions);
767 let click_action = ClickAction::WithPoint(Rc::new(move |point| {
768 local_positions_for_handler.borrow_mut().push(point);
769 }));
770 let local_bounds = Rect {
771 x: 0.0,
772 y: 0.0,
773 width: 20.0,
774 height: 10.0,
775 };
776 let quad = [[20.0, 10.0], [60.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
777 let world_to_local = ProjectiveTransform::from_rect_to_quad(local_bounds, quad)
778 .inverse()
779 .expect("translated quad should be invertible");
780 let hit = HitRegion::with_diagnostics(HitRegionInit {
781 node_id: 1,
782 capture_path: vec![1],
783 geometry: HitGeometry {
784 rect: Rect {
785 x: 20.0,
786 y: 10.0,
787 width: 40.0,
788 height: 20.0,
789 },
790 quad,
791 local_bounds,
792 world_to_local,
793 hit_clip_bounds: None,
794 hit_clips: Vec::new(),
795 },
796 shape: None,
797 click_actions: vec![click_action],
798 pointer_inputs: Vec::new(),
799 z_index: 0,
800 diagnostics: test_diagnostics(),
801 });
802
803 hit.dispatch(PointerEvent::new(
804 PointerEventKind::Down,
805 Point { x: 25.0, y: 17.0 },
806 Point { x: 25.0, y: 17.0 },
807 ));
808
809 assert_eq!(*local_positions.borrow(), vec![Point { x: 2.5, y: 3.5 }]);
810 }
811
812 #[test]
813 fn dispatch_with_applier_counts_live_modifier_slice_lookup_misses() {
814 let handler_calls = Rc::new(Cell::new(0));
815 let handler_calls_for_handler = Rc::clone(&handler_calls);
816 let diagnostics = test_diagnostics();
817 let hit = HitRegion::with_diagnostics(HitRegionInit {
818 node_id: 42,
819 capture_path: vec![42],
820 geometry: hit_geometry_for_rect(Rect {
821 x: 0.0,
822 y: 0.0,
823 width: 50.0,
824 height: 50.0,
825 }),
826 shape: None,
827 click_actions: Vec::new(),
828 pointer_inputs: vec![Rc::new(move |_event: PointerEvent| {
829 handler_calls_for_handler.set(handler_calls_for_handler.get() + 1);
830 })],
831 z_index: 0,
832 diagnostics: Rc::clone(&diagnostics),
833 });
834 let misses_before = diagnostics.live_modifier_slice_lookup_miss_count();
835 let mut applier = MemoryApplier::new();
836
837 hit.dispatch_with_applier(
838 &mut applier,
839 PointerEvent::new(
840 PointerEventKind::Down,
841 Point { x: 10.0, y: 10.0 },
842 Point { x: 10.0, y: 10.0 },
843 ),
844 );
845
846 assert_eq!(handler_calls.get(), 1);
847 assert_eq!(
848 diagnostics.live_modifier_slice_lookup_miss_count(),
849 misses_before + 1
850 );
851 }
852}