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)]
589#[path = "tests/graph_scene_tests.rs"]
590mod tests;