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        interners: &Interners,
84        parent: ClipNodeId,
85    ) -> Self {
86        let region = match item.kind {
87            ClipItemKeyKind::Rectangle(rect, mode) => {
88                HitTestRegion::Rectangle(rect.into(), mode)
89            }
90            ClipItemKeyKind::RoundedRectangle(rect, radius, mode) => {
91                HitTestRegion::RoundedRectangle(rect.into(), radius.into(), mode)
92            }
93            ClipItemKeyKind::ImageMask(rect, _, polygon_handle) => {
94                if let Some(handle) = polygon_handle {
95                    // Retrieve the polygon data from the interner.
96                    let polygon = &interners.polygon[handle];
97                    HitTestRegion::Polygon(rect.into(), *polygon)
98                } else {
99                    HitTestRegion::Rectangle(rect.into(), ClipMode::Clip)
100                }
101            }
102            ClipItemKeyKind::BoxShadow(..) => HitTestRegion::Invalid,
103        };
104
105        HitTestClipNode {
106            region,
107            spatial_node_index: item.spatial_node_index,
108            parent,
109        }
110    }
111}
112
113#[derive(Clone, MallocSizeOf)]
114struct HitTestingItem {
115    rect: LayoutRect,
116    tag: ItemTag,
117    animation_id: u64,
118    is_backface_visible: bool,
119    spatial_node_index: SpatialNodeIndex,
120    clip_node_id: ClipNodeId,
121}
122
123impl HitTestingItem {
124    fn new(
125        tag: ItemTag,
126        animation_id: u64,
127        info: &LayoutPrimitiveInfo,
128        spatial_node_index: SpatialNodeIndex,
129        clip_node_id: ClipNodeId,
130    ) -> HitTestingItem {
131        HitTestingItem {
132            rect: info.rect,
133            tag,
134            animation_id,
135            is_backface_visible: info.flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
136            spatial_node_index,
137            clip_node_id,
138        }
139    }
140}
141
142/// Statistics about allocation sizes of current hit tester,
143/// used to pre-allocate size of the next hit tester.
144pub struct HitTestingSceneStats {
145    pub clip_nodes_count: usize,
146    pub items_count: usize,
147}
148
149impl HitTestingSceneStats {
150    pub fn empty() -> Self {
151        HitTestingSceneStats {
152            clip_nodes_count: 0,
153            items_count: 0,
154        }
155    }
156}
157
158/// Defines the immutable part of a hit tester for a given scene.
159/// The hit tester is recreated each time a frame is built, since
160/// it relies on the current values of the spatial tree.
161/// However, the clip chain and item definitions don't change,
162/// so they are created once per scene, and shared between
163/// hit tester instances via Arc.
164#[derive(MallocSizeOf)]
165pub struct HitTestingScene {
166    clip_nodes: FastHashMap<ClipNodeId, HitTestClipNode>,
167
168    /// List of hit testing primitives.
169    items: Vec<HitTestingItem>,
170}
171
172impl HitTestingScene {
173    /// Construct a new hit testing scene, pre-allocating to size
174    /// provided by previous scene stats.
175    pub fn new(stats: &HitTestingSceneStats) -> Self {
176        HitTestingScene {
177            clip_nodes: FastHashMap::default(),
178            items: Vec::with_capacity(stats.items_count),
179        }
180    }
181
182    pub fn reset(&mut self) {
183        self.clip_nodes.clear();
184        self.items.clear();
185    }
186
187    /// Get stats about the current scene allocation sizes.
188    pub fn get_stats(&self) -> HitTestingSceneStats {
189        HitTestingSceneStats {
190            clip_nodes_count: 0,
191            items_count: self.items.len(),
192        }
193    }
194
195    fn add_clip_node(
196        &mut self,
197        clip_node_id: ClipNodeId,
198        clip_tree_builder: &ClipTreeBuilder,
199        interners: &Interners,
200    ) {
201        if clip_node_id == ClipNodeId::NONE {
202            return;
203        }
204
205        if !self.clip_nodes.contains_key(&clip_node_id) {
206            let src_clip_node = clip_tree_builder.get_node(clip_node_id);
207            let clip_item = &interners.clip[src_clip_node.handle];
208
209            let clip_node = HitTestClipNode::new(
210                &clip_item.key,
211                interners,
212                src_clip_node.parent,
213            );
214
215            self.clip_nodes.insert(clip_node_id, clip_node);
216
217            self.add_clip_node(
218                src_clip_node.parent,
219                clip_tree_builder,
220                interners,
221            );
222        }
223    }
224
225    /// Add a hit testing primitive.
226    pub fn add_item(
227        &mut self,
228        tag: ItemTag,
229        anim_id: u64,
230        info: &LayoutPrimitiveInfo,
231        spatial_node_index: SpatialNodeIndex,
232        clip_node_id: ClipNodeId,
233        clip_tree_builder: &ClipTreeBuilder,
234        interners: &Interners,
235    ) {
236        self.add_clip_node(
237            clip_node_id,
238            clip_tree_builder,
239            interners,
240        );
241
242        let item = HitTestingItem::new(
243            tag,
244            anim_id,
245            info,
246            spatial_node_index,
247            clip_node_id,
248        );
249
250        self.items.push(item);
251    }
252}
253
254#[derive(MallocSizeOf)]
255enum HitTestRegion {
256    Invalid,
257    Rectangle(LayoutRect, ClipMode),
258    RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
259    Polygon(LayoutRect, PolygonKey),
260}
261
262impl HitTestRegion {
263    fn contains(&self, point: &LayoutPoint) -> bool {
264        match *self {
265            HitTestRegion::Rectangle(ref rectangle, ClipMode::Clip) =>
266                rectangle.contains(*point),
267            HitTestRegion::Rectangle(ref rectangle, ClipMode::ClipOut) =>
268                !rectangle.contains(*point),
269            HitTestRegion::RoundedRectangle(rect, radii, ClipMode::Clip) =>
270                rounded_rectangle_contains_point(point, &rect, &radii),
271            HitTestRegion::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
272                !rounded_rectangle_contains_point(point, &rect, &radii),
273            HitTestRegion::Polygon(rect, polygon) =>
274                polygon_contains_point(point, &rect, &polygon),
275            HitTestRegion::Invalid => true,
276        }
277    }
278}
279
280#[derive(MallocSizeOf)]
281pub struct HitTester {
282    #[ignore_malloc_size_of = "Arc"]
283    scene: Arc<HitTestingScene>,
284    spatial_nodes: FastHashMap<SpatialNodeIndex, HitTestSpatialNode>,
285    pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
286}
287
288impl HitTester {
289    pub fn empty() -> Self {
290        HitTester {
291            scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
292            spatial_nodes: FastHashMap::default(),
293            pipeline_root_nodes: FastHashMap::default(),
294        }
295    }
296
297    pub fn new(
298        scene: Arc<HitTestingScene>,
299        spatial_tree: &SpatialTree,
300    ) -> HitTester {
301        let mut hit_tester = HitTester {
302            scene,
303            spatial_nodes: FastHashMap::default(),
304            pipeline_root_nodes: FastHashMap::default(),
305        };
306        hit_tester.read_spatial_tree(spatial_tree);
307        hit_tester
308    }
309
310    fn read_spatial_tree(
311        &mut self,
312        spatial_tree: &SpatialTree,
313    ) {
314        self.spatial_nodes.clear();
315        self.spatial_nodes.reserve(spatial_tree.spatial_node_count());
316
317        self.pipeline_root_nodes.clear();
318
319        spatial_tree.visit_nodes(|index, node| {
320            // If we haven't already seen a node for this pipeline, record this one as the root
321            // node.
322            self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
323
324            //TODO: avoid inverting more than necessary:
325            //  - if the coordinate system is non-invertible, no need to try any of these concrete transforms
326            //  - if there are other places where inversion is needed, let's not repeat the step
327
328            self.spatial_nodes.insert(index, HitTestSpatialNode {
329                pipeline_id: node.pipeline_id,
330                world_content_transform: spatial_tree
331                    .get_world_transform(index)
332                    .into_fast_transform(),
333                world_viewport_transform: spatial_tree
334                    .get_world_viewport_transform(index)
335                    .into_fast_transform(),
336                external_scroll_offset: get_external_scroll_offset(spatial_tree, index),
337            });
338        });
339    }
340
341    pub fn hit_test(&self, test: HitTest) -> HitTestResult {
342        let mut result = HitTestResult::default();
343
344        let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
345        let mut point_in_layer = None;
346        let mut current_root_spatial_node_index = SpatialNodeIndex::INVALID;
347        let mut point_in_viewport = None;
348
349        // For each hit test primitive
350        for item in self.scene.items.iter().rev() {
351            let scroll_node = &self.spatial_nodes[&item.spatial_node_index];
352            let pipeline_id = scroll_node.pipeline_id;
353
354            // Update the cached point in layer space, if the spatial node
355            // changed since last primitive.
356            if item.spatial_node_index != current_spatial_node_index {
357                point_in_layer = scroll_node
358                    .world_content_transform
359                    .inverse()
360                    .and_then(|inverted| inverted.project_point2d(test.point));
361                current_spatial_node_index = item.spatial_node_index;
362            }
363
364            // Only consider hit tests on transformable layers.
365            let point_in_layer = match point_in_layer {
366                Some(p) => p,
367                None => continue,
368            };
369
370            // If the item's rect or clip rect don't contain this point, it's
371            // not a valid hit.
372            if !item.rect.contains(point_in_layer) {
373                continue;
374            }
375
376            // See if any of the clips for this primitive cull out the item.
377            let mut current_clip_node_id = item.clip_node_id;
378            let mut is_valid = true;
379
380            while current_clip_node_id != ClipNodeId::NONE {
381                let clip_node = &self.scene.clip_nodes[&current_clip_node_id];
382
383                let transform = self
384                    .spatial_nodes[&clip_node.spatial_node_index]
385                    .world_content_transform;
386                if let Some(transformed_point) = transform
387                    .inverse()
388                    .and_then(|inverted| inverted.project_point2d(test.point))
389                {
390                    if !clip_node.region.contains(&transformed_point) {
391                        is_valid = false;
392                        break;
393                    }
394                }
395
396                current_clip_node_id = clip_node.parent;
397            }
398
399            if !is_valid {
400                continue;
401            }
402
403            // Don't hit items with backface-visibility:hidden if they are facing the back.
404            if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() {
405                continue;
406            }
407
408            // We need to calculate the position of the test point relative to the origin of
409            // the pipeline of the hit item. If we cannot get a transformed point, we are
410            // in a situation with an uninvertible transformation so we should just skip this
411            // result.
412            let root_spatial_node_index = self.pipeline_root_nodes[&pipeline_id];
413            if root_spatial_node_index != current_root_spatial_node_index {
414                let root_node = &self.spatial_nodes[&root_spatial_node_index];
415                point_in_viewport = root_node
416                    .world_viewport_transform
417                    .inverse()
418                    .and_then(|inverted| inverted.transform_point2d(test.point))
419                    .map(|pt| pt - scroll_node.external_scroll_offset);
420
421                current_root_spatial_node_index = root_spatial_node_index;
422            }
423
424            if let Some(point_in_viewport) = point_in_viewport {
425                result.items.push(HitTestResultItem {
426                    pipeline: pipeline_id,
427                    tag: item.tag,
428                    animation_id: item.animation_id,
429                    point_in_viewport,
430                });
431            }
432        }
433
434        result.items.dedup();
435        result
436    }
437}
438
439#[derive(MallocSizeOf)]
440pub struct HitTest {
441    point: WorldPoint,
442}
443
444impl HitTest {
445    pub fn new(
446        point: WorldPoint,
447    ) -> HitTest {
448        HitTest {
449            point,
450        }
451    }
452}