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, Copy, Debug, PartialEq)]
55pub struct HitClip {
56    pub quad: [[f32; 2]; 4],
57    pub bounds: Rect,
58}
59
60/// Geometry for a hit target, borrowing the clip chain until the sink records it.
61#[derive(Clone, Copy)]
62pub struct HitGeometry<'a> {
63    pub rect: Rect,
64    pub quad: [[f32; 2]; 4],
65    pub local_bounds: Rect,
66    pub world_to_local: ProjectiveTransform,
67    pub hit_clip_bounds: Option<Rect>,
68    pub hit_clips: &'a [HitClip],
69}
70
71/// What a hit target answers with: the shape that narrows its bounds, and the
72/// node's modifier slices, which hold the pointer handlers it dispatches to
73/// and the pointer icon it asks for while hovered.
74pub struct HitTargetSpec<'a> {
75    /// Narrows the target's rectangle to a rounded shape, so a point in a
76    /// corner cutout misses it.
77    pub shape: Option<RoundedCornerShape>,
78    /// The node's slices, shared by the target rather than copied.
79    pub handlers: &'a Rc<ModifierNodeSlices>,
80}
81
82#[derive(Clone)]
83pub struct HitRegion {
84    pub node_id: NodeId,
85    pub capture_path: Vec<NodeId>,
86    pub rect: Rect,
87    pub quad: [[f32; 2]; 4],
88    pub local_bounds: Rect,
89    pub world_to_local: ProjectiveTransform,
90    pub shape: Option<RoundedCornerShape>,
91    /// The node's modifier slices, shared with the render graph: its pointer
92    /// handlers and pointer icon.
93    pub handlers: Rc<ModifierNodeSlices>,
94    pub z_index: usize,
95    pub hit_clip_bounds: Option<Rect>,
96    pub hit_clips: Vec<HitClip>,
97    diagnostics: Rc<RenderDiagnostics>,
98}
99
100struct HitRegionInit<'a> {
101    node_id: NodeId,
102    capture_path: Vec<NodeId>,
103    geometry: HitGeometry<'a>,
104    clip_buffer: Vec<HitClip>,
105    shape: Option<RoundedCornerShape>,
106    handlers: Rc<ModifierNodeSlices>,
107    z_index: usize,
108    diagnostics: Rc<RenderDiagnostics>,
109}
110
111impl Default for HitRegionInit<'_> {
112    fn default() -> Self {
113        Self {
114            node_id: 0,
115            capture_path: Vec::new(),
116            geometry: HitGeometry {
117                rect: Rect {
118                    x: 0.0,
119                    y: 0.0,
120                    width: 0.0,
121                    height: 0.0,
122                },
123                quad: [[0.0, 0.0]; 4],
124                local_bounds: Rect {
125                    x: 0.0,
126                    y: 0.0,
127                    width: 0.0,
128                    height: 0.0,
129                },
130                world_to_local: ProjectiveTransform::identity(),
131                hit_clip_bounds: None,
132                hit_clips: &[],
133            },
134            clip_buffer: Vec::new(),
135            shape: None,
136            handlers: Rc::default(),
137            z_index: 0,
138            diagnostics: Rc::new(RenderDiagnostics::new()),
139        }
140    }
141}
142
143impl HitRegion {
144    fn with_diagnostics(init: HitRegionInit<'_>) -> Self {
145        let HitRegionInit {
146            node_id,
147            capture_path,
148            geometry,
149            clip_buffer: mut hit_clips,
150            shape,
151            handlers,
152            z_index,
153            diagnostics,
154        } = init;
155        let HitGeometry {
156            rect,
157            quad,
158            local_bounds,
159            world_to_local,
160            hit_clip_bounds,
161            hit_clips: clips,
162        } = geometry;
163        hit_clips.extend_from_slice(clips);
164        Self {
165            node_id,
166            capture_path,
167            rect,
168            quad,
169            local_bounds,
170            world_to_local,
171            shape,
172            handlers,
173            z_index,
174            hit_clip_bounds,
175            hit_clips,
176            diagnostics,
177        }
178    }
179
180    fn contains(&self, x: f32, y: f32) -> bool {
181        if !self.rect.contains(x, y) {
182            return false;
183        }
184
185        if let Some(clip_bounds) = self.hit_clip_bounds
186            && !clip_bounds.contains(x, y)
187        {
188            return false;
189        }
190
191        let point = Point { x, y };
192        if !point_in_quad(point, self.quad) {
193            return false;
194        }
195
196        for clip in &self.hit_clips {
197            if !point_in_quad(point, clip.quad) {
198                return false;
199            }
200        }
201
202        let local_point = self.world_to_local.map_point(point);
203        if let Some(shape) = self.shape {
204            point_in_rounded_rect(local_point, self.local_bounds, shape)
205        } else {
206            self.local_bounds.contains(local_point.x, local_point.y)
207        }
208    }
209
210    /// The squared distance from the point to this target when the point lies
211    /// inside the target grown to the minimum touch size and the target takes
212    /// input. None when the target is large enough on its own, when the point
213    /// is outside its reach, or when a clip cuts the point off.
214    fn reach_distance(&self, x: f32, y: f32) -> Option<f32> {
215        if self.handlers.pointer_inputs().is_empty() {
216            return None;
217        }
218        if let Some(clip_bounds) = self.hit_clip_bounds
219            && !clip_bounds.contains(x, y)
220        {
221            return None;
222        }
223        let grow_x = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.width) / 2.0).max(0.0);
224        let grow_y = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.height) / 2.0).max(0.0);
225        if grow_x <= 0.0 && grow_y <= 0.0 {
226            return None;
227        }
228        let right = self.rect.x + self.rect.width;
229        let bottom = self.rect.y + self.rect.height;
230        let in_reach = x >= self.rect.x - grow_x
231            && x <= right + grow_x
232            && y >= self.rect.y - grow_y
233            && y <= bottom + grow_y;
234        if !in_reach {
235            return None;
236        }
237        let dx = (self.rect.x - x).max(x - right).max(0.0);
238        let dy = (self.rect.y - y).max(y - bottom).max(0.0);
239        Some(dx * dx + dy * dy)
240    }
241
242    fn localize_event(&self, event: &PointerEvent) -> PointerEvent {
243        let local = self.world_to_local.map_point(event.global_position);
244        event.copy_with_local_position(Point {
245            x: local.x - self.local_bounds.x,
246            y: local.y - self.local_bounds.y,
247        })
248    }
249
250    fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
251        if should_skip_consumed_event(&event) {
252            return;
253        }
254
255        modifier_slices.dispatch_pointer_event(self.localize_event(&event));
256    }
257
258    fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
259        if let Ok(modifier_slices) =
260            applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
261        {
262            return Some(modifier_slices);
263        }
264
265        applier
266            .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
267                node.modifier_slices_snapshot()
268            })
269            .ok()
270    }
271}
272
273fn is_terminal_pointer_event(kind: PointerEventKind) -> bool {
274    matches!(kind, PointerEventKind::Up | PointerEventKind::Cancel)
275}
276
277fn should_skip_consumed_event(event: &PointerEvent) -> bool {
278    event.is_consumed() && !is_terminal_pointer_event(event.kind)
279}
280
281impl HitTestTarget for HitRegion {
282    fn node_id(&self) -> NodeId {
283        self.node_id
284    }
285
286    fn pointer_icon(&self) -> Option<PointerIcon> {
287        self.handlers.pointer_icon().cloned()
288    }
289
290    fn capture_path(&self) -> Vec<NodeId> {
291        self.capture_path.clone()
292    }
293
294    fn dispatch(&self, event: PointerEvent) {
295        self.dispatch_modifier_slices(&self.handlers, event);
296    }
297
298    fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
299        if let Some(modifier_slices) = self.live_modifier_slices(applier) {
300            self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
301            return;
302        }
303
304        self.diagnostics.record_live_modifier_slice_lookup_miss();
305        self.dispatch_modifier_slices(&self.handlers, event);
306    }
307}
308
309#[derive(Default)]
310struct HitBuffers {
311    hit_clips: Vec<HitClip>,
312    capture_path: Vec<NodeId>,
313}
314
315pub struct Scene {
316    pub graph: Option<RenderGraph>,
317    pub hits: Vec<HitRegion>,
318    hit_buffers: Vec<HitBuffers>,
319    pub next_hit_z: usize,
320    pub node_index: HashMap<NodeId, usize>,
321    diagnostics: Rc<RenderDiagnostics>,
322}
323
324impl Scene {
325    pub fn new() -> Self {
326        Self {
327            graph: None,
328            hits: Vec::new(),
329            hit_buffers: Vec::new(),
330            next_hit_z: 0,
331            node_index: HashMap::new(),
332            diagnostics: Rc::new(RenderDiagnostics::new()),
333        }
334    }
335
336    pub fn diagnostics(&self) -> &RenderDiagnostics {
337        self.diagnostics.as_ref()
338    }
339
340    /// Adds an interactive target in draw order, ignoring targets that neither
341    /// handle a pointer nor name a pointer icon.
342    pub fn push_hit(
343        &mut self,
344        node_id: NodeId,
345        capture_path: &[NodeId],
346        geometry: HitGeometry<'_>,
347        target: HitTargetSpec<'_>,
348    ) {
349        let HitTargetSpec { shape, handlers } = target;
350        if handlers.pointer_inputs().is_empty() && handlers.pointer_icon().is_none() {
351            return;
352        }
353        let mut buffers = self.hit_buffers.pop().unwrap_or_default();
354        buffers.capture_path.extend_from_slice(capture_path);
355
356        let z_index = self.next_hit_z;
357        self.next_hit_z += 1;
358        let hit_index = self.hits.len();
359        self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
360            node_id,
361            capture_path: buffers.capture_path,
362            geometry,
363            clip_buffer: buffers.hit_clips,
364            shape,
365            handlers: Rc::clone(handlers),
366            z_index,
367            diagnostics: Rc::clone(&self.diagnostics),
368        }));
369        self.node_index.insert(node_id, hit_index);
370    }
371
372    /// Removes all hit targets and releases their handlers while retaining vector capacity.
373    pub fn clear_hits(&mut self) {
374        self.hit_buffers.clear();
375        for hit in self.hits.drain(..) {
376            let mut buffers = HitBuffers {
377                hit_clips: hit.hit_clips,
378                capture_path: hit.capture_path,
379            };
380            buffers.hit_clips.clear();
381            buffers.capture_path.clear();
382            self.hit_buffers.push(buffers);
383        }
384        self.node_index.clear();
385        self.next_hit_z = 0;
386    }
387
388    pub fn replace_graph(&mut self, graph: RenderGraph) {
389        self.graph = Some(graph);
390    }
391}
392
393impl Default for Scene {
394    fn default() -> Self {
395        Self::new()
396    }
397}
398
399impl RenderScene for Scene {
400    type HitTarget = HitRegion;
401
402    fn clear(&mut self) {
403        self.graph = None;
404        self.clear_hits();
405    }
406
407    fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
408        let mut hit_indices: Vec<usize> = self
409            .hits
410            .iter()
411            .enumerate()
412            .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
413            .collect();
414
415        hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
416        hit_indices
417            .into_iter()
418            .map(|index| self.hits[index].clone())
419            .collect()
420    }
421
422    fn hit_test_near(&self, x: f32, y: f32) -> Option<Self::HitTarget> {
423        self.hits
424            .iter()
425            .filter_map(|hit| hit.reach_distance(x, y).map(|distance| (distance, hit)))
426            .min_by(|(near, hit), (other_near, other)| {
427                near.total_cmp(other_near)
428                    .then_with(|| other.z_index.cmp(&hit.z_index))
429            })
430            .map(|(_, hit)| hit.clone())
431    }
432
433    fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
434        self.node_index
435            .get(&node_id)
436            .and_then(|&index| self.hits.get(index))
437            .cloned()
438    }
439
440    fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
441        if let Some(graph) = &self.graph {
442            graph.collect_retained_visual_observation_nodes(nodes);
443        } else {
444            nodes.clear();
445        }
446        true
447    }
448}
449
450fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
451    if !rect.contains(point.x, point.y) {
452        return false;
453    }
454
455    let local_x = point.x - rect.x;
456    let local_y = point.y - rect.y;
457    let radii = shape.resolve(rect.width, rect.height);
458    let tl = radii.top_left;
459    let tr = radii.top_right;
460    let bl = radii.bottom_left;
461    let br = radii.bottom_right;
462
463    if local_x < tl && local_y < tl {
464        let dx = tl - local_x;
465        let dy = tl - local_y;
466        return dx * dx + dy * dy <= tl * tl;
467    }
468
469    if local_x > rect.width - tr && local_y < tr {
470        let dx = local_x - (rect.width - tr);
471        let dy = tr - local_y;
472        return dx * dx + dy * dy <= tr * tr;
473    }
474
475    if local_x < bl && local_y > rect.height - bl {
476        let dx = bl - local_x;
477        let dy = local_y - (rect.height - bl);
478        return dx * dx + dy * dy <= bl * bl;
479    }
480
481    if local_x > rect.width - br && local_y > rect.height - br {
482        let dx = local_x - (rect.width - br);
483        let dy = local_y - (rect.height - br);
484        return dx * dx + dy * dy <= br * br;
485    }
486
487    true
488}
489
490fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
491    point_in_triangle(point, quad[0], quad[1], quad[3])
492        || point_in_triangle(point, quad[0], quad[3], quad[2])
493}
494
495fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
496    let d1 = triangle_sign(point, a, b);
497    let d2 = triangle_sign(point, b, c);
498    let d3 = triangle_sign(point, c, a);
499    let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
500    let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
501    !(has_negative && has_positive)
502}
503
504fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
505    (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
506}
507
508#[cfg(test)]
509#[path = "tests/graph_scene_tests.rs"]
510mod tests;