mirror of
https://github.com/servo/servo.git
synced 2025-09-27 23:30:08 +01:00
compositor/layout: Rely on layout for fine-grained input event hit testing (#38480)
Before, the compositor was responsible for doing the hit testing during input events within a page. This change moves that hit testing to layout. With this change, epoch mismatches are no longer a bit deal and we can simply ignore them, as the Constellation and Script will take care of ignoring hit tests against scroll nodes and browsing contexts that no longer exist. This means that hit testing retry support can be removed. Add the concept of a Script `HitTest` that transforms the coarse-grained renderer hit test into one that hit tests against the actual layout items. Testing: Currently we do not have good tests for verifying the behavior of input events, but WebDriver tests should cover this. Fixes: This is part of #37932. Fixes: #26608. Fixes: #25282. Fixes: #38090. Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Oriol Brufau <obrufau@igalia.com> Co-authored-by: kongbai1996 <1782765876@qq.com>
This commit is contained in:
parent
c0cc8484f8
commit
ad805e3110
19 changed files with 348 additions and 511 deletions
|
@ -10,7 +10,7 @@ use std::collections::HashMap;
|
|||
use base::id::ScrollTreeNodeId;
|
||||
use base::print_tree::PrintTree;
|
||||
use bitflags::bitflags;
|
||||
use embedder_traits::{Cursor, ViewportDetails};
|
||||
use embedder_traits::ViewportDetails;
|
||||
use euclid::{SideOffsets2D, Transform3D};
|
||||
use malloc_size_of_derive::MallocSizeOf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -57,20 +57,6 @@ pub struct AxesScrollSensitivity {
|
|||
pub y: ScrollType,
|
||||
}
|
||||
|
||||
/// Information that Servo keeps alongside WebRender display items
|
||||
/// in order to add more context to hit test results.
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct HitTestInfo {
|
||||
/// The id of the node of this hit test item.
|
||||
pub node: u64,
|
||||
|
||||
/// The cursor of this node's hit test item.
|
||||
pub cursor: Option<Cursor>,
|
||||
|
||||
/// The id of the [ScrollTree] associated with this hit test item.
|
||||
pub scroll_tree_node: ScrollTreeNodeId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum SpatialTreeNodeInfo {
|
||||
ReferenceFrame(ReferenceFrameNodeInfo),
|
||||
|
@ -514,17 +500,33 @@ impl ScrollTree {
|
|||
})
|
||||
}
|
||||
|
||||
/// Scroll the given scroll node on this scroll tree. If the node cannot be scrolled,
|
||||
/// because it isn't a scrollable node or it's already scrolled to the maximum scroll
|
||||
fn node_with_external_scroll_node_id(
|
||||
&self,
|
||||
external_id: &ExternalScrollId,
|
||||
) -> Option<ScrollTreeNodeId> {
|
||||
self.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(index, node)| match &node.info {
|
||||
SpatialTreeNodeInfo::Scroll(info) if info.external_id == *external_id => {
|
||||
Some(ScrollTreeNodeId { index })
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Scroll the scroll node with the given [`ExternalScrollId`] on this scroll tree. If
|
||||
/// the node cannot be scrolled, because it's already scrolled to the maximum scroll
|
||||
/// extent, try to scroll an ancestor of this node. Returns the node scrolled and the
|
||||
/// new offset if a scroll was performed, otherwise returns None.
|
||||
pub fn scroll_node_or_ancestor(
|
||||
&mut self,
|
||||
scroll_node_id: &ScrollTreeNodeId,
|
||||
external_id: &ExternalScrollId,
|
||||
scroll_location: ScrollLocation,
|
||||
context: ScrollType,
|
||||
) -> Option<(ExternalScrollId, LayoutVector2D)> {
|
||||
let result = self.scroll_node_or_ancestor_inner(scroll_node_id, scroll_location, context);
|
||||
let scroll_node_id = self.node_with_external_scroll_node_id(external_id)?;
|
||||
let result = self.scroll_node_or_ancestor_inner(&scroll_node_id, scroll_location, context);
|
||||
if result.is_some() {
|
||||
self.invalidate_cached_transforms();
|
||||
}
|
||||
|
@ -713,6 +715,22 @@ impl ScrollTree {
|
|||
};
|
||||
root_node.invalidate_cached_transforms(self, false /* ancestors_invalid */);
|
||||
}
|
||||
|
||||
fn external_scroll_id_for_scroll_tree_node(
|
||||
&self,
|
||||
id: ScrollTreeNodeId,
|
||||
) -> Option<ExternalScrollId> {
|
||||
let mut maybe_node = Some(self.get_node(&id));
|
||||
|
||||
while let Some(node) = maybe_node {
|
||||
if let Some(external_scroll_id) = node.external_id() {
|
||||
return Some(external_scroll_id);
|
||||
}
|
||||
maybe_node = node.parent.map(|id| self.get_node(&id));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// In order to pretty print the [ScrollTree] structure, we are converting
|
||||
|
@ -787,11 +805,6 @@ pub struct CompositorDisplayListInfo {
|
|||
/// The epoch of the display list.
|
||||
pub epoch: Epoch,
|
||||
|
||||
/// An array of `HitTestInfo` which is used to store information
|
||||
/// to assist the compositor to take various actions (set the cursor,
|
||||
/// scroll without layout) using a WebRender hit test result.
|
||||
pub hit_test_info: Vec<HitTestInfo>,
|
||||
|
||||
/// A ScrollTree used by the compositor to scroll the contents of the
|
||||
/// display list.
|
||||
pub scroll_tree: ScrollTree,
|
||||
|
@ -856,7 +869,6 @@ impl CompositorDisplayListInfo {
|
|||
viewport_details,
|
||||
content_size,
|
||||
epoch,
|
||||
hit_test_info: Default::default(),
|
||||
scroll_tree,
|
||||
root_reference_frame_id,
|
||||
root_scroll_node_id,
|
||||
|
@ -865,27 +877,12 @@ impl CompositorDisplayListInfo {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add or re-use a duplicate HitTestInfo entry in this `CompositorHitTestInfo`
|
||||
/// and return the index.
|
||||
pub fn add_hit_test_info(
|
||||
&mut self,
|
||||
node: u64,
|
||||
cursor: Option<Cursor>,
|
||||
scroll_tree_node: ScrollTreeNodeId,
|
||||
) -> usize {
|
||||
let hit_test_info = HitTestInfo {
|
||||
node,
|
||||
cursor,
|
||||
scroll_tree_node,
|
||||
};
|
||||
|
||||
if let Some(last) = self.hit_test_info.last() {
|
||||
if hit_test_info == *last {
|
||||
return self.hit_test_info.len() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.hit_test_info.push(hit_test_info);
|
||||
self.hit_test_info.len() - 1
|
||||
pub fn external_scroll_id_for_scroll_tree_node(
|
||||
&self,
|
||||
id: ScrollTreeNodeId,
|
||||
) -> ExternalScrollId {
|
||||
self.scroll_tree
|
||||
.external_scroll_id_for_scroll_tree_node(id)
|
||||
.unwrap_or(ExternalScrollId(0, self.pipeline_id))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,17 +25,17 @@ use std::sync::{Arc, Mutex};
|
|||
|
||||
use bitflags::bitflags;
|
||||
use display_list::CompositorDisplayListInfo;
|
||||
use embedder_traits::{CompositorHitTestResult, ScreenGeometry};
|
||||
use embedder_traits::ScreenGeometry;
|
||||
use euclid::default::Size2D as UntypedSize2D;
|
||||
use ipc_channel::ipc::{self, IpcSharedMemory};
|
||||
use profile_traits::mem::{OpaqueSender, ReportsChan};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use webrender_api::units::{DevicePoint, LayoutVector2D, TexelRect};
|
||||
use webrender_api::units::{LayoutVector2D, TexelRect};
|
||||
use webrender_api::{
|
||||
BuiltDisplayList, BuiltDisplayListDescriptor, ExternalImage, ExternalImageData,
|
||||
ExternalImageHandler, ExternalImageId, ExternalImageSource, ExternalScrollId,
|
||||
FontInstanceFlags, FontInstanceKey, FontKey, HitTestFlags, ImageData, ImageDescriptor,
|
||||
ImageKey, NativeFontHandle, PipelineId as WebRenderPipelineId,
|
||||
FontInstanceFlags, FontInstanceKey, FontKey, ImageData, ImageDescriptor, ImageKey,
|
||||
NativeFontHandle, PipelineId as WebRenderPipelineId,
|
||||
};
|
||||
|
||||
use crate::viewport_description::ViewportDescription;
|
||||
|
@ -110,14 +110,6 @@ pub enum CompositorMsg {
|
|||
/// An [ipc::IpcBytesReceiver] used to send the raw data of the display list.
|
||||
display_list_receiver: ipc::IpcBytesReceiver,
|
||||
},
|
||||
/// Perform a hit test operation. The result will be returned via
|
||||
/// the provided channel sender.
|
||||
HitTest(
|
||||
Option<WebRenderPipelineId>,
|
||||
DevicePoint,
|
||||
HitTestFlags,
|
||||
IpcSender<Vec<CompositorHitTestResult>>,
|
||||
),
|
||||
/// Create a new image key. The result will be returned via the
|
||||
/// provided channel sender.
|
||||
GenerateImageKey(IpcSender<ImageKey>),
|
||||
|
@ -245,21 +237,6 @@ impl CrossProcessCompositorApi {
|
|||
}
|
||||
}
|
||||
|
||||
/// Perform a hit test operation. Blocks until the operation is complete and
|
||||
/// and a result is available.
|
||||
pub fn hit_test(
|
||||
&self,
|
||||
pipeline: Option<WebRenderPipelineId>,
|
||||
point: DevicePoint,
|
||||
flags: HitTestFlags,
|
||||
) -> Vec<CompositorHitTestResult> {
|
||||
let (sender, receiver) = ipc::channel().unwrap();
|
||||
self.0
|
||||
.send(CompositorMsg::HitTest(pipeline, point, flags, sender))
|
||||
.expect("error sending hit test");
|
||||
receiver.recv().expect("error receiving hit test result")
|
||||
}
|
||||
|
||||
/// Create a new image key. Blocks until the key is available.
|
||||
pub fn generate_image_key_blocking(&self) -> Option<ImageKey> {
|
||||
let (sender, receiver) = ipc::channel().unwrap();
|
||||
|
|
|
@ -7,13 +7,12 @@ use std::cell::Cell;
|
|||
use base::id::ScrollTreeNodeId;
|
||||
use compositing_traits::display_list::{
|
||||
AxesScrollSensitivity, ScrollTree, ScrollType, ScrollableNodeInfo, SpatialTreeNodeInfo,
|
||||
StickyNodeInfo,
|
||||
};
|
||||
use euclid::{SideOffsets2D, Size2D};
|
||||
use euclid::Size2D;
|
||||
use webrender_api::units::LayoutVector2D;
|
||||
use webrender_api::{ExternalScrollId, PipelineId, ScrollLocation, StickyOffsetBounds};
|
||||
use webrender_api::{ExternalScrollId, PipelineId, ScrollLocation};
|
||||
|
||||
fn add_mock_scroll_node(tree: &mut ScrollTree) -> ScrollTreeNodeId {
|
||||
fn add_mock_scroll_node(tree: &mut ScrollTree) -> (ScrollTreeNodeId, ExternalScrollId) {
|
||||
let pipeline_id = PipelineId(0, 0);
|
||||
let num_nodes = tree.nodes.len();
|
||||
let parent = if num_nodes > 0 {
|
||||
|
@ -24,10 +23,11 @@ fn add_mock_scroll_node(tree: &mut ScrollTree) -> ScrollTreeNodeId {
|
|||
None
|
||||
};
|
||||
|
||||
tree.add_scroll_tree_node(
|
||||
let external_id = ExternalScrollId(num_nodes as u64, pipeline_id);
|
||||
let scroll_node_id = tree.add_scroll_tree_node(
|
||||
parent.as_ref(),
|
||||
SpatialTreeNodeInfo::Scroll(ScrollableNodeInfo {
|
||||
external_id: ExternalScrollId(num_nodes as u64, pipeline_id),
|
||||
external_id,
|
||||
content_rect: Size2D::new(200.0, 200.0).into(),
|
||||
clip_rect: Size2D::new(100.0, 100.0).into(),
|
||||
scroll_sensitivity: AxesScrollSensitivity {
|
||||
|
@ -37,42 +37,42 @@ fn add_mock_scroll_node(tree: &mut ScrollTree) -> ScrollTreeNodeId {
|
|||
offset: LayoutVector2D::zero(),
|
||||
offset_changed: Cell::new(false),
|
||||
}),
|
||||
)
|
||||
);
|
||||
(scroll_node_id, external_id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scroll_tree_simple_scroll() {
|
||||
let mut scroll_tree = ScrollTree::default();
|
||||
let pipeline_id = PipelineId(0, 0);
|
||||
let id = add_mock_scroll_node(&mut scroll_tree);
|
||||
let (id, external_id) = add_mock_scroll_node(&mut scroll_tree);
|
||||
|
||||
let (scrolled_id, offset) = scroll_tree
|
||||
.scroll_node_or_ancestor(
|
||||
&id,
|
||||
&external_id,
|
||||
ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)),
|
||||
ScrollType::Script,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_offset = LayoutVector2D::new(20.0, 40.0);
|
||||
assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
|
||||
assert_eq!(scrolled_id, external_id);
|
||||
assert_eq!(offset, expected_offset);
|
||||
assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset));
|
||||
|
||||
let (scrolled_id, offset) = scroll_tree
|
||||
.scroll_node_or_ancestor(
|
||||
&id,
|
||||
&external_id,
|
||||
ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)),
|
||||
ScrollType::Script,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_offset = LayoutVector2D::new(0.0, 0.0);
|
||||
assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
|
||||
assert_eq!(scrolled_id, external_id);
|
||||
assert_eq!(offset, expected_offset);
|
||||
assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset));
|
||||
|
||||
// Scroll offsets must be positive.
|
||||
let result = scroll_tree.scroll_node_or_ancestor(
|
||||
&id,
|
||||
&external_id,
|
||||
ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)),
|
||||
ScrollType::Script,
|
||||
);
|
||||
|
@ -88,26 +88,33 @@ fn test_scroll_tree_simple_scroll_chaining() {
|
|||
let mut scroll_tree = ScrollTree::default();
|
||||
|
||||
let pipeline_id = PipelineId(0, 0);
|
||||
let parent_id = add_mock_scroll_node(&mut scroll_tree);
|
||||
let (parent_id, parent_external_id) = add_mock_scroll_node(&mut scroll_tree);
|
||||
|
||||
let unscrollable_external_id = ExternalScrollId(100 as u64, pipeline_id);
|
||||
let unscrollable_child_id = scroll_tree.add_scroll_tree_node(
|
||||
Some(&parent_id),
|
||||
SpatialTreeNodeInfo::Sticky(StickyNodeInfo {
|
||||
frame_rect: Size2D::new(100.0, 100.0).into(),
|
||||
margins: SideOffsets2D::default(),
|
||||
vertical_offset_bounds: StickyOffsetBounds::new(0.0, 0.0),
|
||||
horizontal_offset_bounds: StickyOffsetBounds::new(0.0, 0.0),
|
||||
SpatialTreeNodeInfo::Scroll(ScrollableNodeInfo {
|
||||
external_id: unscrollable_external_id,
|
||||
content_rect: Size2D::new(100.0, 100.0).into(),
|
||||
clip_rect: Size2D::new(100.0, 100.0).into(),
|
||||
scroll_sensitivity: AxesScrollSensitivity {
|
||||
x: ScrollType::Script | ScrollType::InputEvents,
|
||||
y: ScrollType::Script | ScrollType::InputEvents,
|
||||
},
|
||||
offset: LayoutVector2D::zero(),
|
||||
offset_changed: Cell::new(false),
|
||||
}),
|
||||
);
|
||||
|
||||
let (scrolled_id, offset) = scroll_tree
|
||||
.scroll_node_or_ancestor(
|
||||
&unscrollable_child_id,
|
||||
&unscrollable_external_id,
|
||||
ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)),
|
||||
ScrollType::Script,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_offset = LayoutVector2D::new(20.0, 40.0);
|
||||
assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
|
||||
assert_eq!(scrolled_id, parent_external_id);
|
||||
assert_eq!(offset, expected_offset);
|
||||
assert_eq!(
|
||||
scroll_tree.get_node(&parent_id).offset(),
|
||||
|
@ -116,35 +123,37 @@ fn test_scroll_tree_simple_scroll_chaining() {
|
|||
|
||||
let (scrolled_id, offset) = scroll_tree
|
||||
.scroll_node_or_ancestor(
|
||||
&unscrollable_child_id,
|
||||
&unscrollable_external_id,
|
||||
ScrollLocation::Delta(LayoutVector2D::new(10.0, 15.0)),
|
||||
ScrollType::Script,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_offset = LayoutVector2D::new(30.0, 55.0);
|
||||
assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
|
||||
assert_eq!(scrolled_id, parent_external_id);
|
||||
assert_eq!(offset, expected_offset);
|
||||
assert_eq!(
|
||||
scroll_tree.get_node(&parent_id).offset(),
|
||||
Some(expected_offset)
|
||||
);
|
||||
assert_eq!(scroll_tree.get_node(&unscrollable_child_id).offset(), None);
|
||||
assert_eq!(
|
||||
scroll_tree.get_node(&unscrollable_child_id).offset(),
|
||||
Some(LayoutVector2D::zero())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scroll_tree_chain_when_at_extent() {
|
||||
let mut scroll_tree = ScrollTree::default();
|
||||
|
||||
let pipeline_id = PipelineId(0, 0);
|
||||
let parent_id = add_mock_scroll_node(&mut scroll_tree);
|
||||
let child_id = add_mock_scroll_node(&mut scroll_tree);
|
||||
let (parent_id, parent_external_id) = add_mock_scroll_node(&mut scroll_tree);
|
||||
let (child_id, child_external_id) = add_mock_scroll_node(&mut scroll_tree);
|
||||
|
||||
let (scrolled_id, offset) = scroll_tree
|
||||
.scroll_node_or_ancestor(&child_id, ScrollLocation::End, ScrollType::Script)
|
||||
.scroll_node_or_ancestor(&child_external_id, ScrollLocation::End, ScrollType::Script)
|
||||
.unwrap();
|
||||
|
||||
let expected_offset = LayoutVector2D::new(0.0, 100.0);
|
||||
assert_eq!(scrolled_id, ExternalScrollId(1, pipeline_id));
|
||||
assert_eq!(scrolled_id, child_external_id);
|
||||
assert_eq!(offset, expected_offset);
|
||||
assert_eq!(
|
||||
scroll_tree.get_node(&child_id).offset(),
|
||||
|
@ -155,13 +164,13 @@ fn test_scroll_tree_chain_when_at_extent() {
|
|||
// of its scroll area in the y axis.
|
||||
let (scrolled_id, offset) = scroll_tree
|
||||
.scroll_node_or_ancestor(
|
||||
&child_id,
|
||||
&child_external_id,
|
||||
ScrollLocation::Delta(LayoutVector2D::new(0.0, 10.0)),
|
||||
ScrollType::Script,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_offset = LayoutVector2D::new(0.0, 10.0);
|
||||
assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
|
||||
assert_eq!(scrolled_id, parent_external_id);
|
||||
assert_eq!(offset, expected_offset);
|
||||
assert_eq!(
|
||||
scroll_tree.get_node(&parent_id).offset(),
|
||||
|
@ -175,9 +184,8 @@ fn test_scroll_tree_chain_through_overflow_hidden() {
|
|||
|
||||
// Create a tree with a scrollable leaf, but make its `scroll_sensitivity`
|
||||
// reflect `overflow: hidden` ie not responsive to non-script scroll events.
|
||||
let pipeline_id = PipelineId(0, 0);
|
||||
let parent_id = add_mock_scroll_node(&mut scroll_tree);
|
||||
let overflow_hidden_id = add_mock_scroll_node(&mut scroll_tree);
|
||||
let (parent_id, parent_external_id) = add_mock_scroll_node(&mut scroll_tree);
|
||||
let (overflow_hidden_id, overflow_hidden_external_id) = add_mock_scroll_node(&mut scroll_tree);
|
||||
let node = scroll_tree.get_node_mut(&overflow_hidden_id);
|
||||
|
||||
if let SpatialTreeNodeInfo::Scroll(ref mut scroll_node_info) = node.info {
|
||||
|
@ -189,13 +197,13 @@ fn test_scroll_tree_chain_through_overflow_hidden() {
|
|||
|
||||
let (scrolled_id, offset) = scroll_tree
|
||||
.scroll_node_or_ancestor(
|
||||
&overflow_hidden_id,
|
||||
&overflow_hidden_external_id,
|
||||
ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)),
|
||||
ScrollType::InputEvents,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_offset = LayoutVector2D::new(20.0, 40.0);
|
||||
assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
|
||||
assert_eq!(scrolled_id, parent_external_id);
|
||||
assert_eq!(offset, expected_offset);
|
||||
assert_eq!(
|
||||
scroll_tree.get_node(&parent_id).offset(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue