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