tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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[&current_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 }