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