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 
5 use api::{BorderRadius, ClipMode, HitTestFlags, HitTestItem, HitTestResult, ItemTag, LayerPoint};
6 use api::{LayerPrimitiveInfo, LayerRect, LocalClip, PipelineId, WorldPoint};
7 use clip::{ClipSource, ClipStore, Contains, rounded_rectangle_contains_point};
8 use clip_scroll_node::{ClipScrollNode, NodeType};
9 use clip_scroll_tree::{ClipChainIndex, ClipScrollNodeIndex, ClipScrollTree};
10 use internal_types::FastHashMap;
11 use prim_store::ScrollNodeAndClipChain;
12 use util::LayerToWorldFastTransform;
13 
14 /// A copy of important clip scroll node data to use during hit testing. This a copy of
15 /// data from the ClipScrollTree that will persist as a new frame is under construction,
16 /// allowing hit tests consistent with the currently rendered frame.
17 pub struct HitTestClipScrollNode {
18     /// The pipeline id of this node.
19     pipeline_id: PipelineId,
20 
21     /// A particular point must be inside all of these regions to be considered clipped in
22     /// for the purposes of a hit test.
23     regions: Vec<HitTestRegion>,
24 
25     /// World transform for content transformed by this node.
26     world_content_transform: LayerToWorldFastTransform,
27 
28     /// World viewport transform for content transformed by this node.
29     world_viewport_transform: LayerToWorldFastTransform,
30 
31     /// Origin of the viewport of the node, used to calculate node-relative positions.
32     node_origin: LayerPoint,
33 }
34 
35 /// A description of a clip chain in the HitTester. This is used to describe
36 /// hierarchical clip scroll nodes as well as ClipChains, so that they can be
37 /// handled the same way during hit testing. Once we represent all ClipChains
38 /// using ClipChainDescriptors, we can get rid of this and just use the
39 /// ClipChainDescriptor here.
40 #[derive(Clone)]
41 struct HitTestClipChainDescriptor {
42     parent: Option<ClipChainIndex>,
43     clips: Vec<ClipScrollNodeIndex>,
44 }
45 
46 impl HitTestClipChainDescriptor {
empty() -> HitTestClipChainDescriptor47     fn empty() -> HitTestClipChainDescriptor {
48         HitTestClipChainDescriptor {
49             parent: None,
50             clips: Vec::new(),
51         }
52     }
53 }
54 
55 #[derive(Clone)]
56 pub struct HitTestingItem {
57     rect: LayerRect,
58     clip: LocalClip,
59     tag: ItemTag,
60 }
61 
62 impl HitTestingItem {
new(tag: ItemTag, info: &LayerPrimitiveInfo) -> HitTestingItem63     pub fn new(tag: ItemTag, info: &LayerPrimitiveInfo) -> HitTestingItem {
64         HitTestingItem {
65             rect: info.rect,
66             clip: info.local_clip,
67             tag: tag,
68         }
69     }
70 }
71 
72 #[derive(Clone)]
73 pub struct HitTestingRun(pub Vec<HitTestingItem>, pub ScrollNodeAndClipChain);
74 
75 enum HitTestRegion {
76     Rectangle(LayerRect),
77     RoundedRectangle(LayerRect, BorderRadius, ClipMode),
78 }
79 
80 impl HitTestRegion {
contains(&self, point: &LayerPoint) -> bool81     pub fn contains(&self, point: &LayerPoint) -> bool {
82         match self {
83             &HitTestRegion::Rectangle(ref rectangle) => rectangle.contains(point),
84             &HitTestRegion::RoundedRectangle(rect, radii, ClipMode::Clip) =>
85                 rounded_rectangle_contains_point(point, &rect, &radii),
86             &HitTestRegion::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
87                 !rounded_rectangle_contains_point(point, &rect, &radii),
88         }
89     }
90 }
91 
92 pub struct HitTester {
93     runs: Vec<HitTestingRun>,
94     nodes: Vec<HitTestClipScrollNode>,
95     clip_chains: Vec<HitTestClipChainDescriptor>,
96     pipeline_root_nodes: FastHashMap<PipelineId, ClipScrollNodeIndex>,
97 }
98 
99 impl HitTester {
new( runs: &Vec<HitTestingRun>, clip_scroll_tree: &ClipScrollTree, clip_store: &ClipStore ) -> HitTester100     pub fn new(
101         runs: &Vec<HitTestingRun>,
102         clip_scroll_tree: &ClipScrollTree,
103         clip_store: &ClipStore
104     ) -> HitTester {
105         let mut hit_tester = HitTester {
106             runs: runs.clone(),
107             nodes: Vec::new(),
108             clip_chains: Vec::new(),
109             pipeline_root_nodes: FastHashMap::default(),
110         };
111         hit_tester.read_clip_scroll_tree(clip_scroll_tree, clip_store);
112         hit_tester
113     }
114 
read_clip_scroll_tree( &mut self, clip_scroll_tree: &ClipScrollTree, clip_store: &ClipStore )115     fn read_clip_scroll_tree(
116         &mut self,
117         clip_scroll_tree: &ClipScrollTree,
118         clip_store: &ClipStore
119     ) {
120         self.nodes.clear();
121         self.clip_chains.clear();
122         self.clip_chains.resize(
123             clip_scroll_tree.clip_chains.len(),
124             HitTestClipChainDescriptor::empty()
125         );
126 
127         for (index, node) in clip_scroll_tree.nodes.iter().enumerate() {
128             let index = ClipScrollNodeIndex(index);
129 
130             // If we haven't already seen a node for this pipeline, record this one as the root
131             // node.
132             self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
133 
134             self.nodes.push(HitTestClipScrollNode {
135                 pipeline_id: node.pipeline_id,
136                 regions: get_regions_for_clip_scroll_node(node, clip_store),
137                 world_content_transform: node.world_content_transform,
138                 world_viewport_transform: node.world_viewport_transform,
139                 node_origin: node.local_viewport_rect.origin,
140             });
141 
142             if let NodeType::Clip { clip_chain_index, .. } = node.node_type {
143               let clip_chain = self.clip_chains.get_mut(clip_chain_index.0).unwrap();
144               clip_chain.parent =
145                   clip_scroll_tree.get_clip_chain(clip_chain_index).parent_index;
146               clip_chain.clips = vec![index];
147             }
148         }
149 
150         for descriptor in &clip_scroll_tree.clip_chains_descriptors {
151             let clip_chain = self.clip_chains.get_mut(descriptor.index.0).unwrap();
152             clip_chain.parent = clip_scroll_tree.get_clip_chain(descriptor.index).parent_index;
153             clip_chain.clips = descriptor.clips.clone();
154         }
155     }
156 
is_point_clipped_in_for_clip_chain( &self, point: WorldPoint, clip_chain_index: ClipChainIndex, test: &mut HitTest ) -> bool157     fn is_point_clipped_in_for_clip_chain(
158         &self,
159         point: WorldPoint,
160         clip_chain_index: ClipChainIndex,
161         test: &mut HitTest
162     ) -> bool {
163         if let Some(result) = test.get_from_clip_chain_cache(clip_chain_index) {
164             return result;
165         }
166 
167         let descriptor = &self.clip_chains[clip_chain_index.0];
168         let parent_clipped_in = match descriptor.parent {
169             None => true,
170             Some(parent) => self.is_point_clipped_in_for_clip_chain(point, parent, test),
171         };
172 
173         if !parent_clipped_in {
174             test.set_in_clip_chain_cache(clip_chain_index, false);
175             return false;
176         }
177 
178         for clip_node_index in &descriptor.clips {
179             if !self.is_point_clipped_in_for_node(point, *clip_node_index, test) {
180                 test.set_in_clip_chain_cache(clip_chain_index, false);
181                 return false;
182             }
183         }
184 
185         test.set_in_clip_chain_cache(clip_chain_index, true);
186         true
187     }
188 
is_point_clipped_in_for_node( &self, point: WorldPoint, node_index: ClipScrollNodeIndex, test: &mut HitTest ) -> bool189     fn is_point_clipped_in_for_node(
190         &self,
191         point: WorldPoint,
192         node_index: ClipScrollNodeIndex,
193         test: &mut HitTest
194     ) -> bool {
195         if let Some(point) = test.node_cache.get(&node_index) {
196             return point.is_some();
197         }
198 
199         let node = &self.nodes[node_index.0];
200         let transform = node.world_viewport_transform;
201         let transformed_point = match transform.inverse() {
202             Some(inverted) => inverted.transform_point2d(&point),
203             None => {
204                 test.node_cache.insert(node_index, None);
205                 return false;
206             }
207         };
208 
209         let point_in_layer = transformed_point - node.node_origin.to_vector();
210         for region in &node.regions {
211             if !region.contains(&transformed_point) {
212                 test.node_cache.insert(node_index, None);
213                 return false;
214             }
215         }
216 
217         test.node_cache.insert(node_index, Some(point_in_layer));
218         true
219     }
220 
hit_test(&self, mut test: HitTest) -> HitTestResult221     pub fn hit_test(&self, mut test: HitTest) -> HitTestResult {
222         let point = test.get_absolute_point(self);
223 
224         let mut result = HitTestResult::default();
225         for &HitTestingRun(ref items, ref clip_and_scroll) in self.runs.iter().rev() {
226             let scroll_node_id = clip_and_scroll.scroll_node_id;
227             let scroll_node = &self.nodes[scroll_node_id.0];
228             let pipeline_id = scroll_node.pipeline_id;
229             match (test.pipeline_id, pipeline_id) {
230                 (Some(id), node_id) if node_id != id => continue,
231                 _ => {},
232             }
233 
234             let transform = scroll_node.world_content_transform;
235             let point_in_layer = match transform.inverse() {
236                 Some(inverted) => inverted.transform_point2d(&point),
237                 None => continue,
238             };
239 
240             let mut clipped_in = false;
241             for item in items.iter().rev() {
242                 if !item.rect.contains(&point_in_layer) || !item.clip.contains(&point_in_layer) {
243                     continue;
244                 }
245 
246                 let clip_chain_index = clip_and_scroll.clip_chain_index;
247                 clipped_in |=
248                     self.is_point_clipped_in_for_clip_chain(point, clip_chain_index, &mut test);
249                 if !clipped_in {
250                     break;
251                 }
252 
253                 // We need to trigger a lookup against the root reference frame here, because
254                 // items that are clipped by clip chains won't test against that part of the
255                 // hierarchy. If we don't have a valid point for this test, we are likely
256                 // in a situation where the reference frame has an univertible transform, but the
257                 // item's clip does not.
258                 let root_node_index = self.pipeline_root_nodes[&pipeline_id];
259                 if !self.is_point_clipped_in_for_node(point, root_node_index, &mut test) {
260                     continue;
261                 }
262                 let point_in_viewport = match test.node_cache[&root_node_index] {
263                     Some(point) => point,
264                     None => continue,
265                 };
266 
267                 result.items.push(HitTestItem {
268                     pipeline: pipeline_id,
269                     tag: item.tag,
270                     point_in_viewport,
271                     point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
272                 });
273                 if !test.flags.contains(HitTestFlags::FIND_ALL) {
274                     return result;
275                 }
276             }
277         }
278 
279         result.items.dedup();
280         result
281     }
282 
get_pipeline_root(&self, pipeline_id: PipelineId) -> &HitTestClipScrollNode283     pub fn get_pipeline_root(&self, pipeline_id: PipelineId) -> &HitTestClipScrollNode {
284         &self.nodes[self.pipeline_root_nodes[&pipeline_id].0]
285     }
286 }
287 
get_regions_for_clip_scroll_node( node: &ClipScrollNode, clip_store: &ClipStore ) -> Vec<HitTestRegion>288 fn get_regions_for_clip_scroll_node(
289     node: &ClipScrollNode,
290     clip_store: &ClipStore
291 ) -> Vec<HitTestRegion> {
292     let clips = match node.node_type {
293         NodeType::Clip{ ref handle, .. } => clip_store.get(handle).clips(),
294         _ => return Vec::new(),
295     };
296 
297     clips.iter().map(|ref source| {
298         match source.0 {
299             ClipSource::Rectangle(ref rect) => HitTestRegion::Rectangle(*rect),
300             ClipSource::RoundedRectangle(ref rect, ref radii, ref mode) =>
301                 HitTestRegion::RoundedRectangle(*rect, *radii, *mode),
302             ClipSource::Image(ref mask) => HitTestRegion::Rectangle(mask.rect),
303             ClipSource::BorderCorner(_) |
304             ClipSource::BoxShadow(_) => {
305                 unreachable!("Didn't expect to hit test against BorderCorner / BoxShadow");
306             }
307         }
308     }).collect()
309 }
310 
311 pub struct HitTest {
312     pipeline_id: Option<PipelineId>,
313     point: WorldPoint,
314     flags: HitTestFlags,
315     node_cache: FastHashMap<ClipScrollNodeIndex, Option<LayerPoint>>,
316     clip_chain_cache: Vec<Option<bool>>,
317 }
318 
319 impl HitTest {
new( pipeline_id: Option<PipelineId>, point: WorldPoint, flags: HitTestFlags, ) -> HitTest320     pub fn new(
321         pipeline_id: Option<PipelineId>,
322         point: WorldPoint,
323         flags: HitTestFlags,
324     ) -> HitTest {
325         HitTest {
326             pipeline_id,
327             point,
328             flags,
329             node_cache: FastHashMap::default(),
330             clip_chain_cache: Vec::new(),
331         }
332     }
333 
get_from_clip_chain_cache(&mut self, index: ClipChainIndex) -> Option<bool>334     pub fn get_from_clip_chain_cache(&mut self, index: ClipChainIndex) -> Option<bool> {
335         if index.0 >= self.clip_chain_cache.len() {
336             None
337         } else {
338             self.clip_chain_cache[index.0]
339         }
340     }
341 
set_in_clip_chain_cache(&mut self, index: ClipChainIndex, value: bool)342     pub fn set_in_clip_chain_cache(&mut self, index: ClipChainIndex, value: bool) {
343         if index.0 >= self.clip_chain_cache.len() {
344             self.clip_chain_cache.resize(index.0 + 1, None);
345         }
346         self.clip_chain_cache[index.0] = Some(value);
347     }
348 
get_absolute_point(&self, hit_tester: &HitTester) -> WorldPoint349     pub fn get_absolute_point(&self, hit_tester: &HitTester) -> WorldPoint {
350         if !self.flags.contains(HitTestFlags::POINT_RELATIVE_TO_PIPELINE_VIEWPORT) {
351             return self.point;
352         }
353 
354         let point =  &LayerPoint::new(self.point.x, self.point.y);
355         self.pipeline_id.map(|id|
356             hit_tester.get_pipeline_root(id).world_viewport_transform.transform_point2d(&point)
357         ).unwrap_or_else(|| WorldPoint::new(self.point.x, self.point.y))
358     }
359 }
360