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