hit_test.rs (13546B)
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, HitTestResultItem, HitTestResult, ItemTag, PrimitiveFlags}; 6 use api::{PipelineId, ApiHitTester}; 7 use api::units::*; 8 use crate::clip::{rounded_rectangle_contains_point, ClipNodeId, ClipTreeBuilder}; 9 use crate::clip::{polygon_contains_point, ClipItemKey, ClipItemKeyKind}; 10 use crate::prim_store::PolygonKey; 11 use crate::scene_builder_thread::Interners; 12 use crate::spatial_tree::{SpatialNodeIndex, SpatialTree, get_external_scroll_offset}; 13 use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo}; 14 use std::sync::{Arc, Mutex}; 15 use crate::util::LayoutToWorldFastTransform; 16 17 pub 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 25 impl 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 43 impl 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)] 55 struct 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)] 70 struct 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 80 impl 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)] 114 struct 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 123 impl 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. 144 pub struct HitTestingSceneStats { 145 pub clip_nodes_count: usize, 146 pub items_count: usize, 147 } 148 149 impl 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)] 165 pub struct HitTestingScene { 166 clip_nodes: FastHashMap<ClipNodeId, HitTestClipNode>, 167 168 /// List of hit testing primitives. 169 items: Vec<HitTestingItem>, 170 } 171 172 impl 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)] 255 enum HitTestRegion { 256 Invalid, 257 Rectangle(LayoutRect, ClipMode), 258 RoundedRectangle(LayoutRect, BorderRadius, ClipMode), 259 Polygon(LayoutRect, PolygonKey), 260 } 261 262 impl 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)] 281 pub struct HitTester { 282 #[ignore_malloc_size_of = "Arc"] 283 scene: Arc<HitTestingScene>, 284 spatial_nodes: FastHashMap<SpatialNodeIndex, HitTestSpatialNode>, 285 } 286 287 impl HitTester { 288 pub fn empty() -> Self { 289 HitTester { 290 scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())), 291 spatial_nodes: FastHashMap::default(), 292 } 293 } 294 295 pub fn new( 296 scene: Arc<HitTestingScene>, 297 spatial_tree: &SpatialTree, 298 ) -> HitTester { 299 let mut hit_tester = HitTester { 300 scene, 301 spatial_nodes: FastHashMap::default(), 302 }; 303 hit_tester.read_spatial_tree(spatial_tree); 304 hit_tester 305 } 306 307 fn read_spatial_tree( 308 &mut self, 309 spatial_tree: &SpatialTree, 310 ) { 311 self.spatial_nodes.clear(); 312 self.spatial_nodes.reserve(spatial_tree.spatial_node_count()); 313 314 spatial_tree.visit_nodes(|index, node| { 315 //TODO: avoid inverting more than necessary: 316 // - if the coordinate system is non-invertible, no need to try any of these concrete transforms 317 // - if there are other places where inversion is needed, let's not repeat the step 318 319 self.spatial_nodes.insert(index, HitTestSpatialNode { 320 pipeline_id: node.pipeline_id, 321 world_content_transform: spatial_tree 322 .get_world_transform(index) 323 .into_fast_transform(), 324 world_viewport_transform: spatial_tree 325 .get_world_viewport_transform(index) 326 .into_fast_transform(), 327 external_scroll_offset: get_external_scroll_offset(spatial_tree, index), 328 }); 329 }); 330 } 331 332 pub fn hit_test(&self, test: HitTest) -> HitTestResult { 333 let mut result = HitTestResult::default(); 334 335 let mut current_spatial_node_index = SpatialNodeIndex::INVALID; 336 let mut point_in_layer = None; 337 338 // For each hit test primitive 339 for item in self.scene.items.iter().rev() { 340 let scroll_node = &self.spatial_nodes[&item.spatial_node_index]; 341 let pipeline_id = scroll_node.pipeline_id; 342 343 // Update the cached point in layer space, if the spatial node 344 // changed since last primitive. 345 if item.spatial_node_index != current_spatial_node_index { 346 point_in_layer = scroll_node 347 .world_content_transform 348 .inverse() 349 .and_then(|inverted| inverted.project_point2d(test.point)); 350 current_spatial_node_index = item.spatial_node_index; 351 } 352 353 // Only consider hit tests on transformable layers. 354 let point_in_layer = match point_in_layer { 355 Some(p) => p, 356 None => continue, 357 }; 358 359 // If the item's rect or clip rect don't contain this point, it's 360 // not a valid hit. 361 if !item.rect.contains(point_in_layer) { 362 continue; 363 } 364 365 // See if any of the clips for this primitive cull out the item. 366 let mut current_clip_node_id = item.clip_node_id; 367 let mut is_valid = true; 368 369 while current_clip_node_id != ClipNodeId::NONE { 370 let clip_node = &self.scene.clip_nodes[¤t_clip_node_id]; 371 372 let transform = self 373 .spatial_nodes[&clip_node.spatial_node_index] 374 .world_content_transform; 375 if let Some(transformed_point) = transform 376 .inverse() 377 .and_then(|inverted| inverted.project_point2d(test.point)) 378 { 379 if !clip_node.region.contains(&transformed_point) { 380 is_valid = false; 381 break; 382 } 383 } 384 385 current_clip_node_id = clip_node.parent; 386 } 387 388 if !is_valid { 389 continue; 390 } 391 392 // Don't hit items with backface-visibility:hidden if they are facing the back. 393 if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() { 394 continue; 395 } 396 397 result.items.push(HitTestResultItem { 398 pipeline: pipeline_id, 399 tag: item.tag, 400 animation_id: item.animation_id, 401 }); 402 } 403 404 result.items.dedup(); 405 result 406 } 407 } 408 409 #[derive(MallocSizeOf)] 410 pub struct HitTest { 411 point: WorldPoint, 412 } 413 414 impl HitTest { 415 pub fn new( 416 point: WorldPoint, 417 ) -> HitTest { 418 HitTest { 419 point, 420 } 421 } 422 }