mirror of
https://github.com/servo/servo.git
synced 2025-09-27 15:20:09 +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
|
@ -31,6 +31,7 @@ gleam = { workspace = true }
|
|||
ipc-channel = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
pixels = { path = "../pixels" }
|
||||
profile_traits = { workspace = true }
|
||||
servo_allocator = { path = "../allocator" }
|
||||
|
|
|
@ -11,13 +11,11 @@ use std::rc::Rc;
|
|||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use base::Epoch;
|
||||
use base::cross_process_instant::CrossProcessInstant;
|
||||
use base::id::{PipelineId, WebViewId};
|
||||
use base::{Epoch, WebRenderEpochToU16};
|
||||
use bitflags::bitflags;
|
||||
use compositing_traits::display_list::{
|
||||
CompositorDisplayListInfo, HitTestInfo, ScrollTree, ScrollType,
|
||||
};
|
||||
use compositing_traits::display_list::{CompositorDisplayListInfo, ScrollTree, ScrollType};
|
||||
use compositing_traits::rendering_context::RenderingContext;
|
||||
use compositing_traits::{
|
||||
CompositionPipeline, CompositorMsg, ImageUpdate, PipelineExitSource, SendableFrameTree,
|
||||
|
@ -27,13 +25,12 @@ use constellation_traits::{EmbedderToConstellationMessage, PaintMetricEvent};
|
|||
use crossbeam_channel::{Receiver, Sender};
|
||||
use dpi::PhysicalSize;
|
||||
use embedder_traits::{
|
||||
CompositorHitTestResult, Cursor, InputEvent, ShutdownState, UntrustedNodeAddress,
|
||||
ViewportDetails,
|
||||
CompositorHitTestResult, Cursor, InputEvent, ShutdownState, ViewportDetails,
|
||||
};
|
||||
use euclid::{Point2D, Rect, Scale, Size2D, Transform3D};
|
||||
use ipc_channel::ipc::{self, IpcSharedMemory};
|
||||
use libc::c_void;
|
||||
use log::{debug, info, trace, warn};
|
||||
use num_traits::cast::FromPrimitive;
|
||||
use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage};
|
||||
use profile_traits::mem::{ProcessReports, ProfilerRegistration, Report, ReportKind};
|
||||
use profile_traits::time::{self as profile_time, ProfilerCategory};
|
||||
|
@ -48,10 +45,10 @@ use webrender_api::units::{
|
|||
};
|
||||
use webrender_api::{
|
||||
self, BuiltDisplayList, DirtyRect, DisplayListPayload, DocumentId, Epoch as WebRenderEpoch,
|
||||
FontInstanceFlags, FontInstanceKey, FontInstanceOptions, FontKey, HitTestFlags,
|
||||
PipelineId as WebRenderPipelineId, PropertyBinding, ReferenceFrameKind, RenderReasons,
|
||||
SampledScrollOffset, ScrollLocation, SpaceAndClipInfo, SpatialId, SpatialTreeItemKey,
|
||||
TransformStyle,
|
||||
ExternalScrollId, FontInstanceFlags, FontInstanceKey, FontInstanceOptions, FontKey,
|
||||
HitTestFlags, PipelineId as WebRenderPipelineId, PropertyBinding, ReferenceFrameKind,
|
||||
RenderReasons, SampledScrollOffset, ScrollLocation, SpaceAndClipInfo, SpatialId,
|
||||
SpatialTreeItemKey, TransformStyle,
|
||||
};
|
||||
|
||||
use crate::InitialCompositorState;
|
||||
|
@ -200,10 +197,6 @@ pub(crate) struct PipelineDetails {
|
|||
/// The id of the parent pipeline, if any.
|
||||
pub parent_pipeline_id: Option<PipelineId>,
|
||||
|
||||
/// The epoch of the most recent display list for this pipeline. Note that this display
|
||||
/// list might not be displayed, as WebRender processes display lists asynchronously.
|
||||
pub most_recent_display_list_epoch: Option<WebRenderEpoch>,
|
||||
|
||||
/// Whether animations are running
|
||||
pub animations_running: bool,
|
||||
|
||||
|
@ -213,10 +206,6 @@ pub(crate) struct PipelineDetails {
|
|||
/// Whether to use less resources by stopping animations.
|
||||
pub throttled: bool,
|
||||
|
||||
/// Hit test items for this pipeline. This is used to map WebRender hit test
|
||||
/// information to the full information necessary for Servo.
|
||||
pub hit_test_items: Vec<HitTestInfo>,
|
||||
|
||||
/// The compositor-side [ScrollTree]. This is used to allow finding and scrolling
|
||||
/// nodes in the compositor before forwarding new offsets to WebRender.
|
||||
pub scroll_tree: ScrollTree,
|
||||
|
@ -252,12 +241,10 @@ impl PipelineDetails {
|
|||
PipelineDetails {
|
||||
pipeline: None,
|
||||
parent_pipeline_id: None,
|
||||
most_recent_display_list_epoch: None,
|
||||
viewport_scale: None,
|
||||
animations_running: false,
|
||||
animation_callbacks_running: false,
|
||||
throttled: false,
|
||||
hit_test_items: Vec::new(),
|
||||
scroll_tree: ScrollTree::default(),
|
||||
first_paint_metric: PaintMetricState::Waiting,
|
||||
first_contentful_paint_metric: PaintMetricState::Waiting,
|
||||
|
@ -272,11 +259,6 @@ impl PipelineDetails {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum HitTestError {
|
||||
EpochMismatch,
|
||||
Others,
|
||||
}
|
||||
|
||||
impl ServoRenderer {
|
||||
pub fn shutdown_state(&self) -> ShutdownState {
|
||||
self.shutdown_state.get()
|
||||
|
@ -286,57 +268,33 @@ impl ServoRenderer {
|
|||
&self,
|
||||
point: DevicePoint,
|
||||
details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>,
|
||||
) -> Result<CompositorHitTestResult, HitTestError> {
|
||||
match self.hit_test_at_point_with_flags_and_pipeline(
|
||||
point,
|
||||
HitTestFlags::empty(),
|
||||
None,
|
||||
details_for_pipeline,
|
||||
) {
|
||||
Ok(hit_test_results) => hit_test_results
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or(HitTestError::Others),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
) -> Vec<CompositorHitTestResult> {
|
||||
self.hit_test_at_point_with_flags(point, HitTestFlags::empty(), details_for_pipeline)
|
||||
}
|
||||
|
||||
// TODO: split this into first half (global) and second half (one for whole compositor, one for webview)
|
||||
pub(crate) fn hit_test_at_point_with_flags_and_pipeline<'a>(
|
||||
pub(crate) fn hit_test_at_point_with_flags<'a>(
|
||||
&self,
|
||||
point: DevicePoint,
|
||||
flags: HitTestFlags,
|
||||
pipeline_id: Option<WebRenderPipelineId>,
|
||||
details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>,
|
||||
) -> Result<Vec<CompositorHitTestResult>, HitTestError> {
|
||||
) -> Vec<CompositorHitTestResult> {
|
||||
// DevicePoint and WorldPoint are the same for us.
|
||||
let world_point = WorldPoint::from_untyped(point.to_untyped());
|
||||
let results =
|
||||
self.webrender_api
|
||||
.hit_test(self.webrender_document, pipeline_id, world_point, flags);
|
||||
let results = self.webrender_api.hit_test(
|
||||
self.webrender_document,
|
||||
None, /* pipeline_id */
|
||||
world_point,
|
||||
flags,
|
||||
);
|
||||
|
||||
let mut epoch_mismatch = false;
|
||||
let results = results
|
||||
results
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let pipeline_id = item.pipeline.into();
|
||||
let details = details_for_pipeline(pipeline_id)?;
|
||||
|
||||
// If the epoch in the tag does not match the current epoch of the pipeline,
|
||||
// then the hit test is against an old version of the display list.
|
||||
match details.most_recent_display_list_epoch {
|
||||
Some(epoch) => {
|
||||
if epoch.as_u16() != item.tag.1 {
|
||||
// It's too early to hit test for now.
|
||||
// New scene building is in progress.
|
||||
epoch_mismatch = true;
|
||||
return None;
|
||||
}
|
||||
},
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
let offset = details
|
||||
.scroll_tree
|
||||
.scroll_offset(pipeline_id.root_scroll_id())
|
||||
|
@ -344,28 +302,20 @@ impl ServoRenderer {
|
|||
let point_in_initial_containing_block =
|
||||
(item.point_in_viewport + offset).to_untyped();
|
||||
|
||||
let info = &details.hit_test_items[item.tag.0 as usize];
|
||||
let external_scroll_id = ExternalScrollId(item.tag.0, item.pipeline);
|
||||
let cursor = Cursor::from_u16(item.tag.1);
|
||||
|
||||
Some(CompositorHitTestResult {
|
||||
pipeline_id,
|
||||
point_in_viewport: Point2D::from_untyped(item.point_in_viewport.to_untyped()),
|
||||
point_relative_to_initial_containing_block: Point2D::from_untyped(
|
||||
point_in_initial_containing_block,
|
||||
),
|
||||
point_relative_to_item: Point2D::from_untyped(
|
||||
item.point_relative_to_item.to_untyped(),
|
||||
),
|
||||
node: UntrustedNodeAddress(info.node as *const c_void),
|
||||
cursor: info.cursor,
|
||||
scroll_tree_node: info.scroll_tree_node,
|
||||
cursor,
|
||||
external_scroll_id,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if epoch_mismatch {
|
||||
return Err(HitTestError::EpochMismatch);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn send_transaction(&mut self, transaction: Transaction) {
|
||||
|
@ -643,10 +593,11 @@ impl IOCompositor {
|
|||
.global
|
||||
.borrow()
|
||||
.hit_test_at_point(point, details_for_pipeline);
|
||||
if let Ok(result) = result {
|
||||
|
||||
if let Some(result) = result.first() {
|
||||
self.global
|
||||
.borrow_mut()
|
||||
.update_cursor_from_hittest(point, &result);
|
||||
.update_cursor_from_hittest(point, result);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -768,14 +719,10 @@ impl IOCompositor {
|
|||
return warn!("Could not find WebView for incoming display list");
|
||||
};
|
||||
|
||||
// WebRender is not ready until we receive "NewWebRenderFrameReady"
|
||||
webview_renderer.webrender_frame_ready.set(false);
|
||||
let old_scale = webview_renderer.device_pixels_per_page_pixel();
|
||||
|
||||
let pipeline_id = display_list_info.pipeline_id;
|
||||
let details = webview_renderer.ensure_pipeline_details(pipeline_id.into());
|
||||
details.most_recent_display_list_epoch = Some(display_list_info.epoch);
|
||||
details.hit_test_items = display_list_info.hit_test_info;
|
||||
details.install_new_scroll_tree(display_list_info.scroll_tree);
|
||||
details.viewport_scale =
|
||||
Some(display_list_info.viewport_details.hidpi_scale_factor);
|
||||
|
@ -808,33 +755,6 @@ impl IOCompositor {
|
|||
self.global.borrow_mut().send_transaction(transaction);
|
||||
},
|
||||
|
||||
CompositorMsg::HitTest(pipeline, point, flags, sender) => {
|
||||
// When a display list is sent to WebRender, it starts scene building in a
|
||||
// separate thread and then that display list is available for hit testing.
|
||||
// Without flushing scene building, any hit test we do might be done against
|
||||
// a previous scene, if the last one we sent hasn't finished building.
|
||||
//
|
||||
// TODO(mrobinson): Flushing all scene building is a big hammer here, because
|
||||
// we might only be interested in a single pipeline. The only other option
|
||||
// would be to listen to the TransactionNotifier for previous per-pipeline
|
||||
// transactions, but that isn't easily compatible with the event loop wakeup
|
||||
// mechanism from libserver.
|
||||
self.global.borrow().webrender_api.flush_scene_builder();
|
||||
|
||||
let details_for_pipeline = |pipeline_id| self.details_for_pipeline(pipeline_id);
|
||||
let result = self
|
||||
.global
|
||||
.borrow()
|
||||
.hit_test_at_point_with_flags_and_pipeline(
|
||||
point,
|
||||
flags,
|
||||
pipeline,
|
||||
details_for_pipeline,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
let _ = sender.send(result);
|
||||
},
|
||||
|
||||
CompositorMsg::GenerateImageKey(sender) => {
|
||||
let _ = sender.send(self.global.borrow().webrender_api.generate_image_key());
|
||||
},
|
||||
|
@ -1566,15 +1486,6 @@ impl IOCompositor {
|
|||
},
|
||||
CompositorMsg::NewWebRenderFrameReady(..) => {
|
||||
found_recomposite_msg = true;
|
||||
|
||||
// Process all pending events
|
||||
// FIXME: Shouldn't `webview_frame_ready` be stored globally and why can't `pending_frames`
|
||||
// be used here?
|
||||
self.webview_renderers.iter().for_each(|webview| {
|
||||
webview.dispatch_pending_point_input_events();
|
||||
webview.webrender_frame_ready.set(true);
|
||||
});
|
||||
|
||||
true
|
||||
},
|
||||
_ => true,
|
||||
|
|
|
@ -42,7 +42,6 @@ mod from_constellation {
|
|||
Self::SendInitialTransaction(..) => target!("SendInitialTransaction"),
|
||||
Self::SendScrollNode(..) => target!("SendScrollNode"),
|
||||
Self::SendDisplayList { .. } => target!("SendDisplayList"),
|
||||
Self::HitTest(..) => target!("HitTest"),
|
||||
Self::GenerateImageKey(..) => target!("GenerateImageKey"),
|
||||
Self::UpdateImages(..) => target!("UpdateImages"),
|
||||
Self::GenerateFontKeys(..) => target!("GenerateFontKeys"),
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::{Entry, Keys};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::rc::Rc;
|
||||
|
||||
use base::id::{PipelineId, WebViewId};
|
||||
|
@ -27,7 +27,7 @@ use style_traits::{CSSPixel, PinchZoomFactor};
|
|||
use webrender_api::units::{DeviceIntPoint, DevicePixel, DevicePoint, DeviceRect, LayoutVector2D};
|
||||
use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation};
|
||||
|
||||
use crate::compositor::{HitTestError, PipelineDetails, ServoRenderer};
|
||||
use crate::compositor::{PipelineDetails, ServoRenderer};
|
||||
use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -96,10 +96,6 @@ pub(crate) struct WebViewRenderer {
|
|||
/// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with
|
||||
/// active animations or animation frame callbacks.
|
||||
animating: bool,
|
||||
/// Pending input events queue. Priavte and only this thread pushes events to it.
|
||||
pending_point_input_events: RefCell<VecDeque<InputEvent>>,
|
||||
/// WebRender is not ready between `SendDisplayList` and `WebRenderFrameReady` messages.
|
||||
pub webrender_frame_ready: Cell<bool>,
|
||||
/// A [`ViewportDescription`] for this [`WebViewRenderer`], which contains the limitations
|
||||
/// and initial values for zoom derived from the `viewport` meta tag in web content.
|
||||
viewport_description: Option<ViewportDescription>,
|
||||
|
@ -135,8 +131,6 @@ impl WebViewRenderer {
|
|||
pinch_zoom: PinchZoomFactor::new(1.0),
|
||||
hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
|
||||
animating: false,
|
||||
pending_point_input_events: Default::default(),
|
||||
webrender_frame_ready: Cell::default(),
|
||||
viewport_description: None,
|
||||
}
|
||||
}
|
||||
|
@ -335,55 +329,24 @@ impl WebViewRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_point_input_event(&self, event: InputEvent) -> bool {
|
||||
self.dispatch_point_input_event_internal(event, true)
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_pending_point_input_events(&self) {
|
||||
while let Some(event) = self.pending_point_input_events.borrow_mut().pop_front() {
|
||||
// TODO: Add multiple retry later if needed.
|
||||
self.dispatch_point_input_event_internal(event, false);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_point_input_event_internal(
|
||||
&self,
|
||||
mut event: InputEvent,
|
||||
retry_on_error: bool,
|
||||
) -> bool {
|
||||
pub(crate) fn dispatch_point_input_event(&self, mut event: InputEvent) -> bool {
|
||||
// Events that do not need to do hit testing are sent directly to the
|
||||
// constellation to filter down.
|
||||
let Some(point) = event.point() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Delay the event if the epoch is not synchronized yet (new frame is not ready),
|
||||
// or hit test result would fail and the event is rejected anyway.
|
||||
if retry_on_error &&
|
||||
(!self.webrender_frame_ready.get() ||
|
||||
!self.pending_point_input_events.borrow().is_empty())
|
||||
{
|
||||
self.pending_point_input_events
|
||||
.borrow_mut()
|
||||
.push_back(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we can't find a pipeline to send this event to, we cannot continue.
|
||||
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
|
||||
let result = match self
|
||||
let Some(result) = self
|
||||
.global
|
||||
.borrow()
|
||||
.hit_test_at_point(point, get_pipeline_details)
|
||||
{
|
||||
Ok(hit_test_results) => Some(hit_test_results),
|
||||
Err(HitTestError::EpochMismatch) if retry_on_error => {
|
||||
self.pending_point_input_events
|
||||
.borrow_mut()
|
||||
.push_back(event.clone());
|
||||
return false;
|
||||
},
|
||||
_ => None,
|
||||
.into_iter()
|
||||
.nth(0)
|
||||
else {
|
||||
warn!("Empty hit test result for input event, ignoring.");
|
||||
return false;
|
||||
};
|
||||
|
||||
match event {
|
||||
|
@ -394,19 +357,15 @@ impl WebViewRenderer {
|
|||
InputEvent::MouseLeave(_) |
|
||||
InputEvent::MouseMove(_) |
|
||||
InputEvent::Wheel(_) => {
|
||||
if let Some(ref result) = result {
|
||||
self.global
|
||||
.borrow_mut()
|
||||
.update_cursor_from_hittest(point, result);
|
||||
} else {
|
||||
warn!("Not hit test result.");
|
||||
}
|
||||
self.global
|
||||
.borrow_mut()
|
||||
.update_cursor_from_hittest(point, &result);
|
||||
},
|
||||
_ => unreachable!("Unexpected input event type: {event:?}"),
|
||||
}
|
||||
|
||||
if let Err(error) = self.global.borrow().constellation_sender.send(
|
||||
EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, result),
|
||||
EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)),
|
||||
) {
|
||||
warn!("Sending event to constellation failed ({error:?}).");
|
||||
false
|
||||
|
@ -895,16 +854,11 @@ impl WebViewRenderer {
|
|||
};
|
||||
|
||||
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
|
||||
let hit_test_results = self
|
||||
.global
|
||||
.borrow()
|
||||
.hit_test_at_point_with_flags_and_pipeline(
|
||||
cursor,
|
||||
HitTestFlags::FIND_ALL,
|
||||
None,
|
||||
get_pipeline_details,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
let hit_test_results = self.global.borrow().hit_test_at_point_with_flags(
|
||||
cursor,
|
||||
HitTestFlags::FIND_ALL,
|
||||
get_pipeline_details,
|
||||
);
|
||||
|
||||
// Iterate through all hit test results, processing only the first node of each pipeline.
|
||||
// This is needed to propagate the scroll events from a pipeline representing an iframe to
|
||||
|
@ -916,7 +870,7 @@ impl WebViewRenderer {
|
|||
Some(&hit_test_result.pipeline_id)
|
||||
{
|
||||
let scroll_result = pipeline_details.scroll_tree.scroll_node_or_ancestor(
|
||||
&hit_test_result.scroll_tree_node,
|
||||
&hit_test_result.external_scroll_id,
|
||||
scroll_location,
|
||||
ScrollType::InputEvents,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue