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, space::Ray};
14
15use crate::{
16    HitTestTarget, RenderScene, SceneUpdates,
17    graph::{ProjectiveTransform, RenderGraph},
18    scene_pick::ScenePick,
19};
20
21/// Counters and once-only warnings that a scene collects while it dispatches pointer events.
22pub struct RenderDiagnostics {
23    reported_warnings: RefCell<HashSet<&'static str>>,
24    live_modifier_slice_lookup_miss_count: Cell<usize>,
25}
26
27impl RenderDiagnostics {
28    /// Creates diagnostics with no warnings reported and no lookup misses counted.
29    pub fn new() -> Self {
30        Self {
31            reported_warnings: RefCell::new(HashSet::default()),
32            live_modifier_slice_lookup_miss_count: Cell::new(0),
33        }
34    }
35
36    /// Returns true the first time it is called with `key`, so that a warning is reported once.
37    pub fn claim_warning_once(&self, key: &'static str) -> bool {
38        self.reported_warnings.borrow_mut().insert(key)
39    }
40
41    /// Counts one pointer dispatch that fell back to the captured modifier slices because the live
42    /// node could not be found.
43    pub fn record_live_modifier_slice_lookup_miss(&self) {
44        self.live_modifier_slice_lookup_miss_count.set(
45            self.live_modifier_slice_lookup_miss_count
46                .get()
47                .saturating_add(1),
48        );
49    }
50
51    /// Number of pointer dispatches that did not find the live node and used the modifier slices
52    /// captured at build time instead.
53    pub fn live_modifier_slice_lookup_miss_count(&self) -> usize {
54        self.live_modifier_slice_lookup_miss_count.get()
55    }
56}
57
58impl Default for RenderDiagnostics {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64/// A clip that cuts a hit target, kept as a transformed rectangle.
65#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct HitClip {
67    /// The clip rectangle after the layer transforms, as four corners in window space.
68    pub quad: [[f32; 2]; 4],
69    /// Axis-aligned bounds of `quad` in window coordinates.
70    pub bounds: Rect,
71}
72
73/// A mesh surface of a scene, as the hit targets on its content see it:
74/// the scene's pick, which surface it is, and the clips above it.
75pub struct SurfaceTarget {
76    /// Finds the surface and content point a window point shows.
77    pub pick: Rc<ScenePick>,
78    /// The surface's index in `pick`.
79    pub index: usize,
80    /// Axis-aligned bounds of the clips above the surface, in window space.
81    pub outer_clip_bounds: Option<Rect>,
82    /// The clips above the surface, as quads in window space.
83    pub outer_clips: Vec<HitClip>,
84}
85
86/// How many parts a target's rect splits into along each side for its
87/// bounds in the window: on a curved surface the corners alone miss the
88/// bulge between them.
89const WINDOW_BOUNDS_STEPS: u16 = 2;
90
91impl SurfaceTarget {
92    /// Where `local`, a rect of a target's own space into which
93    /// `content_to_local` maps the surface's content, shows in the window:
94    /// the bounds of a grid of its points in front of the eye; `None` when
95    /// none is.
96    pub fn window_bounds(
97        &self,
98        content_to_local: &ProjectiveTransform,
99        local: Rect,
100    ) -> Option<Rect> {
101        let local_to_content = content_to_local.inverse()?;
102        let steps = f32::from(WINDOW_BOUNDS_STEPS);
103        let grid = (0..=WINDOW_BOUNDS_STEPS)
104            .flat_map(|row| (0..=WINDOW_BOUNDS_STEPS).map(move |column| (row, column)));
105        grid.filter_map(|(row, column)| {
106            let point = Point::new(
107                local.x + local.width * f32::from(column) / steps,
108                local.y + local.height * f32::from(row) / steps,
109            );
110            let content = local_to_content.map_point(point);
111            self.pick.window_point(self.index, content)
112        })
113        .map(|point| Rect {
114            x: point.x,
115            y: point.y,
116            width: 0.0,
117            height: 0.0,
118        })
119        .reduce(|bounds, point| bounds.union(point))
120    }
121}
122
123/// Where a hit target on a mesh surface lies on the surface's content.
124#[derive(Clone, Copy)]
125pub struct SurfaceGeometry<'a> {
126    /// The surface the target is on.
127    pub target: &'a Rc<SurfaceTarget>,
128    /// Maps the surface's content into the node's local space.
129    pub content_to_local: ProjectiveTransform,
130    /// Axis-aligned bounds of the clips within the surface, in content
131    /// space, or `None` when none clips.
132    pub clip_bounds: Option<Rect>,
133    /// The clips within the surface, as quads in content space.
134    pub clips: &'a [HitClip],
135}
136
137impl SurfaceGeometry<'_> {
138    /// Where `local`, a rect of the target's own space, shows in the window.
139    pub fn window_bounds(&self, local: Rect) -> Option<Rect> {
140        self.target.window_bounds(&self.content_to_local, local)
141    }
142}
143
144/// Geometry for a hit target, borrowing the clip chain until the sink records it.
145#[derive(Clone, Copy)]
146pub struct HitGeometry<'a> {
147    /// Axis-aligned bounds of `quad` in window coordinates.
148    pub rect: Rect,
149    /// The node's rectangle after the layer transforms, as four corners in window space.
150    pub quad: [[f32; 2]; 4],
151    /// The node's own rectangle in its local space.
152    pub local_bounds: Rect,
153    /// Maps window coordinates into the node's local space.
154    pub world_to_local: ProjectiveTransform,
155    /// Axis-aligned bounds of the ancestors' clips in window space, or `None` when none clips.
156    pub hit_clip_bounds: Option<Rect>,
157    /// Clips of the ancestors that cut the target, as transformed quads in window space.
158    pub hit_clips: &'a [HitClip],
159    /// For a target on a mesh surface, where it lies on the surface's
160    /// content. The window-space fields above then hold the surface's flat
161    /// stand-in, exact only on a flat mesh; the surface decides the hit.
162    pub surface: Option<SurfaceGeometry<'a>>,
163}
164
165/// What a hit target answers with: the shape that narrows its bounds, and the
166/// node's modifier slices, which hold the pointer handlers it dispatches to
167/// and the pointer icon it asks for while hovered.
168pub struct HitTargetSpec<'a> {
169    /// Narrows the target's rectangle to a rounded shape, so a point in a
170    /// corner cutout misses it.
171    pub shape: Option<RoundedCornerShape>,
172    /// The node's slices, shared by the target rather than copied.
173    pub handlers: &'a Rc<ModifierNodeSlices>,
174}
175
176/// A hit target in window space: the node's geometry, clips and pointer handlers.
177#[derive(Clone)]
178pub struct HitRegion {
179    /// The layout node that owns the target.
180    pub node_id: NodeId,
181    /// Nodes that keep receiving the move, up and cancel events of a gesture that starts here: this
182    /// node first, then its pointer-input ancestors from the nearest outward.
183    pub capture_path: Vec<NodeId>,
184    /// Axis-aligned bounds of `quad` in window coordinates.
185    pub rect: Rect,
186    /// The node's rectangle after the layer transforms, as four corners in window space.
187    pub quad: [[f32; 2]; 4],
188    /// The node's own rectangle in its local space.
189    pub local_bounds: Rect,
190    /// Maps window coordinates into the node's local space.
191    pub world_to_local: ProjectiveTransform,
192    /// Rounded shape that narrows the target's rectangle, or `None` for a plain rectangle.
193    pub shape: Option<RoundedCornerShape>,
194    /// The node's modifier slices, shared with the render graph: its pointer
195    /// handlers and pointer icon.
196    pub handlers: Rc<ModifierNodeSlices>,
197    /// Draw order of the target. A larger value is drawn later and is hit first.
198    pub z_index: usize,
199    /// Axis-aligned bounds of the ancestors' clips in window space, or `None` when none clips.
200    pub hit_clip_bounds: Option<Rect>,
201    /// Clips of the ancestors that cut this target, as transformed quads in window space.
202    pub hit_clips: Vec<HitClip>,
203    /// For a target on a mesh surface, where it lies on the surface; shared,
204    /// as few targets are and clones of a target hold the same.
205    surface: Option<Rc<SurfaceRegion>>,
206    diagnostics: Rc<RenderDiagnostics>,
207}
208
209/// A hit target's place on a mesh surface's content.
210struct SurfaceRegion {
211    target: Rc<SurfaceTarget>,
212    content_to_local: ProjectiveTransform,
213    clip_bounds: Option<Rect>,
214    clips: Vec<HitClip>,
215}
216
217impl SurfaceRegion {
218    /// The local point the window point shows on the target, when the
219    /// target's surface is the nearest there and no clip cuts the point.
220    fn local_point(&self, window: Point) -> Option<Point> {
221        let target = &self.target;
222        if target
223            .outer_clip_bounds
224            .is_some_and(|clip| !clip.contains(window.x, window.y))
225            || !target
226                .outer_clips
227                .iter()
228                .all(|clip| point_in_quad(window, clip.quad))
229        {
230            return None;
231        }
232        self.local_in(target.pick.content_point(window, target.index)?)
233    }
234
235    /// The local point `ray`, in the space the scene's eye stands in, meets
236    /// on the target, when the target's surface is the nearest along it and
237    /// no clip within the surface cuts the point.
238    fn local_point_along(&self, ray: Ray) -> Option<Point> {
239        let (index, hit) = self.target.pick.nearest_along(ray)?;
240        (index == self.target.index)
241            .then(|| self.local_in(hit.point))
242            .flatten()
243    }
244
245    /// The local point of content point `content`, unless a clip within
246    /// the surface cuts it.
247    fn local_in(&self, content: Point) -> Option<Point> {
248        if self
249            .clip_bounds
250            .is_some_and(|clip| !clip.contains(content.x, content.y))
251            || !self
252                .clips
253                .iter()
254                .all(|clip| point_in_quad(content, clip.quad))
255        {
256            return None;
257        }
258        self.content_to_local.map_point_in_front(content)
259    }
260
261    /// The local point under the window point, or along the event's ray,
262    /// for a gesture the target holds, which may leave the surface.
263    fn drag_point(&self, event: &PointerEvent) -> Option<Point> {
264        let target = &self.target;
265        let content = match event.ray {
266            Some(ray) => target.pick.drag_point_along(ray, target.index)?,
267            None => target
268                .pick
269                .drag_point(event.global_position, target.index)?,
270        };
271        Some(self.content_to_local.map_point(content))
272    }
273}
274
275struct HitRegionInit<'a> {
276    node_id: NodeId,
277    capture_path: Vec<NodeId>,
278    geometry: HitGeometry<'a>,
279    clip_buffer: Vec<HitClip>,
280    shape: Option<RoundedCornerShape>,
281    handlers: Rc<ModifierNodeSlices>,
282    z_index: usize,
283    diagnostics: Rc<RenderDiagnostics>,
284}
285
286impl Default for HitRegionInit<'_> {
287    fn default() -> Self {
288        Self {
289            node_id: 0,
290            capture_path: Vec::new(),
291            geometry: HitGeometry {
292                rect: Rect {
293                    x: 0.0,
294                    y: 0.0,
295                    width: 0.0,
296                    height: 0.0,
297                },
298                quad: [[0.0, 0.0]; 4],
299                local_bounds: Rect {
300                    x: 0.0,
301                    y: 0.0,
302                    width: 0.0,
303                    height: 0.0,
304                },
305                world_to_local: ProjectiveTransform::identity(),
306                hit_clip_bounds: None,
307                hit_clips: &[],
308                surface: None,
309            },
310            clip_buffer: Vec::new(),
311            shape: None,
312            handlers: Rc::default(),
313            z_index: 0,
314            diagnostics: Rc::new(RenderDiagnostics::new()),
315        }
316    }
317}
318
319impl HitRegion {
320    fn with_diagnostics(init: HitRegionInit<'_>) -> Self {
321        let HitRegionInit {
322            node_id,
323            capture_path,
324            geometry,
325            clip_buffer: mut hit_clips,
326            shape,
327            handlers,
328            z_index,
329            diagnostics,
330        } = init;
331        let HitGeometry {
332            rect,
333            quad,
334            local_bounds,
335            world_to_local,
336            hit_clip_bounds,
337            hit_clips: clips,
338            surface,
339        } = geometry;
340        hit_clips.extend_from_slice(clips);
341        Self {
342            node_id,
343            capture_path,
344            rect,
345            quad,
346            local_bounds,
347            world_to_local,
348            shape,
349            handlers,
350            z_index,
351            hit_clip_bounds,
352            hit_clips,
353            surface: surface.map(|surface| {
354                Rc::new(SurfaceRegion {
355                    target: Rc::clone(surface.target),
356                    content_to_local: surface.content_to_local,
357                    clip_bounds: surface.clip_bounds,
358                    clips: surface.clips.to_vec(),
359                })
360            }),
361            diagnostics,
362        }
363    }
364
365    fn contains(&self, x: f32, y: f32) -> bool {
366        if let Some(surface) = &self.surface {
367            return surface
368                .local_point(Point { x, y })
369                .is_some_and(|local| self.contains_local(local));
370        }
371        if !self.rect.contains(x, y) {
372            return false;
373        }
374
375        if let Some(clip_bounds) = self.hit_clip_bounds
376            && !clip_bounds.contains(x, y)
377        {
378            return false;
379        }
380
381        let point = Point { x, y };
382        if !point_in_quad(point, self.quad) {
383            return false;
384        }
385
386        for clip in &self.hit_clips {
387            if !point_in_quad(point, clip.quad) {
388                return false;
389            }
390        }
391
392        let Some(local_point) = self.world_to_local.map_point_in_front(point) else {
393            return false;
394        };
395        self.contains_local(local_point)
396    }
397
398    fn contains_local(&self, local_point: Point) -> bool {
399        if let Some(shape) = self.shape {
400            point_in_rounded_rect(local_point, self.local_bounds, shape)
401        } else {
402            self.local_bounds.contains(local_point.x, local_point.y)
403        }
404    }
405
406    /// The squared distance from the point to this target when the point lies
407    /// inside the target grown to the minimum touch size and the target takes
408    /// input. None when the target is large enough on its own, when the point
409    /// is outside its reach, or when a clip cuts the point off.
410    fn reach_distance(&self, x: f32, y: f32) -> Option<f32> {
411        if self.handlers.pointer_inputs().is_empty() || self.surface.is_some() {
412            return None;
413        }
414        if let Some(clip_bounds) = self.hit_clip_bounds
415            && !clip_bounds.contains(x, y)
416        {
417            return None;
418        }
419        let grow_x = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.width) / 2.0).max(0.0);
420        let grow_y = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.height) / 2.0).max(0.0);
421        if grow_x <= 0.0 && grow_y <= 0.0 {
422            return None;
423        }
424        let right = self.rect.x + self.rect.width;
425        let bottom = self.rect.y + self.rect.height;
426        let in_reach = x >= self.rect.x - grow_x
427            && x <= right + grow_x
428            && y >= self.rect.y - grow_y
429            && y <= bottom + grow_y;
430        if !in_reach {
431            return None;
432        }
433        let dx = (self.rect.x - x).max(x - right).max(0.0);
434        let dy = (self.rect.y - y).max(y - bottom).max(0.0);
435        Some(dx * dx + dy * dy)
436    }
437
438    fn localize_event(&self, event: &PointerEvent) -> PointerEvent {
439        let local = match &self.surface {
440            Some(surface) => surface
441                .drag_point(event)
442                .unwrap_or_else(|| self.world_to_local.map_point(event.global_position)),
443            None => self.world_to_local.map_point(event.global_position),
444        };
445        event.copy_with_local_position(Point {
446            x: local.x - self.local_bounds.x,
447            y: local.y - self.local_bounds.y,
448        })
449    }
450
451    fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
452        if should_skip_consumed_event(&event) {
453            return;
454        }
455
456        modifier_slices.dispatch_pointer_event(self.localize_event(&event));
457    }
458
459    fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
460        if let Ok(modifier_slices) =
461            applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
462        {
463            return Some(modifier_slices);
464        }
465
466        applier
467            .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
468                node.modifier_slices_snapshot()
469            })
470            .ok()
471    }
472}
473
474fn is_terminal_pointer_event(kind: PointerEventKind) -> bool {
475    matches!(kind, PointerEventKind::Up | PointerEventKind::Cancel)
476}
477
478fn should_skip_consumed_event(event: &PointerEvent) -> bool {
479    event.is_consumed() && !is_terminal_pointer_event(event.kind)
480}
481
482impl HitTestTarget for HitRegion {
483    fn node_id(&self) -> NodeId {
484        self.node_id
485    }
486
487    fn pointer_icon(&self) -> Option<PointerIcon> {
488        self.handlers.pointer_icon().cloned()
489    }
490
491    fn capture_path(&self) -> Vec<NodeId> {
492        self.capture_path.clone()
493    }
494
495    fn dispatch(&self, event: PointerEvent) {
496        self.dispatch_modifier_slices(&self.handlers, event);
497    }
498
499    fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
500        if let Some(modifier_slices) = self.live_modifier_slices(applier) {
501            self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
502            return;
503        }
504
505        self.diagnostics.record_live_modifier_slice_lookup_miss();
506        self.dispatch_modifier_slices(&self.handlers, event);
507    }
508}
509
510#[derive(Default)]
511struct HitBuffers {
512    hit_clips: Vec<HitClip>,
513    capture_path: Vec<NodeId>,
514}
515
516/// The hit targets of a [`Scene`], borrowed apart from its graph, so a walk
517/// of the graph pushes targets while it reads the graph in place.
518pub(crate) struct HitTargets<'a> {
519    hits: &'a mut Vec<HitRegion>,
520    buffers: &'a mut Vec<HitBuffers>,
521    next_z: &'a mut usize,
522    node_index: &'a mut HashMap<NodeId, usize>,
523    diagnostics: &'a Rc<RenderDiagnostics>,
524}
525
526impl HitTargets<'_> {
527    /// Adds an interactive target in draw order (see [`Scene::push_hit`]).
528    pub(crate) fn push(
529        &mut self,
530        node_id: NodeId,
531        capture_path: &[NodeId],
532        geometry: HitGeometry<'_>,
533        target: HitTargetSpec<'_>,
534    ) {
535        let HitTargetSpec { shape, handlers } = target;
536        if handlers.pointer_inputs().is_empty() && handlers.pointer_icon().is_none() {
537            return;
538        }
539        let mut buffers = self.buffers.pop().unwrap_or_default();
540        buffers.capture_path.extend_from_slice(capture_path);
541
542        let z_index = *self.next_z;
543        *self.next_z += 1;
544        let hit_index = self.hits.len();
545        self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
546            node_id,
547            capture_path: buffers.capture_path,
548            geometry,
549            clip_buffer: buffers.hit_clips,
550            shape,
551            handlers: Rc::clone(handlers),
552            z_index,
553            diagnostics: Rc::clone(self.diagnostics),
554        }));
555        self.node_index.insert(node_id, hit_index);
556    }
557}
558
559/// The retained render graph of a window together with the hit targets built from it.
560pub struct Scene {
561    /// The retained render graph, or `None` before the first build.
562    pub graph: Option<RenderGraph>,
563    /// Hit targets in draw order, the last drawn at the end.
564    pub hits: Vec<HitRegion>,
565    hit_buffers: Vec<HitBuffers>,
566    /// Draw order index that the next pushed hit target receives.
567    pub next_hit_z: usize,
568    /// Position in `hits` of the target of each node, for finding a target by node id.
569    pub node_index: HashMap<NodeId, usize>,
570    diagnostics: Rc<RenderDiagnostics>,
571}
572
573impl Scene {
574    /// Creates an empty scene with no graph and no hit targets.
575    pub fn new() -> Self {
576        Self {
577            graph: None,
578            hits: Vec::new(),
579            hit_buffers: Vec::new(),
580            next_hit_z: 0,
581            node_index: HashMap::default(),
582            diagnostics: Rc::new(RenderDiagnostics::new()),
583        }
584    }
585
586    /// The diagnostics shared by the scene and its hit targets.
587    pub fn diagnostics(&self) -> &RenderDiagnostics {
588        self.diagnostics.as_ref()
589    }
590
591    /// Rebuilds the retained graph and hit targets from the applier, reusing
592    /// storage from the previous graph.
593    pub fn rebuild_from_applier(&mut self, applier: &MemoryApplier, root: NodeId) {
594        let previous = self.graph.take();
595        self.clear_hits();
596        let Some(graph) =
597            crate::scene_builder::rebuild_graph_from_applier(applier, root, 1.0, previous)
598        else {
599            return;
600        };
601        crate::scene_builder::publish_space(applier, &graph);
602        self.graph = Some(graph);
603        self.collect_hits();
604    }
605
606    /// Pushes the hit targets of the scene's graph, which stays in place.
607    fn collect_hits(&mut self) {
608        let Self {
609            graph: Some(graph),
610            hits,
611            hit_buffers,
612            next_hit_z,
613            node_index,
614            diagnostics,
615        } = self
616        else {
617            return;
618        };
619        let mut targets = HitTargets {
620            hits,
621            buffers: hit_buffers,
622            next_z: next_hit_z,
623            node_index,
624            diagnostics,
625        };
626        crate::hit_graph::collect_hits_from_graph(
627            &graph.root,
628            ProjectiveTransform::identity(),
629            &mut targets,
630            None,
631        );
632    }
633
634    /// Applies content and layer-property changes to the retained graph.
635    /// Empty updates or an unavailable scoped update rebuild the scene.
636    /// Hit targets are refreshed when their geometry changes or `refresh_hits`
637    /// is true, as required after layout or pointer-handler changes.
638    pub fn update_from_applier(
639        &mut self,
640        applier: &MemoryApplier,
641        root: NodeId,
642        updates: SceneUpdates<'_>,
643        refresh_hits: bool,
644    ) {
645        let report = if updates.is_empty() {
646            None
647        } else {
648            self.graph.as_mut().map(|graph| {
649                crate::scene_builder::update_graph_from_applier_report(applier, graph, updates, 1.0)
650            })
651        };
652        let Some(report) = report.filter(|report| report.applied()) else {
653            self.rebuild_from_applier(applier, root);
654            return;
655        };
656        if let Some(graph) = &self.graph {
657            crate::scene_builder::publish_space(applier, graph);
658        }
659        if !refresh_hits && !report.hit_graph_dirty {
660            return;
661        }
662        self.clear_hits();
663        self.collect_hits();
664    }
665
666    /// Adds an interactive target in draw order, ignoring targets that neither
667    /// handle a pointer nor name a pointer icon.
668    pub fn push_hit(
669        &mut self,
670        node_id: NodeId,
671        capture_path: &[NodeId],
672        geometry: HitGeometry<'_>,
673        target: HitTargetSpec<'_>,
674    ) {
675        HitTargets {
676            hits: &mut self.hits,
677            buffers: &mut self.hit_buffers,
678            next_z: &mut self.next_hit_z,
679            node_index: &mut self.node_index,
680            diagnostics: &self.diagnostics,
681        }
682        .push(node_id, capture_path, geometry, target);
683    }
684
685    /// Removes all hit targets and releases their handlers while retaining vector capacity.
686    pub fn clear_hits(&mut self) {
687        self.hit_buffers.clear();
688        for hit in self.hits.drain(..) {
689            let mut buffers = HitBuffers {
690                hit_clips: hit.hit_clips,
691                capture_path: hit.capture_path,
692            };
693            buffers.hit_clips.clear();
694            buffers.capture_path.clear();
695            self.hit_buffers.push(buffers);
696        }
697        self.node_index.clear();
698        self.next_hit_z = 0;
699    }
700
701    /// Replaces the retained render graph and leaves the hit targets as they are.
702    pub fn replace_graph(&mut self, graph: RenderGraph) {
703        self.graph = Some(graph);
704    }
705}
706
707impl Default for Scene {
708    fn default() -> Self {
709        Self::new()
710    }
711}
712
713impl RenderScene for Scene {
714    type HitTarget = HitRegion;
715
716    fn clear(&mut self) {
717        self.graph = None;
718        self.clear_hits();
719    }
720
721    fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
722        let mut hit_indices: Vec<usize> = self
723            .hits
724            .iter()
725            .enumerate()
726            .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
727            .collect();
728
729        hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
730        hit_indices
731            .into_iter()
732            .map(|index| self.hits[index].clone())
733            .collect()
734    }
735
736    fn hit_test_near(&self, x: f32, y: f32) -> Option<Self::HitTarget> {
737        self.hits
738            .iter()
739            .filter_map(|hit| hit.reach_distance(x, y).map(|distance| (distance, hit)))
740            .min_by(|(near, hit), (other_near, other)| {
741                near.total_cmp(other_near)
742                    .then_with(|| other.z_index.cmp(&hit.z_index))
743            })
744            .map(|(_, hit)| hit.clone())
745    }
746
747    fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
748        self.node_index
749            .get(&node_id)
750            .and_then(|&index| self.hits.get(index))
751            .cloned()
752    }
753
754    fn hit_test_ray(&self, scene: NodeId, ray: Ray) -> Vec<Self::HitTarget> {
755        let mut hit: Vec<&HitRegion> = self
756            .hits
757            .iter()
758            .filter(|hit| {
759                hit.surface.as_ref().is_some_and(|surface| {
760                    surface.target.pick.scene() == Some(scene)
761                        && surface
762                            .local_point_along(ray)
763                            .is_some_and(|local| hit.contains_local(local))
764                })
765            })
766            .collect();
767        hit.sort_by_key(|hit| Reverse(hit.z_index));
768        hit.into_iter().cloned().collect()
769    }
770
771    fn surface_windows(&self, visit: &mut dyn FnMut(NodeId, ProjectiveTransform)) {
772        let Some(graph) = &self.graph else {
773            return;
774        };
775        let root = ProjectiveTransform::identity();
776        crate::space::for_each_window_eye(&graph.root, root, &mut |_, view, to_window| {
777            for seen in &view.surfaces {
778                if let (Some(node_id), Some(content)) =
779                    (seen.layer.node_id, view.content_to_window(seen, to_window))
780                {
781                    visit(node_id, content);
782                }
783            }
784        });
785    }
786
787    fn surface_window_bounds(&self, node_id: NodeId) -> Option<Rect> {
788        let hit = self.hits.get(*self.node_index.get(&node_id)?)?;
789        let surface = hit.surface.as_ref()?;
790        surface
791            .target
792            .window_bounds(&surface.content_to_local, hit.local_bounds)
793    }
794
795    fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
796        if let Some(graph) = &self.graph {
797            graph.collect_retained_visual_observation_nodes(nodes);
798        } else {
799            nodes.clear();
800        }
801        true
802    }
803}
804
805fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
806    if !rect.contains(point.x, point.y) {
807        return false;
808    }
809
810    let local_x = point.x - rect.x;
811    let local_y = point.y - rect.y;
812    let radii = shape.resolve(rect.width, rect.height);
813    let tl = radii.top_left;
814    let tr = radii.top_right;
815    let bl = radii.bottom_left;
816    let br = radii.bottom_right;
817
818    if local_x < tl && local_y < tl {
819        let dx = tl - local_x;
820        let dy = tl - local_y;
821        return dx * dx + dy * dy <= tl * tl;
822    }
823
824    if local_x > rect.width - tr && local_y < tr {
825        let dx = local_x - (rect.width - tr);
826        let dy = tr - local_y;
827        return dx * dx + dy * dy <= tr * tr;
828    }
829
830    if local_x < bl && local_y > rect.height - bl {
831        let dx = bl - local_x;
832        let dy = local_y - (rect.height - bl);
833        return dx * dx + dy * dy <= bl * bl;
834    }
835
836    if local_x > rect.width - br && local_y > rect.height - br {
837        let dx = local_x - (rect.width - br);
838        let dy = local_y - (rect.height - br);
839        return dx * dx + dy * dy <= br * br;
840    }
841
842    true
843}
844
845fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
846    point_in_triangle(point, quad[0], quad[1], quad[3])
847        || point_in_triangle(point, quad[0], quad[3], quad[2])
848}
849
850fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
851    let d1 = triangle_sign(point, a, b);
852    let d2 = triangle_sign(point, b, c);
853    let d3 = triangle_sign(point, c, a);
854    let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
855    let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
856    !(has_negative && has_positive)
857}
858
859fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
860    (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
861}
862
863#[cfg(test)]
864#[path = "tests/graph_scene_tests.rs"]
865mod tests;