Skip to main content

webrender/
hit_test.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use api::{BorderRadius, ClipMode, HitTestResultItem, HitTestResult, ItemTag, PrimitiveFlags};
6use api::{PipelineId, ApiHitTester};
7use api::units::*;
8use crate::clip::{rounded_rectangle_contains_point, ClipNodeId, ClipTreeBuilder};
9use crate::clip::{polygon_contains_point, ClipItemKey, ClipItemKeyKind};
10use crate::prim_store::PolygonKey;
11use crate::scene_builder_thread::Interners;
12use crate::spatial_tree::{SpatialNodeIndex, SpatialTree, get_external_scroll_offset};
13use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo};
14use std::sync::{Arc, Mutex};
15use crate::util::LayoutToWorldFastTransform;
16
17pub struct SharedHitTester {
18    // We don't really need a mutex here. We could do with some sort of
19    // atomic-atomic-ref-counted pointer (an Arc which would let the pointer
20    // be swapped atomically like an AtomicPtr).
21    // In practive this shouldn't cause performance issues, though.
22    hit_tester: Mutex<Arc<HitTester>>,
23}
24
25impl SharedHitTester {
26    pub fn new() -> Self {
27        SharedHitTester {
28            hit_tester: Mutex::new(Arc::new(HitTester::empty())),
29        }
30    }
31
32    pub fn get_ref(&self) -> Arc<HitTester> {
33        let guard = self.hit_tester.lock().unwrap();
34        Arc::clone(&*guard)
35    }
36
37    pub(crate) fn update(&self, new_hit_tester: Arc<HitTester>) {
38        let mut guard = self.hit_tester.lock().unwrap();
39        *guard = new_hit_tester;
40    }
41}
42
43impl ApiHitTester for SharedHitTester {
44    fn hit_test(&self,
45        point: WorldPoint,
46    ) -> HitTestResult {
47        self.get_ref().hit_test(HitTest::new(point))
48    }
49}
50
51/// A copy of important spatial node data to use during hit testing. This a copy of
52/// data from the SpatialTree that will persist as a new frame is under construction,
53/// allowing hit tests consistent with the currently rendered frame.
54#[derive(MallocSizeOf)]
55struct HitTestSpatialNode {
56    /// The pipeline id of this node.
57    pipeline_id: PipelineId,
58
59    /// World transform for content transformed by this node.
60    world_content_transform: LayoutToWorldFastTransform,
61
62    /// World viewport transform for content transformed by this node.
63    world_viewport_transform: LayoutToWorldFastTransform,
64
65    /// The accumulated external scroll offset for this spatial node.
66    external_scroll_offset: LayoutVector2D,
67}
68
69#[derive(MallocSizeOf)]
70struct HitTestClipNode {
71    /// A particular point must be inside all of these regions to be considered clipped in
72    /// for the purposes of a hit test.
73    region: HitTestRegion,
74    /// The positioning node for this clip
75    spatial_node_index: SpatialNodeIndex,
76    /// Parent clip node
77    parent: ClipNodeId,
78}
79
80impl HitTestClipNode {
81    fn new(
82        item: &ClipItemKey,
83        clip_rect: LayoutRect,
84        interners: &Interners,
85        parent: ClipNodeId,
86        spatial_node_index: SpatialNodeIndex,
87    ) -> Self {
88        let region = match item.kind {
89            ClipItemKeyKind::Rectangle(mode) => {
90                HitTestRegion::Rectangle(clip_rect, mode)
91            }
92            ClipItemKeyKind::RoundedRectangle(radius, mode) => {
93                HitTestRegion::RoundedRectangle(clip_rect, radius.into(), mode)
94            }
95            ClipItemKeyKind::ImageMask(_, polygon_handle) => {
96                if let Some(handle) = polygon_handle {
97                    // Retrieve the polygon data from the interner.
98                    let polygon = &interners.polygon[handle];
99                    HitTestRegion::Polygon(clip_rect, *polygon)
100                } else {
101                    HitTestRegion::Rectangle(clip_rect, ClipMode::Clip)
102                }
103            }
104        };
105
106        HitTestClipNode {
107            region,
108            spatial_node_index,
109            parent,
110        }
111    }
112}
113
114#[derive(Clone, MallocSizeOf)]
115struct HitTestingItem {
116    rect: LayoutRect,
117    tag: ItemTag,
118    animation_id: u64,
119    is_backface_visible: bool,
120    spatial_node_index: SpatialNodeIndex,
121    clip_node_id: ClipNodeId,
122}
123
124impl HitTestingItem {
125    fn new(
126        tag: ItemTag,
127        animation_id: u64,
128        info: &LayoutPrimitiveInfo,
129        spatial_node_index: SpatialNodeIndex,
130        clip_node_id: ClipNodeId,
131    ) -> HitTestingItem {
132        HitTestingItem {
133            rect: info.rect,
134            tag,
135            animation_id,
136            is_backface_visible: info.flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
137            spatial_node_index,
138            clip_node_id,
139        }
140    }
141}
142
143/// Statistics about allocation sizes of current hit tester,
144/// used to pre-allocate size of the next hit tester.
145pub struct HitTestingSceneStats {
146    pub clip_nodes_count: usize,
147    pub items_count: usize,
148}
149
150impl HitTestingSceneStats {
151    pub fn empty() -> Self {
152        HitTestingSceneStats {
153            clip_nodes_count: 0,
154            items_count: 0,
155        }
156    }
157}
158
159/// Defines the immutable part of a hit tester for a given scene.
160/// The hit tester is recreated each time a frame is built, since
161/// it relies on the current values of the spatial tree.
162/// However, the clip chain and item definitions don't change,
163/// so they are created once per scene, and shared between
164/// hit tester instances via Arc.
165#[derive(MallocSizeOf)]
166pub struct HitTestingScene {
167    clip_nodes: FastHashMap<ClipNodeId, HitTestClipNode>,
168
169    /// List of hit testing primitives.
170    items: Vec<HitTestingItem>,
171}
172
173impl HitTestingScene {
174    /// Construct a new hit testing scene, pre-allocating to size
175    /// provided by previous scene stats.
176    pub fn new(stats: &HitTestingSceneStats) -> Self {
177        HitTestingScene {
178            clip_nodes: FastHashMap::default(),
179            items: Vec::with_capacity(stats.items_count),
180        }
181    }
182
183    pub fn reset(&mut self) {
184        self.clip_nodes.clear();
185        self.items.clear();
186    }
187
188    /// Get stats about the current scene allocation sizes.
189    pub fn get_stats(&self) -> HitTestingSceneStats {
190        HitTestingSceneStats {
191            clip_nodes_count: 0,
192            items_count: self.items.len(),
193        }
194    }
195
196    fn add_clip_node(
197        &mut self,
198        clip_node_id: ClipNodeId,
199        clip_tree_builder: &ClipTreeBuilder,
200        interners: &Interners,
201    ) {
202        if clip_node_id == ClipNodeId::NONE {
203            return;
204        }
205
206        if !self.clip_nodes.contains_key(&clip_node_id) {
207            let src_clip_node = clip_tree_builder.get_node(clip_node_id);
208            let clip_item = &interners.clip[src_clip_node.handle];
209
210            // SNAPTODO: Scene-build hit-test scene captures the unsnapped
211            // clip rect. Snapping happens against frame-time spatial state
212            // which isn't available here; audit hit-test consumers to
213            // confirm using the unsnapped value is correct for hit
214            // semantics, or apply a frame-time snap before testing.
215            let clip_node = HitTestClipNode::new(
216                &clip_item.key,
217                src_clip_node.unsnapped_clip_rect,
218                interners,
219                src_clip_node.parent,
220                src_clip_node.spatial_node_index,
221            );
222
223            self.clip_nodes.insert(clip_node_id, clip_node);
224
225            self.add_clip_node(
226                src_clip_node.parent,
227                clip_tree_builder,
228                interners,
229            );
230        }
231    }
232
233    /// Add a hit testing primitive.
234    pub fn add_item(
235        &mut self,
236        tag: ItemTag,
237        anim_id: u64,
238        info: &LayoutPrimitiveInfo,
239        spatial_node_index: SpatialNodeIndex,
240        clip_node_id: ClipNodeId,
241        clip_tree_builder: &ClipTreeBuilder,
242        interners: &Interners,
243    ) {
244        self.add_clip_node(
245            clip_node_id,
246            clip_tree_builder,
247            interners,
248        );
249
250        let item = HitTestingItem::new(
251            tag,
252            anim_id,
253            info,
254            spatial_node_index,
255            clip_node_id,
256        );
257
258        self.items.push(item);
259    }
260}
261
262#[derive(MallocSizeOf)]
263enum HitTestRegion {
264    Rectangle(LayoutRect, ClipMode),
265    RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
266    Polygon(LayoutRect, PolygonKey),
267}
268
269impl HitTestRegion {
270    fn contains(&self, point: &LayoutPoint) -> bool {
271        match *self {
272            HitTestRegion::Rectangle(ref rectangle, ClipMode::Clip) =>
273                rectangle.contains(*point),
274            HitTestRegion::Rectangle(ref rectangle, ClipMode::ClipOut) =>
275                !rectangle.contains(*point),
276            HitTestRegion::RoundedRectangle(rect, radii, ClipMode::Clip) =>
277                rounded_rectangle_contains_point(point, &rect, &radii),
278            HitTestRegion::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
279                !rounded_rectangle_contains_point(point, &rect, &radii),
280            HitTestRegion::Polygon(rect, polygon) =>
281                polygon_contains_point(point, &rect, &polygon),
282        }
283    }
284}
285
286#[derive(MallocSizeOf)]
287pub struct HitTester {
288    #[ignore_malloc_size_of = "Arc"]
289    scene: Arc<HitTestingScene>,
290    spatial_nodes: FastHashMap<SpatialNodeIndex, HitTestSpatialNode>,
291    pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
292}
293
294impl HitTester {
295    pub fn empty() -> Self {
296        HitTester {
297            scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
298            spatial_nodes: FastHashMap::default(),
299            pipeline_root_nodes: FastHashMap::default(),
300        }
301    }
302
303    pub fn new(
304        scene: Arc<HitTestingScene>,
305        spatial_tree: &SpatialTree,
306    ) -> HitTester {
307        let mut hit_tester = HitTester {
308            scene,
309            spatial_nodes: FastHashMap::default(),
310            pipeline_root_nodes: FastHashMap::default(),
311        };
312        hit_tester.read_spatial_tree(spatial_tree);
313        hit_tester
314    }
315
316    fn read_spatial_tree(
317        &mut self,
318        spatial_tree: &SpatialTree,
319    ) {
320        self.spatial_nodes.clear();
321        self.spatial_nodes.reserve(spatial_tree.spatial_node_count());
322
323        self.pipeline_root_nodes.clear();
324
325        spatial_tree.visit_nodes(|index, node| {
326            // If we haven't already seen a node for this pipeline, record this one as the root
327            // node.
328            self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
329
330            //TODO: avoid inverting more than necessary:
331            //  - if the coordinate system is non-invertible, no need to try any of these concrete transforms
332            //  - if there are other places where inversion is needed, let's not repeat the step
333
334            self.spatial_nodes.insert(index, HitTestSpatialNode {
335                pipeline_id: node.pipeline_id,
336                world_content_transform: spatial_tree
337                    .get_world_transform(index)
338                    .into_fast_transform(),
339                world_viewport_transform: spatial_tree
340                    .get_world_viewport_transform(index)
341                    .into_fast_transform(),
342                external_scroll_offset: get_external_scroll_offset(spatial_tree, index),
343            });
344        });
345    }
346
347    pub fn hit_test(&self, test: HitTest) -> HitTestResult {
348        let mut result = HitTestResult::default();
349
350        let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
351        let mut point_in_layer = None;
352        let mut current_root_spatial_node_index = SpatialNodeIndex::INVALID;
353        let mut point_in_viewport = None;
354
355        // For each hit test primitive
356        for item in self.scene.items.iter().rev() {
357            let scroll_node = &self.spatial_nodes[&item.spatial_node_index];
358            let pipeline_id = scroll_node.pipeline_id;
359
360            // Update the cached point in layer space, if the spatial node
361            // changed since last primitive.
362            if item.spatial_node_index != current_spatial_node_index {
363                point_in_layer = scroll_node
364                    .world_content_transform
365                    .inverse()
366                    .and_then(|inverted| inverted.project_point2d(test.point));
367                current_spatial_node_index = item.spatial_node_index;
368            }
369
370            // Only consider hit tests on transformable layers.
371            let point_in_layer = match point_in_layer {
372                Some(p) => p,
373                None => continue,
374            };
375
376            // If the item's rect or clip rect don't contain this point, it's
377            // not a valid hit.
378            if !item.rect.contains(point_in_layer) {
379                continue;
380            }
381
382            // See if any of the clips for this primitive cull out the item.
383            let mut current_clip_node_id = item.clip_node_id;
384            let mut is_valid = true;
385
386            while current_clip_node_id != ClipNodeId::NONE {
387                let clip_node = &self.scene.clip_nodes[&current_clip_node_id];
388
389                let transform = self
390                    .spatial_nodes[&clip_node.spatial_node_index]
391                    .world_content_transform;
392                if let Some(transformed_point) = transform
393                    .inverse()
394                    .and_then(|inverted| inverted.project_point2d(test.point))
395                {
396                    if !clip_node.region.contains(&transformed_point) {
397                        is_valid = false;
398                        break;
399                    }
400                }
401
402                current_clip_node_id = clip_node.parent;
403            }
404
405            if !is_valid {
406                continue;
407            }
408
409            // Don't hit items with backface-visibility:hidden if they are facing the back.
410            if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() {
411                continue;
412            }
413
414            // We need to calculate the position of the test point relative to the origin of
415            // the pipeline of the hit item. If we cannot get a transformed point, we are
416            // in a situation with an uninvertible transformation so we should just skip this
417            // result.
418            let root_spatial_node_index = self.pipeline_root_nodes[&pipeline_id];
419            if root_spatial_node_index != current_root_spatial_node_index {
420                let root_node = &self.spatial_nodes[&root_spatial_node_index];
421                point_in_viewport = root_node
422                    .world_viewport_transform
423                    .inverse()
424                    .and_then(|inverted| inverted.transform_point2d(test.point))
425                    .map(|pt| pt - scroll_node.external_scroll_offset);
426
427                current_root_spatial_node_index = root_spatial_node_index;
428            }
429
430            if let Some(point_in_viewport) = point_in_viewport {
431                result.items.push(HitTestResultItem {
432                    pipeline: pipeline_id,
433                    tag: item.tag,
434                    animation_id: item.animation_id,
435                    point_in_viewport,
436                });
437            }
438        }
439
440        result.items.dedup();
441        result
442    }
443}
444
445#[derive(MallocSizeOf)]
446pub struct HitTest {
447    point: WorldPoint,
448}
449
450impl HitTest {
451    pub fn new(
452        point: WorldPoint,
453    ) -> HitTest {
454        HitTest {
455            point,
456        }
457    }
458}