Skip to main content

cranpose_render_common/
graph_scene.rs

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/// Geometry for a hit target, borrowing the clip chain until the sink records it.
76#[derive(Clone, Copy)]
77pub struct HitGeometry<'a> {
78    pub rect: Rect,
79    pub quad: [[f32; 2]; 4],
80    pub local_bounds: Rect,
81    pub world_to_local: ProjectiveTransform,
82    pub hit_clip_bounds: Option<Rect>,
83    pub hit_clips: &'a [HitClip],
84}
85
86/// What a hit target answers with: the shape that narrows its bounds, the
87/// handlers it dispatches to, and the pointer icon it asks for while hovered.
88pub struct HitTargetSpec<'a, I> {
89    /// Narrows the target's rectangle to a rounded shape, so a point in a
90    /// corner cutout misses it.
91    pub shape: Option<RoundedCornerShape>,
92    /// Click handlers, invoked on an unconsumed press inside the target.
93    pub click_actions: I,
94    /// Raw pointer handlers, invoked for every event the target receives.
95    pub pointer_inputs: &'a [Rc<dyn Fn(PointerEvent)>],
96    /// The pointer's appearance while it hovers this target.
97    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    /// The squared distance from the point to this target when the point lies
237    /// inside the target grown to the minimum touch size and the target takes
238    /// input. None when the target is large enough on its own, when the point
239    /// is outside its reach, or when a clip cuts the point off.
240    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    /// Adds an interactive target in draw order, ignoring targets that neither
405    /// handle a pointer nor name a pointer icon.
406    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    /// Removes all hit targets and releases their handlers while retaining vector capacity.
449    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}