From 974e744926cb5339c0c8e2c7147e743d788a4d69 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 24 Jan 2024 15:22:18 +0100 Subject: [PATCH] Restore some hit testing APIs These APIs are still used by Servo, so keep them around until Servo can migrate off of them. --- examples/scrolling.rs | 2 + webrender/src/hit_test.rs | 93 +++++++++++++++++++++++++++++---- webrender/src/render_api.rs | 8 +-- webrender/src/render_backend.rs | 4 +- webrender_api/src/lib.rs | 28 ++++++++-- wrench/src/main.rs | 2 + wrench/src/rawtest.rs | 2 + 7 files changed, 120 insertions(+), 19 deletions(-) diff --git a/examples/scrolling.rs b/examples/scrolling.rs index 315b945d20..7d3dd1b2d6 100644 --- a/examples/scrolling.rs +++ b/examples/scrolling.rs @@ -271,7 +271,9 @@ impl Example for App { winit::event::WindowEvent::MouseInput { .. } => { let results = api.hit_test( document_id, + None, self.cursor_position, + HitTestFlags::empty(), ); println!("Hit test results:"); diff --git a/webrender/src/hit_test.rs b/webrender/src/hit_test.rs index 34a7e7e404..de99e419d9 100644 --- a/webrender/src/hit_test.rs +++ b/webrender/src/hit_test.rs @@ -2,8 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use api::{BorderRadius, ClipMode, HitTestResultItem, HitTestResult, ItemTag, PrimitiveFlags}; -use api::{PipelineId, ApiHitTester}; +use api::{BorderRadius, ClipMode, HitTestFlags, HitTestResultItem, HitTestResult, ItemTag, PrimitiveFlags}; +use api::{ApiHitTester, PipelineId}; use api::units::*; use crate::clip::{rounded_rectangle_contains_point, ClipNodeId, ClipTreeBuilder}; use crate::clip::{polygon_contains_point, ClipItemKey, ClipItemKeyKind}; @@ -41,10 +41,13 @@ impl SharedHitTester { } impl ApiHitTester for SharedHitTester { - fn hit_test(&self, + fn hit_test( + &self, + pipeline_id: Option, point: WorldPoint, + flags: HitTestFlags, ) -> HitTestResult { - self.get_ref().hit_test(HitTest::new(point)) + self.get_ref().hit_test(HitTest::new(pipeline_id, point, flags)) } } @@ -280,6 +283,7 @@ pub struct HitTester { #[ignore_malloc_size_of = "Arc"] scene: Arc, spatial_nodes: FastHashMap, + pipeline_root_nodes: FastHashMap, } impl HitTester { @@ -287,6 +291,7 @@ impl HitTester { HitTester { scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())), spatial_nodes: FastHashMap::default(), + pipeline_root_nodes: FastHashMap::default(), } } @@ -297,6 +302,7 @@ impl HitTester { let mut hit_tester = HitTester { scene, spatial_nodes: FastHashMap::default(), + pipeline_root_nodes: FastHashMap::default(), }; hit_tester.read_spatial_tree(spatial_tree); hit_tester @@ -309,7 +315,13 @@ impl HitTester { self.spatial_nodes.clear(); self.spatial_nodes.reserve(spatial_tree.spatial_node_count()); + self.pipeline_root_nodes.clear(); + spatial_tree.visit_nodes(|index, node| { + // If we haven't already seen a node for this pipeline, record this one as the root + // node. + self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index); + //TODO: avoid inverting more than necessary: // - if the coordinate system is non-invertible, no need to try any of these concrete transforms // - if there are other places where inversion is needed, let's not repeat the step @@ -328,10 +340,14 @@ impl HitTester { } pub fn hit_test(&self, 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 each hit test primitive for item in self.scene.items.iter().rev() { @@ -344,7 +360,7 @@ impl HitTester { point_in_layer = scroll_node .world_content_transform .inverse() - .and_then(|inverted| inverted.project_point2d(test.point)); + .and_then(|inverted| inverted.project_point2d(point)); current_spatial_node_index = item.spatial_node_index; } @@ -372,7 +388,7 @@ impl HitTester { .world_content_transform; if let Some(transformed_point) = transform .inverse() - .and_then(|inverted| inverted.project_point2d(test.point)) + .and_then(|inverted| inverted.project_point2d(point)) { if !clip_node.region.contains(&transformed_point) { is_valid = false; @@ -392,29 +408,84 @@ impl HitTester { continue; } - result.items.push(HitTestResultItem { - pipeline: pipeline_id, - tag: item.tag, - animation_id: item.animation_id, - }); + // We need to calculate the position of the test point relative to the origin of + // the pipeline of the hit item. If we cannot get a transformed point, we are + // in a situation with an uninvertible transformation so we should just skip this + // result. + 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]; + point_in_viewport = root_node + .world_viewport_transform + .inverse() + .and_then(|inverted| inverted.transform_point2d(test.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(HitTestResultItem { + pipeline: pipeline_id, + tag: item.tag, + animation_id: item.animation_id, + point_in_viewport, + point_relative_to_item: point_in_layer - item.rect.min.to_vector(), + }); + } + + if !test.flags.contains(HitTestFlags::FIND_ALL) { + return result; + } } result.items.dedup(); result } + + fn get_pipeline_root(&self, pipeline_id: PipelineId) -> &HitTestSpatialNode { + &self.spatial_nodes[&self.pipeline_root_nodes[&pipeline_id]] + } + } #[derive(MallocSizeOf)] pub struct HitTest { + pipeline_id: Option, point: WorldPoint, + #[ignore_malloc_size_of = "bitflags"] + flags: HitTestFlags, } impl HitTest { pub fn new( + pipeline_id: Option, point: WorldPoint, + flags: HitTestFlags, ) -> HitTest { HitTest { + pipeline_id, point, + flags } } + + 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) + }) + } + } diff --git a/webrender/src/render_api.rs b/webrender/src/render_api.rs index 2db2016f34..7c0ec809c4 100644 --- a/webrender/src/render_api.rs +++ b/webrender/src/render_api.rs @@ -10,7 +10,7 @@ use std::marker::PhantomData; use std::path::PathBuf; use std::sync::Arc; use std::u32; -use api::MinimapData; +use api::{HitTestFlags, MinimapData}; use time::precise_time_ns; use crate::api::channel::{Sender, single_msg_channel, unbounded_channel}; use crate::api::{BuiltDisplayList, IdNamespace, ExternalScrollId, Parameter, BoolParameter}; @@ -792,7 +792,7 @@ pub enum FrameMsg { /// UpdateEpoch(PipelineId, Epoch), /// - HitTest(WorldPoint, Sender), + HitTest(Option, WorldPoint, HitTestFlags, Sender), /// RequestHitTester(Sender>), /// @@ -1276,13 +1276,15 @@ impl RenderApi { /// front to back. pub fn hit_test(&self, document_id: DocumentId, + pipeline_id: Option, point: WorldPoint, + flags: HitTestFlags, ) -> HitTestResult { let (tx, rx) = single_msg_channel(); self.send_frame_msg( document_id, - FrameMsg::HitTest(point, tx) + FrameMsg::HitTest(pipeline_id, point, flags, tx) ); rx.recv().unwrap() } diff --git a/webrender/src/render_backend.rs b/webrender/src/render_backend.rs index ff4de9e82e..4c4aa0b6a0 100644 --- a/webrender/src/render_backend.rs +++ b/webrender/src/render_backend.rs @@ -443,14 +443,14 @@ impl Document { FrameMsg::UpdateEpoch(pipeline_id, epoch) => { self.scene.pipeline_epochs.insert(pipeline_id, epoch); } - FrameMsg::HitTest(point, tx) => { + FrameMsg::HitTest(pipeline_id, point, flags, tx) => { if !self.hit_tester_is_valid { self.rebuild_hit_tester(); } let result = match self.hit_tester { Some(ref hit_tester) => { - hit_tester.hit_test(HitTest::new(point)) + hit_tester.hit_test(HitTest::new(pipeline_id, point, flags)) } None => HitTestResult { items: Vec::new() }, }; diff --git a/webrender_api/src/lib.rs b/webrender_api/src/lib.rs index fb8aba5e58..0d4c630424 100644 --- a/webrender_api/src/lib.rs +++ b/webrender_api/src/lib.rs @@ -336,9 +336,11 @@ impl NotificationRequest { /// the RenderBackendThread. pub trait ApiHitTester: Send + Sync { /// Does a hit test on display items in the specified document, at the given - /// point. The vector of hit results will contain all display items that match, - /// ordered from front to back. - fn hit_test(&self, point: WorldPoint) -> HitTestResult; + /// point. If a pipeline_id is specified, it is used to further restrict the + /// hit results so that only items inside that pipeline are matched. The vector + /// of hit results will contain all display items that match, ordered from + /// front to back. + fn hit_test(&self, pipeline_id: Option, point: WorldPoint, flags: HitTestFlags) -> HitTestResult; } /// A hit tester requested to the render backend thread but not necessarily ready yet. @@ -367,6 +369,15 @@ pub struct HitTestResultItem { /// The animation id from the stacking context. pub animation_id: u64, + + /// The hit point in the coordinate space of the "viewport" of the display item. The + /// viewport is the scroll node formed by the root reference frame of the display item's + /// pipeline. + pub point_in_viewport: LayoutPoint, + + /// The coordinates of the original hit test point relative to the origin of this item. + /// This is useful for calculating things like text offsets in the client. + pub point_relative_to_item: LayoutPoint, } /// Returned by `RenderApi::hit_test`. @@ -376,6 +387,17 @@ pub struct HitTestResult { pub items: Vec, } +bitflags! { + #[derive(Deserialize, Serialize)] + /// + pub struct HitTestFlags: u8 { + /// + const FIND_ALL = 0b00000001; + /// + const POINT_RELATIVE_TO_PIPELINE_VIEWPORT = 0b00000010; + } +} + impl Drop for NotificationRequest { fn drop(&mut self) { if let Some(ref mut handler) = self.handler { diff --git a/wrench/src/main.rs b/wrench/src/main.rs index d7280f9545..53963c808e 100644 --- a/wrench/src/main.rs +++ b/wrench/src/main.rs @@ -984,7 +984,9 @@ fn render<'a>( VirtualKeyCode::X => { let results = wrench.api.hit_test( wrench.document_id, + None, cursor_position, + HitTestFlags::empty(), ); println!("Hit test results:"); diff --git a/wrench/src/rawtest.rs b/wrench/src/rawtest.rs index 19d3b025f7..40776933d0 100644 --- a/wrench/src/rawtest.rs +++ b/wrench/src/rawtest.rs @@ -1387,7 +1387,9 @@ impl<'a> RawtestHarness<'a> { let hit_test = |point: WorldPoint| -> HitTestResult { self.wrench.api.hit_test( self.wrench.document_id, + None, point, + HitTestFlags::empty(), ) };