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