summaryrefslogtreecommitdiffstats
path: root/gfx/wr/webrender/src/hit_test.rs
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/wr/webrender/src/hit_test.rs')
-rw-r--r--gfx/wr/webrender/src/hit_test.rs499
1 files changed, 499 insertions, 0 deletions
diff --git a/gfx/wr/webrender/src/hit_test.rs b/gfx/wr/webrender/src/hit_test.rs
new file mode 100644
index 0000000000..c68801ba0c
--- /dev/null
+++ b/gfx/wr/webrender/src/hit_test.rs
@@ -0,0 +1,499 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use api::{BorderRadius, ClipMode, HitTestItem, HitTestResult, ItemTag, PrimitiveFlags};
+use api::{PipelineId, ApiHitTester, ClipId};
+use api::units::*;
+use crate::clip::{ClipItemKind, ClipStore, ClipNode, rounded_rectangle_contains_point};
+use crate::spatial_tree::{SpatialNodeIndex, SpatialTree};
+use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo};
+use std::ops;
+use std::sync::{Arc, Mutex};
+use crate::util::LayoutToWorldFastTransform;
+
+pub struct SharedHitTester {
+ // We don't really need a mutex here. We could do with some sort of
+ // atomic-atomic-ref-counted pointer (an Arc which would let the pointer
+ // be swapped atomically like an AtomicPtr).
+ // In practive this shouldn't cause performance issues, though.
+ hit_tester: Mutex<Arc<HitTester>>,
+}
+
+impl SharedHitTester {
+ pub fn new() -> Self {
+ SharedHitTester {
+ hit_tester: Mutex::new(Arc::new(HitTester::empty())),
+ }
+ }
+
+ pub fn get_ref(&self) -> Arc<HitTester> {
+ let guard = self.hit_tester.lock().unwrap();
+ Arc::clone(&*guard)
+ }
+
+ pub(crate) fn update(&self, new_hit_tester: Arc<HitTester>) {
+ let mut guard = self.hit_tester.lock().unwrap();
+ *guard = new_hit_tester;
+ }
+}
+
+impl ApiHitTester for SharedHitTester {
+ fn hit_test(&self,
+ pipeline_id: Option<PipelineId>,
+ point: WorldPoint,
+ ) -> HitTestResult {
+ self.get_ref().hit_test(HitTest::new(pipeline_id, point))
+ }
+}
+
+/// A copy of important spatial node data to use during hit testing. This a copy of
+/// data from the SpatialTree that will persist as a new frame is under construction,
+/// allowing hit tests consistent with the currently rendered frame.
+#[derive(MallocSizeOf)]
+struct HitTestSpatialNode {
+ /// The pipeline id of this node.
+ pipeline_id: PipelineId,
+
+ /// World transform for content transformed by this node.
+ world_content_transform: LayoutToWorldFastTransform,
+
+ /// World viewport transform for content transformed by this node.
+ world_viewport_transform: LayoutToWorldFastTransform,
+
+ /// The accumulated external scroll offset for this spatial node.
+ external_scroll_offset: LayoutVector2D,
+}
+
+#[derive(MallocSizeOf)]
+struct HitTestClipNode {
+ /// A particular point must be inside all of these regions to be considered clipped in
+ /// for the purposes of a hit test.
+ region: HitTestRegion,
+ /// The positioning node for this clip
+ spatial_node_index: SpatialNodeIndex,
+}
+
+impl HitTestClipNode {
+ fn new(
+ node: ClipNode,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Self {
+ let region = match node.item.kind {
+ ClipItemKind::Rectangle { rect, mode } => {
+ HitTestRegion::Rectangle(rect, mode)
+ }
+ ClipItemKind::RoundedRectangle { rect, radius, mode } => {
+ HitTestRegion::RoundedRectangle(rect, radius, mode)
+ }
+ ClipItemKind::Image { rect, .. } => {
+ HitTestRegion::Rectangle(rect, ClipMode::Clip)
+ }
+ ClipItemKind::BoxShadow { .. } => HitTestRegion::Invalid,
+ };
+
+ HitTestClipNode {
+ region,
+ spatial_node_index,
+ }
+ }
+}
+
+#[derive(Clone, MallocSizeOf)]
+struct HitTestingItem {
+ rect: LayoutRect,
+ clip_rect: LayoutRect,
+ tag: ItemTag,
+ is_backface_visible: bool,
+ spatial_node_index: SpatialNodeIndex,
+ #[ignore_malloc_size_of = "Range"]
+ clip_nodes_range: ops::Range<ClipNodeIndex>,
+}
+
+impl HitTestingItem {
+ fn new(
+ tag: ItemTag,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_nodes_range: ops::Range<ClipNodeIndex>,
+ ) -> HitTestingItem {
+ HitTestingItem {
+ rect: info.rect,
+ clip_rect: info.clip_rect,
+ tag,
+ is_backface_visible: info.flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
+ spatial_node_index,
+ clip_nodes_range,
+ }
+ }
+}
+
+/// Statistics about allocation sizes of current hit tester,
+/// used to pre-allocate size of the next hit tester.
+pub struct HitTestingSceneStats {
+ pub clip_nodes_count: usize,
+ pub items_count: usize,
+}
+
+impl HitTestingSceneStats {
+ pub fn empty() -> Self {
+ HitTestingSceneStats {
+ clip_nodes_count: 0,
+ items_count: 0,
+ }
+ }
+}
+
+#[derive(MallocSizeOf, Debug, Copy, Clone)]
+pub struct ClipNodeIndex(u32);
+
+/// Defines the immutable part of a hit tester for a given scene.
+/// The hit tester is recreated each time a frame is built, since
+/// it relies on the current values of the spatial tree.
+/// However, the clip chain and item definitions don't change,
+/// so they are created once per scene, and shared between
+/// hit tester instances via Arc.
+#[derive(MallocSizeOf)]
+pub struct HitTestingScene {
+ /// Packed array of all hit test clip nodes
+ clip_nodes: Vec<HitTestClipNode>,
+
+ /// List of hit testing primitives.
+ items: Vec<HitTestingItem>,
+
+ /// Current stack of clip ids from stacking context
+ #[ignore_malloc_size_of = "ClipId"]
+ clip_id_stack: Vec<ClipId>,
+
+ /// Last cached clip id, useful for scenes with a lot
+ /// of hit-test items that reference the same clip
+ #[ignore_malloc_size_of = "simple"]
+ cached_clip_id: Option<(ClipId, ops::Range<ClipNodeIndex>)>,
+}
+
+impl HitTestingScene {
+ /// Construct a new hit testing scene, pre-allocating to size
+ /// provided by previous scene stats.
+ pub fn new(stats: &HitTestingSceneStats) -> Self {
+ HitTestingScene {
+ clip_nodes: Vec::with_capacity(stats.clip_nodes_count),
+ items: Vec::with_capacity(stats.items_count),
+ clip_id_stack: Vec::with_capacity(8),
+ cached_clip_id: None,
+ }
+ }
+
+ /// Get stats about the current scene allocation sizes.
+ pub fn get_stats(&self) -> HitTestingSceneStats {
+ HitTestingSceneStats {
+ clip_nodes_count: self.clip_nodes.len(),
+ items_count: self.items.len(),
+ }
+ }
+
+ /// Add a hit testing primitive.
+ pub fn add_item(
+ &mut self,
+ tag: ItemTag,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_id: ClipId,
+ clip_store: &ClipStore,
+ ) {
+ let clip_range = match self.cached_clip_id {
+ Some((cached_clip_id, ref range)) if cached_clip_id == clip_id => {
+ range.clone()
+ }
+ Some(_) | None => {
+ let start = ClipNodeIndex(self.clip_nodes.len() as u32);
+
+ // Flatten all clips from the stacking context hierarchy
+ for clip_id in &self.clip_id_stack {
+ add_clips(
+ *clip_id,
+ clip_store,
+ &mut self.clip_nodes,
+ );
+ }
+
+ // Add the primitive clip
+ add_clips(
+ clip_id,
+ clip_store,
+ &mut self.clip_nodes,
+ );
+
+ let end = ClipNodeIndex(self.clip_nodes.len() as u32);
+
+ let range = ops::Range {
+ start,
+ end,
+ };
+
+ self.cached_clip_id = Some((clip_id, range.clone()));
+
+ range
+ }
+ };
+
+ let item = HitTestingItem::new(
+ tag,
+ info,
+ spatial_node_index,
+ clip_range,
+ );
+
+ self.items.push(item);
+ }
+
+ /// Push a clip onto the current stack
+ pub fn push_clip(
+ &mut self,
+ clip_id: ClipId,
+ ) {
+ // Invalidate the cache since the stack may affect the produced hit test clip struct
+ self.cached_clip_id = None;
+
+ self.clip_id_stack.push(clip_id);
+ }
+
+ /// Pop a clip from the current stack
+ pub fn pop_clip(
+ &mut self,
+ ) {
+ // Invalidate the cache since the stack may affect the produced hit test clip struct
+ self.cached_clip_id = None;
+
+ self.clip_id_stack.pop().unwrap();
+ }
+}
+
+#[derive(MallocSizeOf)]
+enum HitTestRegion {
+ Invalid,
+ Rectangle(LayoutRect, ClipMode),
+ RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
+}
+
+impl HitTestRegion {
+ fn contains(&self, point: &LayoutPoint) -> bool {
+ match *self {
+ HitTestRegion::Rectangle(ref rectangle, ClipMode::Clip) =>
+ rectangle.contains(*point),
+ HitTestRegion::Rectangle(ref rectangle, ClipMode::ClipOut) =>
+ !rectangle.contains(*point),
+ HitTestRegion::RoundedRectangle(rect, radii, ClipMode::Clip) =>
+ rounded_rectangle_contains_point(point, &rect, &radii),
+ HitTestRegion::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
+ !rounded_rectangle_contains_point(point, &rect, &radii),
+ HitTestRegion::Invalid => true,
+ }
+ }
+}
+
+#[derive(MallocSizeOf)]
+pub struct HitTester {
+ #[ignore_malloc_size_of = "Arc"]
+ scene: Arc<HitTestingScene>,
+ spatial_nodes: Vec<HitTestSpatialNode>,
+ pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
+}
+
+impl HitTester {
+ pub fn empty() -> Self {
+ HitTester {
+ scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
+ spatial_nodes: Vec::new(),
+ pipeline_root_nodes: FastHashMap::default(),
+ }
+ }
+
+ pub fn new(
+ scene: Arc<HitTestingScene>,
+ spatial_tree: &SpatialTree,
+ ) -> HitTester {
+ let mut hit_tester = HitTester {
+ scene,
+ spatial_nodes: Vec::new(),
+ pipeline_root_nodes: FastHashMap::default(),
+ };
+ hit_tester.read_spatial_tree(spatial_tree);
+ hit_tester
+ }
+
+ fn read_spatial_tree(
+ &mut self,
+ spatial_tree: &SpatialTree,
+ ) {
+ self.spatial_nodes.clear();
+
+ self.spatial_nodes.reserve(spatial_tree.spatial_nodes.len());
+ for (index, node) in spatial_tree.spatial_nodes.iter().enumerate() {
+ let index = SpatialNodeIndex::new(index);
+
+ // If we haven't already seen a node for this pipeline, record this one as the root
+ // node.
+ self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
+
+ //TODO: avoid inverting more than necessary:
+ // - if the coordinate system is non-invertible, no need to try any of these concrete transforms
+ // - if there are other places where inversion is needed, let's not repeat the step
+
+ self.spatial_nodes.push(HitTestSpatialNode {
+ pipeline_id: node.pipeline_id,
+ world_content_transform: spatial_tree
+ .get_world_transform(index)
+ .into_fast_transform(),
+ world_viewport_transform: spatial_tree
+ .get_world_viewport_transform(index)
+ .into_fast_transform(),
+ external_scroll_offset: spatial_tree.external_scroll_offset(index),
+ });
+ }
+ }
+
+ pub fn hit_test(&self, test: HitTest) -> HitTestResult {
+ let mut result = HitTestResult::default();
+
+ let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
+ let mut point_in_layer = None;
+ let mut current_root_spatial_node_index = SpatialNodeIndex::INVALID;
+ let mut point_in_viewport = None;
+
+ // For each hit test primitive
+ for item in self.scene.items.iter().rev() {
+ let scroll_node = &self.spatial_nodes[item.spatial_node_index.0 as usize];
+ let pipeline_id = scroll_node.pipeline_id;
+ match (test.pipeline_id, pipeline_id) {
+ (Some(id), node_id) if node_id != id => continue,
+ _ => {},
+ }
+
+ // Update the cached point in layer space, if the spatial node
+ // changed since last primitive.
+ if item.spatial_node_index != current_spatial_node_index {
+ point_in_layer = scroll_node
+ .world_content_transform
+ .inverse()
+ .and_then(|inverted| inverted.transform_point2d(test.point));
+ current_spatial_node_index = item.spatial_node_index;
+ }
+
+ // Only consider hit tests on transformable layers.
+ if let Some(point_in_layer) = point_in_layer {
+ // If the item's rect or clip rect don't contain this point,
+ // it's not a valid hit.
+ if !item.rect.contains(point_in_layer) {
+ continue;
+ }
+ if !item.clip_rect.contains(point_in_layer) {
+ continue;
+ }
+
+ // See if any of the clips for this primitive cull out the item.
+ let mut is_valid = true;
+ let clip_nodes = &self.scene.clip_nodes[item.clip_nodes_range.start.0 as usize .. item.clip_nodes_range.end.0 as usize];
+ for clip_node in clip_nodes {
+ let transform = self
+ .spatial_nodes[clip_node.spatial_node_index.0 as usize]
+ .world_content_transform;
+ let transformed_point = match transform
+ .inverse()
+ .and_then(|inverted| inverted.transform_point2d(test.point))
+ {
+ Some(point) => point,
+ None => {
+ continue;
+ }
+ };
+ if !clip_node.region.contains(&transformed_point) {
+ is_valid = false;
+ break;
+ }
+ }
+ if !is_valid {
+ continue;
+ }
+
+ // Don't hit items with backface-visibility:hidden if they are facing the back.
+ if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() {
+ continue;
+ }
+
+ // We need to calculate the position of the test point relative to the origin of
+ // the pipeline of the hit item. If we cannot get a transformed point, we are
+ // in a situation with an uninvertible transformation so we should just skip this
+ // result.
+ let root_spatial_node_index = self.pipeline_root_nodes[&pipeline_id];
+ if root_spatial_node_index != current_root_spatial_node_index {
+ let root_node = &self.spatial_nodes[root_spatial_node_index.0 as usize];
+ point_in_viewport = root_node
+ .world_viewport_transform
+ .inverse()
+ .and_then(|inverted| inverted.transform_point2d(test.point))
+ .map(|pt| pt - scroll_node.external_scroll_offset);
+
+ current_root_spatial_node_index = root_spatial_node_index;
+ }
+
+ if let Some(point_in_viewport) = point_in_viewport {
+ result.items.push(HitTestItem {
+ pipeline: pipeline_id,
+ tag: item.tag,
+ point_in_viewport,
+ point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
+ });
+ }
+ }
+ }
+
+ result.items.dedup();
+ result
+ }
+}
+
+#[derive(MallocSizeOf)]
+pub struct HitTest {
+ pipeline_id: Option<PipelineId>,
+ point: WorldPoint,
+}
+
+impl HitTest {
+ pub fn new(
+ pipeline_id: Option<PipelineId>,
+ point: WorldPoint,
+ ) -> HitTest {
+ HitTest {
+ pipeline_id,
+ point,
+ }
+ }
+}
+
+/// Collect clips for a given ClipId, convert and add them to the hit testing
+/// scene, if not already present.
+fn add_clips(
+ clip_id: ClipId,
+ clip_store: &ClipStore,
+ clip_nodes: &mut Vec<HitTestClipNode>,
+) {
+ let template = &clip_store.templates[&clip_id];
+
+ for clip in &template.clips {
+ let hit_test_clip_node = HitTestClipNode::new(
+ clip.key.into(),
+ clip.clip.spatial_node_index,
+ );
+
+ clip_nodes.push(hit_test_clip_node);
+ }
+
+ // The ClipId parenting is terminated when we reach the root ClipId
+ if clip_id != template.parent {
+ add_clips(
+ template.parent,
+ clip_store,
+ clip_nodes,
+ );
+ }
+}