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/. */
5 use api::{BorderRadius, ClipMode, HitTestItem, HitTestResult, ItemTag, PrimitiveFlags};
6 use api::{PipelineId, ApiHitTester, ClipId};
8 use crate::clip::{ClipItemKind, ClipStore, ClipNode, rounded_rectangle_contains_point};
9 use crate::clip::{polygon_contains_point};
10 use crate::prim_store::PolygonKey;
11 use crate::scene_builder_thread::Interners;
12 use crate::spatial_tree::{SpatialNodeIndex, SpatialTree};
13 use crate::internal_types::{FastHashMap, FastHashSet, LayoutPrimitiveInfo};
15 use std::sync::{Arc, Mutex};
16 use crate::util::{LayoutToWorldFastTransform, VecHelper};
18 pub struct SharedHitTester {
19 // We don't really need a mutex here. We could do with some sort of
20 // atomic-atomic-ref-counted pointer (an Arc which would let the pointer
21 // be swapped atomically like an AtomicPtr).
22 // In practive this shouldn't cause performance issues, though.
23 hit_tester: Mutex<Arc<HitTester>>,
26 impl SharedHitTester {
27 pub fn new() -> Self {
29 hit_tester: Mutex::new(Arc::new(HitTester::empty())),
33 pub fn get_ref(&self) -> Arc<HitTester> {
34 let guard = self.hit_tester.lock().unwrap();
38 pub(crate) fn update(&self, new_hit_tester: Arc<HitTester>) {
39 let mut guard = self.hit_tester.lock().unwrap();
40 *guard = new_hit_tester;
44 impl ApiHitTester for SharedHitTester {
46 pipeline_id: Option<PipelineId>,
49 self.get_ref().hit_test(HitTest::new(pipeline_id, point))
53 /// A copy of important spatial node data to use during hit testing. This a copy of
54 /// data from the SpatialTree that will persist as a new frame is under construction,
55 /// allowing hit tests consistent with the currently rendered frame.
56 #[derive(MallocSizeOf)]
57 struct HitTestSpatialNode {
58 /// The pipeline id of this node.
59 pipeline_id: PipelineId,
61 /// World transform for content transformed by this node.
62 world_content_transform: LayoutToWorldFastTransform,
64 /// World viewport transform for content transformed by this node.
65 world_viewport_transform: LayoutToWorldFastTransform,
67 /// The accumulated external scroll offset for this spatial node.
68 external_scroll_offset: LayoutVector2D,
71 #[derive(MallocSizeOf)]
72 struct HitTestClipNode {
73 /// A particular point must be inside all of these regions to be considered clipped in
74 /// for the purposes of a hit test.
75 region: HitTestRegion,
76 /// The positioning node for this clip
77 spatial_node_index: SpatialNodeIndex,
80 impl HitTestClipNode {
83 spatial_node_index: SpatialNodeIndex,
84 interners: &Interners,
86 let region = match node.item.kind {
87 ClipItemKind::Rectangle { rect, mode } => {
88 HitTestRegion::Rectangle(rect, mode)
90 ClipItemKind::RoundedRectangle { rect, radius, mode } => {
91 HitTestRegion::RoundedRectangle(rect, radius, mode)
93 ClipItemKind::Image { 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, *polygon)
99 HitTestRegion::Rectangle(rect, ClipMode::Clip)
102 ClipItemKind::BoxShadow { .. } => HitTestRegion::Invalid,
112 #[derive(Clone, MallocSizeOf)]
113 struct HitTestingItem {
115 clip_rect: LayoutRect,
117 is_backface_visible: bool,
118 spatial_node_index: SpatialNodeIndex,
119 #[ignore_malloc_size_of = "Range"]
120 clip_nodes_range: ops::Range<ClipNodeIndex>,
123 impl HitTestingItem {
126 info: &LayoutPrimitiveInfo,
127 spatial_node_index: SpatialNodeIndex,
128 clip_nodes_range: ops::Range<ClipNodeIndex>,
129 ) -> HitTestingItem {
132 clip_rect: info.clip_rect,
134 is_backface_visible: info.flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
141 /// Statistics about allocation sizes of current hit tester,
142 /// used to pre-allocate size of the next hit tester.
143 pub struct HitTestingSceneStats {
144 pub clip_nodes_count: usize,
145 pub items_count: usize,
148 impl HitTestingSceneStats {
149 pub fn empty() -> Self {
150 HitTestingSceneStats {
157 #[derive(MallocSizeOf, Debug, Copy, Clone)]
158 pub struct ClipNodeIndex(u32);
160 /// Defines the immutable part of a hit tester for a given scene.
161 /// The hit tester is recreated each time a frame is built, since
162 /// it relies on the current values of the spatial tree.
163 /// However, the clip chain and item definitions don't change,
164 /// so they are created once per scene, and shared between
165 /// hit tester instances via Arc.
166 #[derive(MallocSizeOf)]
167 pub struct HitTestingScene {
168 /// Packed array of all hit test clip nodes
169 clip_nodes: Vec<HitTestClipNode>,
171 /// List of hit testing primitives.
172 items: Vec<HitTestingItem>,
174 /// Current stack of clip ids from stacking context
175 #[ignore_malloc_size_of = "ClipId"]
176 clip_id_stack: Vec<ClipId>,
178 /// Last cached clip id, useful for scenes with a lot
179 /// of hit-test items that reference the same clip
180 #[ignore_malloc_size_of = "simple"]
181 cached_clip_id: Option<(ClipId, ops::Range<ClipNodeIndex>)>,
183 /// Temporary buffer used to de-duplicate clip ids when creating hit
185 #[ignore_malloc_size_of = "ClipId"]
186 seen_clips: FastHashSet<ClipId>,
189 impl HitTestingScene {
190 /// Construct a new hit testing scene, pre-allocating to size
191 /// provided by previous scene stats.
192 pub fn new(stats: &HitTestingSceneStats) -> Self {
194 clip_nodes: Vec::with_capacity(stats.clip_nodes_count),
195 items: Vec::with_capacity(stats.items_count),
196 clip_id_stack: Vec::with_capacity(8),
197 cached_clip_id: None,
198 seen_clips: FastHashSet::default(),
202 /// Get stats about the current scene allocation sizes.
203 pub fn get_stats(&self) -> HitTestingSceneStats {
204 HitTestingSceneStats {
205 clip_nodes_count: self.clip_nodes.len(),
206 items_count: self.items.len(),
210 /// Add a hit testing primitive.
214 info: &LayoutPrimitiveInfo,
215 spatial_node_index: SpatialNodeIndex,
217 clip_store: &ClipStore,
218 interners: &Interners,
220 let clip_range = match self.cached_clip_id {
221 Some((cached_clip_id, ref range)) if cached_clip_id == clip_id => {
225 let start = ClipNodeIndex(self.clip_nodes.len() as u32);
227 // Clear the set of which clip ids have been encountered for this item
228 self.seen_clips.clear();
230 // Flatten all clips from the stacking context hierarchy
231 for clip_id in &self.clip_id_stack {
235 &mut self.clip_nodes,
236 &mut self.seen_clips,
241 // Add the primitive clip
245 &mut self.clip_nodes,
246 &mut self.seen_clips,
250 let end = ClipNodeIndex(self.clip_nodes.len() as u32);
252 let range = ops::Range {
257 self.cached_clip_id = Some((clip_id, range.clone()));
263 let item = HitTestingItem::new(
270 self.items.push(item);
273 /// Push a clip onto the current stack
278 // Invalidate the cache since the stack may affect the produced hit test clip struct
279 self.cached_clip_id = None;
281 self.clip_id_stack.push(clip_id);
284 /// Pop a clip from the current stack
288 // Invalidate the cache since the stack may affect the produced hit test clip struct
289 self.cached_clip_id = None;
291 self.clip_id_stack.pop().unwrap();
295 #[derive(MallocSizeOf)]
298 Rectangle(LayoutRect, ClipMode),
299 RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
300 Polygon(LayoutRect, PolygonKey),
304 fn contains(&self, point: &LayoutPoint) -> bool {
306 HitTestRegion::Rectangle(ref rectangle, ClipMode::Clip) =>
307 rectangle.contains(*point),
308 HitTestRegion::Rectangle(ref rectangle, ClipMode::ClipOut) =>
309 !rectangle.contains(*point),
310 HitTestRegion::RoundedRectangle(rect, radii, ClipMode::Clip) =>
311 rounded_rectangle_contains_point(point, &rect, &radii),
312 HitTestRegion::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
313 !rounded_rectangle_contains_point(point, &rect, &radii),
314 HitTestRegion::Polygon(rect, polygon) =>
315 polygon_contains_point(point, &rect, &polygon),
316 HitTestRegion::Invalid => true,
321 #[derive(MallocSizeOf)]
322 pub struct HitTester {
323 #[ignore_malloc_size_of = "Arc"]
324 scene: Arc<HitTestingScene>,
325 spatial_nodes: Vec<HitTestSpatialNode>,
326 pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
330 pub fn empty() -> Self {
332 scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
333 spatial_nodes: Vec::new(),
334 pipeline_root_nodes: FastHashMap::default(),
339 scene: Arc<HitTestingScene>,
340 spatial_tree: &SpatialTree,
342 let mut hit_tester = HitTester {
344 spatial_nodes: Vec::new(),
345 pipeline_root_nodes: FastHashMap::default(),
347 hit_tester.read_spatial_tree(spatial_tree);
351 fn read_spatial_tree(
353 spatial_tree: &SpatialTree,
355 self.spatial_nodes.clear();
357 self.spatial_nodes.reserve(spatial_tree.spatial_nodes.len());
358 for (index, node) in spatial_tree.spatial_nodes.iter().enumerate() {
359 let index = SpatialNodeIndex::new(index);
361 // If we haven't already seen a node for this pipeline, record this one as the root
363 self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
365 //TODO: avoid inverting more than necessary:
366 // - if the coordinate system is non-invertible, no need to try any of these concrete transforms
367 // - if there are other places where inversion is needed, let's not repeat the step
369 self.spatial_nodes.push(HitTestSpatialNode {
370 pipeline_id: node.pipeline_id,
371 world_content_transform: spatial_tree
372 .get_world_transform(index)
373 .into_fast_transform(),
374 world_viewport_transform: spatial_tree
375 .get_world_viewport_transform(index)
376 .into_fast_transform(),
377 external_scroll_offset: spatial_tree.external_scroll_offset(index),
382 pub fn hit_test(&self, test: HitTest) -> HitTestResult {
383 let mut result = HitTestResult::default();
385 let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
386 let mut point_in_layer = None;
387 let mut current_root_spatial_node_index = SpatialNodeIndex::INVALID;
388 let mut point_in_viewport = None;
390 // For each hit test primitive
391 for item in self.scene.items.iter().rev() {
392 let scroll_node = &self.spatial_nodes[item.spatial_node_index.0 as usize];
393 let pipeline_id = scroll_node.pipeline_id;
394 match (test.pipeline_id, pipeline_id) {
395 (Some(id), node_id) if node_id != id => continue,
399 // Update the cached point in layer space, if the spatial node
400 // changed since last primitive.
401 if item.spatial_node_index != current_spatial_node_index {
402 point_in_layer = scroll_node
403 .world_content_transform
405 .and_then(|inverted| inverted.transform_point2d(test.point));
406 current_spatial_node_index = item.spatial_node_index;
409 // Only consider hit tests on transformable layers.
410 if let Some(point_in_layer) = point_in_layer {
411 // If the item's rect or clip rect don't contain this point,
412 // it's not a valid hit.
413 if !item.rect.contains(point_in_layer) {
416 if !item.clip_rect.contains(point_in_layer) {
420 // See if any of the clips for this primitive cull out the item.
421 let mut is_valid = true;
422 let clip_nodes = &self.scene.clip_nodes[item.clip_nodes_range.start.0 as usize .. item.clip_nodes_range.end.0 as usize];
423 for clip_node in clip_nodes {
425 .spatial_nodes[clip_node.spatial_node_index.0 as usize]
426 .world_content_transform;
427 let transformed_point = match transform
429 .and_then(|inverted| inverted.transform_point2d(test.point))
431 Some(point) => point,
436 if !clip_node.region.contains(&transformed_point) {
445 // Don't hit items with backface-visibility:hidden if they are facing the back.
446 if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() {
450 // We need to calculate the position of the test point relative to the origin of
451 // the pipeline of the hit item. If we cannot get a transformed point, we are
452 // in a situation with an uninvertible transformation so we should just skip this
454 let root_spatial_node_index = self.pipeline_root_nodes[&pipeline_id];
455 if root_spatial_node_index != current_root_spatial_node_index {
456 let root_node = &self.spatial_nodes[root_spatial_node_index.0 as usize];
457 point_in_viewport = root_node
458 .world_viewport_transform
460 .and_then(|inverted| inverted.transform_point2d(test.point))
461 .map(|pt| pt - scroll_node.external_scroll_offset);
463 current_root_spatial_node_index = root_spatial_node_index;
466 if let Some(point_in_viewport) = point_in_viewport {
467 result.items.push(HitTestItem {
468 pipeline: pipeline_id,
471 point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
477 result.items.dedup();
482 #[derive(MallocSizeOf)]
484 pipeline_id: Option<PipelineId>,
490 pipeline_id: Option<PipelineId>,
500 /// Collect clips for a given ClipId, convert and add them to the hit testing
501 /// scene, if not already present.
504 clip_store: &ClipStore,
505 clip_nodes: &mut Vec<HitTestClipNode>,
506 seen_clips: &mut FastHashSet<ClipId>,
507 interners: &Interners,
509 // If this clip-id has already been added to this hit-test item, skip it
510 if seen_clips.contains(&clip_id) {
513 seen_clips.insert(clip_id);
515 let template = &clip_store.templates[&clip_id];
516 let instances = &clip_store.instances[template.clips.start as usize .. template.clips.end as usize];
518 for clip in instances {
519 clip_nodes.alloc().init(
520 HitTestClipNode::new(
522 clip.clip.spatial_node_index,
528 // The ClipId parenting is terminated when we reach the root ClipId
529 if clip_id != template.parent {