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