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::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/// 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, 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    /// Adds an interactive target in draw order, ignoring targets that neither
411    /// handle a pointer nor name a pointer icon.
412    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    /// Removes all hit targets and releases their handlers while retaining vector capacity.
455    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}