use api::{BorderRadius, ClipMode, HitTestFlags, HitTestItem, HitTestResult, ItemTag, PrimitiveFlags};
use api::{PipelineId, ApiHitTester};
use api::units::*;
use crate::clip::{ClipChainId, ClipDataStore, ClipNode, ClipItemKind, ClipStore};
use crate::clip::{rounded_rectangle_contains_point};
use crate::spatial_tree::{SpatialNodeIndex, SpatialTree};
use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo};
use std::{ops, u32};
use std::sync::{Arc, Mutex};
use crate::util::LayoutToWorldFastTransform;
pub struct SharedHitTester {
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,
flags: HitTestFlags
) -> HitTestResult {
self.get_ref().hit_test(HitTest::new(pipeline_id, point, flags))
}
}
#[derive(MallocSizeOf)]
pub struct HitTestSpatialNode {
pipeline_id: PipelineId,
world_content_transform: LayoutToWorldFastTransform,
world_viewport_transform: LayoutToWorldFastTransform,
external_scroll_offset: LayoutVector2D,
}
#[derive(MallocSizeOf)]
pub struct HitTestClipNode {
region: HitTestRegion,
}
impl HitTestClipNode {
fn new(node: &ClipNode) -> 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,
}
}
}
#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq, Eq, Hash)]
pub struct HitTestClipChainId(u32);
impl HitTestClipChainId {
pub const NONE: Self = HitTestClipChainId(u32::MAX);
}
#[derive(MallocSizeOf)]
pub struct HitTestClipChainNode {
pub region: HitTestClipNode,
pub spatial_node_index: SpatialNodeIndex,
pub parent_clip_chain_id: HitTestClipChainId,
}
#[derive(Copy, Clone, Debug, MallocSizeOf)]
pub struct HitTestingClipChainIndex(u32);
#[derive(Clone, MallocSizeOf)]
pub struct HitTestingItem {
rect: LayoutRect,
clip_rect: LayoutRect,
tag: ItemTag,
is_backface_visible: bool,
#[ignore_malloc_size_of = "simple"]
clip_chain_range: ops::Range<HitTestingClipChainIndex>,
spatial_node_index: SpatialNodeIndex,
}
impl HitTestingItem {
pub fn new(
tag: ItemTag,
info: &LayoutPrimitiveInfo,
spatial_node_index: SpatialNodeIndex,
clip_chain_range: ops::Range<HitTestingClipChainIndex>,
) -> 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_chain_range,
}
}
}
pub struct HitTestingSceneStats {
pub clip_chain_roots_count: usize,
pub items_count: usize,
}
impl HitTestingSceneStats {
pub fn empty() -> Self {
HitTestingSceneStats {
clip_chain_roots_count: 0,
items_count: 0,
}
}
}
#[derive(MallocSizeOf)]
pub struct HitTestingScene {
pub clip_chain_roots: Vec<HitTestClipChainId>,
pub items: Vec<HitTestingItem>,
}
impl HitTestingScene {
pub fn new(stats: &HitTestingSceneStats) -> Self {
HitTestingScene {
clip_chain_roots: Vec::with_capacity(stats.clip_chain_roots_count),
items: Vec::with_capacity(stats.items_count),
}
}
pub fn get_stats(&self) -> HitTestingSceneStats {
HitTestingSceneStats {
clip_chain_roots_count: self.clip_chain_roots.len(),
items_count: self.items.len(),
}
}
pub fn add_item(&mut self, item: HitTestingItem) {
self.items.push(item);
}
pub fn add_clip_chain(&mut self, clip_chain_id: ClipChainId) {
if clip_chain_id != ClipChainId::INVALID {
self.clip_chain_roots.push(HitTestClipChainId(clip_chain_id.0));
}
}
fn get_clip_chains_for_item(&self, item: &HitTestingItem) -> &[HitTestClipChainId] {
&self.clip_chain_roots[item.clip_chain_range.start.0 as usize .. item.clip_chain_range.end.0 as usize]
}
pub fn next_clip_chain_index(&self) -> HitTestingClipChainIndex {
HitTestingClipChainIndex(self.clip_chain_roots.len() as u32)
}
}
#[derive(MallocSizeOf)]
enum HitTestRegion {
Invalid,
Rectangle(LayoutRect, ClipMode),
RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
}
impl HitTestRegion {
pub 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>,
clip_chains: Vec<HitTestClipChainNode>,
pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
}
impl HitTester {
pub fn empty() -> Self {
HitTester {
scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
spatial_nodes: Vec::new(),
clip_chains: Vec::new(),
pipeline_root_nodes: FastHashMap::default(),
}
}
pub fn new(
scene: Arc<HitTestingScene>,
spatial_tree: &SpatialTree,
clip_store: &ClipStore,
clip_data_store: &ClipDataStore,
) -> HitTester {
let mut hit_tester = HitTester {
scene,
spatial_nodes: Vec::new(),
clip_chains: Vec::new(),
pipeline_root_nodes: FastHashMap::default(),
};
hit_tester.read_spatial_tree(
spatial_tree,
clip_store,
clip_data_store,
);
hit_tester
}
fn read_spatial_tree(
&mut self,
spatial_tree: &SpatialTree,
clip_store: &ClipStore,
clip_data_store: &ClipDataStore,
) {
self.spatial_nodes.clear();
self.clip_chains.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);
self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
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),
});
}
self.clip_chains.reserve(clip_store.clip_chain_nodes.len());
for node in &clip_store.clip_chain_nodes {
let clip_node = &clip_data_store[node.handle];
self.clip_chains.push(HitTestClipChainNode {
region: HitTestClipNode::new(clip_node),
spatial_node_index: clip_node.item.spatial_node_index,
parent_clip_chain_id: HitTestClipChainId(node.parent_clip_chain_id.0),
});
}
}
fn is_point_clipped_in_for_clip_chain(
&self,
point: WorldPoint,
clip_chain_id: HitTestClipChainId,
test: &mut HitTest
) -> bool {
if clip_chain_id == HitTestClipChainId::NONE {
return true;
}
if let Some(result) = test.get_from_clip_chain_cache(clip_chain_id) {
return result == ClippedIn::ClippedIn;
}
let descriptor = &self.clip_chains[clip_chain_id.0 as usize];
let parent_clipped_in = self.is_point_clipped_in_for_clip_chain(
point,
descriptor.parent_clip_chain_id,
test,
);
if !parent_clipped_in {
test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::NotClippedIn);
return false;
}
if !self.is_point_clipped_in_for_clip_node(
point,
clip_chain_id,
descriptor.spatial_node_index,
test,
) {
test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::NotClippedIn);
return false;
}
test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::ClippedIn);
true
}
fn is_point_clipped_in_for_clip_node(
&self,
point: WorldPoint,
clip_chain_node_id: HitTestClipChainId,
spatial_node_index: SpatialNodeIndex,
test: &mut HitTest
) -> bool {
if let Some(clipped_in) = test.node_cache.get(&clip_chain_node_id) {
return *clipped_in == ClippedIn::ClippedIn;
}
let node = &self.clip_chains[clip_chain_node_id.0 as usize].region;
let transform = self
.spatial_nodes[spatial_node_index.0 as usize]
.world_content_transform;
let transformed_point = match transform
.inverse()
.and_then(|inverted| inverted.transform_point2d(point))
{
Some(point) => point,
None => {
test.node_cache.insert(clip_chain_node_id, ClippedIn::NotClippedIn);
return false;
}
};
if !node.region.contains(&transformed_point) {
test.node_cache.insert(clip_chain_node_id, ClippedIn::NotClippedIn);
return false;
}
test.node_cache.insert(clip_chain_node_id, ClippedIn::ClippedIn);
true
}
pub fn find_node_under_point(&self, mut test: HitTest) -> Option<SpatialNodeIndex> {
let point = test.get_absolute_point(self);
let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
let mut point_in_layer = None;
for item in self.scene.items.iter().rev() {
let scroll_node = &self.spatial_nodes[item.spatial_node_index.0 as usize];
if item.spatial_node_index != current_spatial_node_index {
point_in_layer = scroll_node
.world_content_transform
.inverse()
.and_then(|inverted| inverted.transform_point2d(point));
current_spatial_node_index = item.spatial_node_index;
}
if let Some(point_in_layer) = point_in_layer {
if !item.rect.contains(point_in_layer) {
continue;
}
if !item.clip_rect.contains(point_in_layer) {
continue;
}
let clip_chains = self.scene.get_clip_chains_for_item(item);
let mut is_valid = true;
for clip_chain_id in clip_chains {
if !self.is_point_clipped_in_for_clip_chain(point, *clip_chain_id, &mut test) {
is_valid = false;
break;
}
}
if is_valid {
return Some(item.spatial_node_index);
}
}
}
None
}
pub fn hit_test(&self, mut test: HitTest) -> HitTestResult {
let point = test.get_absolute_point(self);
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 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,
_ => {},
}
if item.spatial_node_index != current_spatial_node_index {
point_in_layer = scroll_node
.world_content_transform
.inverse()
.and_then(|inverted| inverted.transform_point2d(point));
current_spatial_node_index = item.spatial_node_index;
}
if let Some(point_in_layer) = point_in_layer {
if !item.rect.contains(point_in_layer) {
continue;
}
if !item.clip_rect.contains(point_in_layer) {
continue;
}
let clip_chains = self.scene.get_clip_chains_for_item(item);
let mut is_valid = true;
for clip_chain_id in clip_chains {
if !self.is_point_clipped_in_for_clip_chain(point, *clip_chain_id, &mut test) {
is_valid = false;
break;
}
}
if !is_valid {
continue;
}
if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() {
continue;
}
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(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(),
});
if !test.flags.contains(HitTestFlags::FIND_ALL) {
return result;
}
}
}
}
result.items.dedup();
result
}
pub fn get_pipeline_root(&self, pipeline_id: PipelineId) -> &HitTestSpatialNode {
&self.spatial_nodes[self.pipeline_root_nodes[&pipeline_id].0 as usize]
}
}
#[derive(Clone, Copy, MallocSizeOf, PartialEq)]
enum ClippedIn {
ClippedIn,
NotClippedIn,
}
#[derive(MallocSizeOf)]
pub struct HitTest {
pipeline_id: Option<PipelineId>,
point: WorldPoint,
flags: HitTestFlags,
node_cache: FastHashMap<HitTestClipChainId, ClippedIn>,
clip_chain_cache: Vec<Option<ClippedIn>>,
}
impl HitTest {
pub fn new(
pipeline_id: Option<PipelineId>,
point: WorldPoint,
flags: HitTestFlags,
) -> HitTest {
HitTest {
pipeline_id,
point,
flags,
node_cache: FastHashMap::default(),
clip_chain_cache: Vec::new(),
}
}
fn get_from_clip_chain_cache(&mut self, index: HitTestClipChainId) -> Option<ClippedIn> {
let index = index.0 as usize;
if index >= self.clip_chain_cache.len() {
None
} else {
self.clip_chain_cache[index]
}
}
fn set_in_clip_chain_cache(&mut self, index: HitTestClipChainId, value: ClippedIn) {
let index = index.0 as usize;
if index >= self.clip_chain_cache.len() {
self.clip_chain_cache.resize(index + 1, None);
}
self.clip_chain_cache[index] = Some(value);
}
fn get_absolute_point(&self, hit_tester: &HitTester) -> WorldPoint {
if !self.flags.contains(HitTestFlags::POINT_RELATIVE_TO_PIPELINE_VIEWPORT) {
return self.point;
}
let point = LayoutPoint::new(self.point.x, self.point.y);
self.pipeline_id
.and_then(|id|
hit_tester
.get_pipeline_root(id)
.world_viewport_transform
.transform_point2d(point)
)
.unwrap_or_else(|| {
WorldPoint::new(self.point.x, self.point.y)
})
}
}