Skip to main content

cranpose_render_common/
graph_scene.rs

1use std::{
2    cell::{Cell, RefCell},
3    cmp::Reverse,
4    rc::Rc,
5};
6
7use cranpose_core::{
8    MemoryApplier, NodeId,
9    collections::map::{HashMap, HashSet},
10};
11use cranpose_foundation::{MINIMUM_TOUCH_TARGET_SIZE, PointerEvent, PointerEventKind};
12use cranpose_ui::{LayoutNode, ModifierNodeSlices, SubcomposeLayoutNode};
13use cranpose_ui_graphics::{Point, PointerIcon, Rect, RoundedCornerShape};
14
15use crate::{
16    HitTestTarget, RenderScene, SceneUpdates,
17    graph::{ProjectiveTransform, RenderGraph},
18};
19
20/// Counters and once-only warnings that a scene collects while it dispatches pointer events.
21pub struct RenderDiagnostics {
22    reported_warnings: RefCell<HashSet<&'static str>>,
23    live_modifier_slice_lookup_miss_count: Cell<usize>,
24}
25
26impl RenderDiagnostics {
27    /// Creates diagnostics with no warnings reported and no lookup misses counted.
28    pub fn new() -> Self {
29        Self {
30            reported_warnings: RefCell::new(HashSet::default()),
31            live_modifier_slice_lookup_miss_count: Cell::new(0),
32        }
33    }
34
35    /// Returns true the first time it is called with `key`, so that a warning is reported once.
36    pub fn claim_warning_once(&self, key: &'static str) -> bool {
37        self.reported_warnings.borrow_mut().insert(key)
38    }
39
40    /// Counts one pointer dispatch that fell back to the captured modifier slices because the live
41    /// node could not be found.
42    pub fn record_live_modifier_slice_lookup_miss(&self) {
43        self.live_modifier_slice_lookup_miss_count.set(
44            self.live_modifier_slice_lookup_miss_count
45                .get()
46                .saturating_add(1),
47        );
48    }
49
50    /// Number of pointer dispatches that did not find the live node and used the modifier slices
51    /// captured at build time instead.
52    pub fn live_modifier_slice_lookup_miss_count(&self) -> usize {
53        self.live_modifier_slice_lookup_miss_count.get()
54    }
55}
56
57impl Default for RenderDiagnostics {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63/// A clip that cuts a hit target, kept as a transformed rectangle.
64#[derive(Clone, Copy, Debug, PartialEq)]
65pub struct HitClip {
66    /// The clip rectangle after the layer transforms, as four corners in window space.
67    pub quad: [[f32; 2]; 4],
68    /// Axis-aligned bounds of `quad` in window coordinates.
69    pub bounds: Rect,
70}
71
72/// Geometry for a hit target, borrowing the clip chain until the sink records it.
73#[derive(Clone, Copy)]
74pub struct HitGeometry<'a> {
75    /// Axis-aligned bounds of `quad` in window coordinates.
76    pub rect: Rect,
77    /// The node's rectangle after the layer transforms, as four corners in window space.
78    pub quad: [[f32; 2]; 4],
79    /// The node's own rectangle in its local space.
80    pub local_bounds: Rect,
81    /// Maps window coordinates into the node's local space.
82    pub world_to_local: ProjectiveTransform,
83    /// Axis-aligned bounds of the ancestors' clips in window space, or `None` when none clips.
84    pub hit_clip_bounds: Option<Rect>,
85    /// Clips of the ancestors that cut the target, as transformed quads in window space.
86    pub hit_clips: &'a [HitClip],
87}
88
89/// What a hit target answers with: the shape that narrows its bounds, and the
90/// node's modifier slices, which hold the pointer handlers it dispatches to
91/// and the pointer icon it asks for while hovered.
92pub struct HitTargetSpec<'a> {
93    /// Narrows the target's rectangle to a rounded shape, so a point in a
94    /// corner cutout misses it.
95    pub shape: Option<RoundedCornerShape>,
96    /// The node's slices, shared by the target rather than copied.
97    pub handlers: &'a Rc<ModifierNodeSlices>,
98}
99
100/// A hit target in window space: the node's geometry, clips and pointer handlers.
101#[derive(Clone)]
102pub struct HitRegion {
103    /// The layout node that owns the target.
104    pub node_id: NodeId,
105    /// Nodes that keep receiving the move, up and cancel events of a gesture that starts here: this
106    /// node first, then its pointer-input ancestors from the nearest outward.
107    pub capture_path: Vec<NodeId>,
108    /// Axis-aligned bounds of `quad` in window coordinates.
109    pub rect: Rect,
110    /// The node's rectangle after the layer transforms, as four corners in window space.
111    pub quad: [[f32; 2]; 4],
112    /// The node's own rectangle in its local space.
113    pub local_bounds: Rect,
114    /// Maps window coordinates into the node's local space.
115    pub world_to_local: ProjectiveTransform,
116    /// Rounded shape that narrows the target's rectangle, or `None` for a plain rectangle.
117    pub shape: Option<RoundedCornerShape>,
118    /// The node's modifier slices, shared with the render graph: its pointer
119    /// handlers and pointer icon.
120    pub handlers: Rc<ModifierNodeSlices>,
121    /// Draw order of the target. A larger value is drawn later and is hit first.
122    pub z_index: usize,
123    /// Axis-aligned bounds of the ancestors' clips in window space, or `None` when none clips.
124    pub hit_clip_bounds: Option<Rect>,
125    /// Clips of the ancestors that cut this target, as transformed quads in window space.
126    pub hit_clips: Vec<HitClip>,
127    diagnostics: Rc<RenderDiagnostics>,
128}
129
130struct HitRegionInit<'a> {
131    node_id: NodeId,
132    capture_path: Vec<NodeId>,
133    geometry: HitGeometry<'a>,
134    clip_buffer: Vec<HitClip>,
135    shape: Option<RoundedCornerShape>,
136    handlers: Rc<ModifierNodeSlices>,
137    z_index: usize,
138    diagnostics: Rc<RenderDiagnostics>,
139}
140
141impl Default for HitRegionInit<'_> {
142    fn default() -> Self {
143        Self {
144            node_id: 0,
145            capture_path: Vec::new(),
146            geometry: HitGeometry {
147                rect: Rect {
148                    x: 0.0,
149                    y: 0.0,
150                    width: 0.0,
151                    height: 0.0,
152                },
153                quad: [[0.0, 0.0]; 4],
154                local_bounds: Rect {
155                    x: 0.0,
156                    y: 0.0,
157                    width: 0.0,
158                    height: 0.0,
159                },
160                world_to_local: ProjectiveTransform::identity(),
161                hit_clip_bounds: None,
162                hit_clips: &[],
163            },
164            clip_buffer: Vec::new(),
165            shape: None,
166            handlers: Rc::default(),
167            z_index: 0,
168            diagnostics: Rc::new(RenderDiagnostics::new()),
169        }
170    }
171}
172
173impl HitRegion {
174    fn with_diagnostics(init: HitRegionInit<'_>) -> Self {
175        let HitRegionInit {
176            node_id,
177            capture_path,
178            geometry,
179            clip_buffer: mut hit_clips,
180            shape,
181            handlers,
182            z_index,
183            diagnostics,
184        } = init;
185        let HitGeometry {
186            rect,
187            quad,
188            local_bounds,
189            world_to_local,
190            hit_clip_bounds,
191            hit_clips: clips,
192        } = geometry;
193        hit_clips.extend_from_slice(clips);
194        Self {
195            node_id,
196            capture_path,
197            rect,
198            quad,
199            local_bounds,
200            world_to_local,
201            shape,
202            handlers,
203            z_index,
204            hit_clip_bounds,
205            hit_clips,
206            diagnostics,
207        }
208    }
209
210    fn contains(&self, x: f32, y: f32) -> bool {
211        if !self.rect.contains(x, y) {
212            return false;
213        }
214
215        if let Some(clip_bounds) = self.hit_clip_bounds
216            && !clip_bounds.contains(x, y)
217        {
218            return false;
219        }
220
221        let point = Point { x, y };
222        if !point_in_quad(point, self.quad) {
223            return false;
224        }
225
226        for clip in &self.hit_clips {
227            if !point_in_quad(point, clip.quad) {
228                return false;
229            }
230        }
231
232        let local_point = self.world_to_local.map_point(point);
233        if let Some(shape) = self.shape {
234            point_in_rounded_rect(local_point, self.local_bounds, shape)
235        } else {
236            self.local_bounds.contains(local_point.x, local_point.y)
237        }
238    }
239
240    /// The squared distance from the point to this target when the point lies
241    /// inside the target grown to the minimum touch size and the target takes
242    /// input. None when the target is large enough on its own, when the point
243    /// is outside its reach, or when a clip cuts the point off.
244    fn reach_distance(&self, x: f32, y: f32) -> Option<f32> {
245        if self.handlers.pointer_inputs().is_empty() {
246            return None;
247        }
248        if let Some(clip_bounds) = self.hit_clip_bounds
249            && !clip_bounds.contains(x, y)
250        {
251            return None;
252        }
253        let grow_x = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.width) / 2.0).max(0.0);
254        let grow_y = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.height) / 2.0).max(0.0);
255        if grow_x <= 0.0 && grow_y <= 0.0 {
256            return None;
257        }
258        let right = self.rect.x + self.rect.width;
259        let bottom = self.rect.y + self.rect.height;
260        let in_reach = x >= self.rect.x - grow_x
261            && x <= right + grow_x
262            && y >= self.rect.y - grow_y
263            && y <= bottom + grow_y;
264        if !in_reach {
265            return None;
266        }
267        let dx = (self.rect.x - x).max(x - right).max(0.0);
268        let dy = (self.rect.y - y).max(y - bottom).max(0.0);
269        Some(dx * dx + dy * dy)
270    }
271
272    fn localize_event(&self, event: &PointerEvent) -> PointerEvent {
273        let local = self.world_to_local.map_point(event.global_position);
274        event.copy_with_local_position(Point {
275            x: local.x - self.local_bounds.x,
276            y: local.y - self.local_bounds.y,
277        })
278    }
279
280    fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
281        if should_skip_consumed_event(&event) {
282            return;
283        }
284
285        modifier_slices.dispatch_pointer_event(self.localize_event(&event));
286    }
287
288    fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
289        if let Ok(modifier_slices) =
290            applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
291        {
292            return Some(modifier_slices);
293        }
294
295        applier
296            .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
297                node.modifier_slices_snapshot()
298            })
299            .ok()
300    }
301}
302
303fn is_terminal_pointer_event(kind: PointerEventKind) -> bool {
304    matches!(kind, PointerEventKind::Up | PointerEventKind::Cancel)
305}
306
307fn should_skip_consumed_event(event: &PointerEvent) -> bool {
308    event.is_consumed() && !is_terminal_pointer_event(event.kind)
309}
310
311impl HitTestTarget for HitRegion {
312    fn node_id(&self) -> NodeId {
313        self.node_id
314    }
315
316    fn pointer_icon(&self) -> Option<PointerIcon> {
317        self.handlers.pointer_icon().cloned()
318    }
319
320    fn capture_path(&self) -> Vec<NodeId> {
321        self.capture_path.clone()
322    }
323
324    fn dispatch(&self, event: PointerEvent) {
325        self.dispatch_modifier_slices(&self.handlers, event);
326    }
327
328    fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
329        if let Some(modifier_slices) = self.live_modifier_slices(applier) {
330            self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
331            return;
332        }
333
334        self.diagnostics.record_live_modifier_slice_lookup_miss();
335        self.dispatch_modifier_slices(&self.handlers, event);
336    }
337}
338
339#[derive(Default)]
340struct HitBuffers {
341    hit_clips: Vec<HitClip>,
342    capture_path: Vec<NodeId>,
343}
344
345/// The retained render graph of a window together with the hit targets built from it.
346pub struct Scene {
347    /// The retained render graph, or `None` before the first build.
348    pub graph: Option<RenderGraph>,
349    /// Hit targets in draw order, the last drawn at the end.
350    pub hits: Vec<HitRegion>,
351    hit_buffers: Vec<HitBuffers>,
352    /// Draw order index that the next pushed hit target receives.
353    pub next_hit_z: usize,
354    /// Position in `hits` of the target of each node, for finding a target by node id.
355    pub node_index: HashMap<NodeId, usize>,
356    diagnostics: Rc<RenderDiagnostics>,
357}
358
359impl Scene {
360    /// Creates an empty scene with no graph and no hit targets.
361    pub fn new() -> Self {
362        Self {
363            graph: None,
364            hits: Vec::new(),
365            hit_buffers: Vec::new(),
366            next_hit_z: 0,
367            node_index: HashMap::default(),
368            diagnostics: Rc::new(RenderDiagnostics::new()),
369        }
370    }
371
372    /// The diagnostics shared by the scene and its hit targets.
373    pub fn diagnostics(&self) -> &RenderDiagnostics {
374        self.diagnostics.as_ref()
375    }
376
377    /// Rebuilds the retained graph and hit targets from the applier, reusing
378    /// storage from the previous graph.
379    pub fn rebuild_from_applier(&mut self, applier: &MemoryApplier, root: NodeId) {
380        let previous = self.graph.take();
381        self.clear_hits();
382        let Some(graph) =
383            crate::scene_builder::rebuild_graph_from_applier(applier, root, 1.0, previous)
384        else {
385            return;
386        };
387        self.install_graph_with_hits(graph);
388    }
389
390    fn install_graph_with_hits(&mut self, graph: RenderGraph) {
391        crate::hit_graph::collect_hits_from_graph(
392            &graph.root,
393            ProjectiveTransform::identity(),
394            self,
395            None,
396        );
397        self.replace_graph(graph);
398    }
399
400    /// Applies content and layer-property changes to the retained graph.
401    /// Empty updates or an unavailable scoped update rebuild the scene.
402    /// Hit targets are refreshed when their geometry changes or `refresh_hits`
403    /// is true, as required after layout or pointer-handler changes.
404    pub fn update_from_applier(
405        &mut self,
406        applier: &MemoryApplier,
407        root: NodeId,
408        updates: SceneUpdates<'_>,
409        refresh_hits: bool,
410    ) {
411        let report = if updates.is_empty() {
412            None
413        } else {
414            self.graph.as_mut().map(|graph| {
415                crate::scene_builder::update_graph_from_applier_report(applier, graph, updates, 1.0)
416            })
417        };
418        let Some(report) = report.filter(|report| report.applied()) else {
419            self.rebuild_from_applier(applier, root);
420            return;
421        };
422        if !refresh_hits && !report.hit_graph_dirty {
423            return;
424        }
425        self.clear_hits();
426        if let Some(graph) = self.graph.take() {
427            self.install_graph_with_hits(graph);
428        }
429    }
430
431    /// Adds an interactive target in draw order, ignoring targets that neither
432    /// handle a pointer nor name a pointer icon.
433    pub fn push_hit(
434        &mut self,
435        node_id: NodeId,
436        capture_path: &[NodeId],
437        geometry: HitGeometry<'_>,
438        target: HitTargetSpec<'_>,
439    ) {
440        let HitTargetSpec { shape, handlers } = target;
441        if handlers.pointer_inputs().is_empty() && handlers.pointer_icon().is_none() {
442            return;
443        }
444        let mut buffers = self.hit_buffers.pop().unwrap_or_default();
445        buffers.capture_path.extend_from_slice(capture_path);
446
447        let z_index = self.next_hit_z;
448        self.next_hit_z += 1;
449        let hit_index = self.hits.len();
450        self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
451            node_id,
452            capture_path: buffers.capture_path,
453            geometry,
454            clip_buffer: buffers.hit_clips,
455            shape,
456            handlers: Rc::clone(handlers),
457            z_index,
458            diagnostics: Rc::clone(&self.diagnostics),
459        }));
460        self.node_index.insert(node_id, hit_index);
461    }
462
463    /// Removes all hit targets and releases their handlers while retaining vector capacity.
464    pub fn clear_hits(&mut self) {
465        self.hit_buffers.clear();
466        for hit in self.hits.drain(..) {
467            let mut buffers = HitBuffers {
468                hit_clips: hit.hit_clips,
469                capture_path: hit.capture_path,
470            };
471            buffers.hit_clips.clear();
472            buffers.capture_path.clear();
473            self.hit_buffers.push(buffers);
474        }
475        self.node_index.clear();
476        self.next_hit_z = 0;
477    }
478
479    /// Replaces the retained render graph and leaves the hit targets as they are.
480    pub fn replace_graph(&mut self, graph: RenderGraph) {
481        self.graph = Some(graph);
482    }
483}
484
485impl Default for Scene {
486    fn default() -> Self {
487        Self::new()
488    }
489}
490
491impl RenderScene for Scene {
492    type HitTarget = HitRegion;
493
494    fn clear(&mut self) {
495        self.graph = None;
496        self.clear_hits();
497    }
498
499    fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
500        let mut hit_indices: Vec<usize> = self
501            .hits
502            .iter()
503            .enumerate()
504            .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
505            .collect();
506
507        hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
508        hit_indices
509            .into_iter()
510            .map(|index| self.hits[index].clone())
511            .collect()
512    }
513
514    fn hit_test_near(&self, x: f32, y: f32) -> Option<Self::HitTarget> {
515        self.hits
516            .iter()
517            .filter_map(|hit| hit.reach_distance(x, y).map(|distance| (distance, hit)))
518            .min_by(|(near, hit), (other_near, other)| {
519                near.total_cmp(other_near)
520                    .then_with(|| other.z_index.cmp(&hit.z_index))
521            })
522            .map(|(_, hit)| hit.clone())
523    }
524
525    fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
526        self.node_index
527            .get(&node_id)
528            .and_then(|&index| self.hits.get(index))
529            .cloned()
530    }
531
532    fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
533        if let Some(graph) = &self.graph {
534            graph.collect_retained_visual_observation_nodes(nodes);
535        } else {
536            nodes.clear();
537        }
538        true
539    }
540}
541
542fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
543    if !rect.contains(point.x, point.y) {
544        return false;
545    }
546
547    let local_x = point.x - rect.x;
548    let local_y = point.y - rect.y;
549    let radii = shape.resolve(rect.width, rect.height);
550    let tl = radii.top_left;
551    let tr = radii.top_right;
552    let bl = radii.bottom_left;
553    let br = radii.bottom_right;
554
555    if local_x < tl && local_y < tl {
556        let dx = tl - local_x;
557        let dy = tl - local_y;
558        return dx * dx + dy * dy <= tl * tl;
559    }
560
561    if local_x > rect.width - tr && local_y < tr {
562        let dx = local_x - (rect.width - tr);
563        let dy = tr - local_y;
564        return dx * dx + dy * dy <= tr * tr;
565    }
566
567    if local_x < bl && local_y > rect.height - bl {
568        let dx = bl - local_x;
569        let dy = local_y - (rect.height - bl);
570        return dx * dx + dy * dy <= bl * bl;
571    }
572
573    if local_x > rect.width - br && local_y > rect.height - br {
574        let dx = local_x - (rect.width - br);
575        let dy = local_y - (rect.height - br);
576        return dx * dx + dy * dy <= br * br;
577    }
578
579    true
580}
581
582fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
583    point_in_triangle(point, quad[0], quad[1], quad[3])
584        || point_in_triangle(point, quad[0], quad[3], quad[2])
585}
586
587fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
588    let d1 = triangle_sign(point, a, b);
589    let d2 = triangle_sign(point, b, c);
590    let d3 = triangle_sign(point, c, a);
591    let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
592    let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
593    !(has_negative && has_positive)
594}
595
596fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
597    (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
598}
599
600#[cfg(test)]
601#[path = "tests/graph_scene_tests.rs"]
602mod tests;