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:
Martin Robinson 2025-08-07 10:38:43 +02:00 committed by GitHub
parent c0cc8484f8
commit ad805e3110
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 348 additions and 511 deletions

2
Cargo.lock generated
View file

@ -1390,6 +1390,7 @@ dependencies = [
"ipc-channel",
"libc",
"log",
"num-traits",
"pixels",
"profile_traits",
"servo-tracing",
@ -4789,6 +4790,7 @@ dependencies = [
"servo_malloc_size_of",
"servo_url",
"stylo",
"stylo_traits",
"webrender_api",
]

View file

@ -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" }

View file

@ -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,

View file

@ -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"),

View file

@ -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,
);

View file

@ -6,12 +6,13 @@ use std::collections::HashMap;
use app_units::Au;
use base::id::ScrollTreeNodeId;
use euclid::{Box2D, Point2D, Point3D};
use euclid::{Box2D, Point2D, Point3D, Vector2D};
use kurbo::{Ellipse, Shape};
use layout_api::{ElementsFromPointFlags, ElementsFromPointResult};
use style::computed_values::backface_visibility::T as BackfaceVisibility;
use style::computed_values::pointer_events::T as PointerEvents;
use style::computed_values::visibility::T as Visibility;
use style::properties::ComputedValues;
use webrender_api::BorderRadius;
use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSize, LayoutTransform, RectExt};
@ -20,7 +21,7 @@ use crate::display_list::stacking_context::StackingContextSection;
use crate::display_list::{
StackingContext, StackingContextContent, StackingContextTree, ToWebRender,
};
use crate::fragment_tree::{BoxFragment, Fragment};
use crate::fragment_tree::Fragment;
use crate::geom::PhysicalRect;
pub(crate) struct HitTest<'a> {
@ -125,7 +126,7 @@ impl<'a> HitTest<'a> {
impl Clip {
fn contains(&self, point: LayoutPoint) -> bool {
rounded_rect_contains_point(self.rect, || self.radii, point)
rounded_rect_contains_point(self.rect, &self.radii, point)
}
}
@ -245,22 +246,10 @@ impl Fragment {
return false;
};
let point_in_target;
let transform;
let hit = match self {
Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => {
(point_in_target, transform) =
match hit_test.location_in_spatial_node(spatial_node_id) {
Some(point) => point,
None => return false,
};
box_fragment
.borrow()
.hit_test(point_in_target, containing_block, &transform)
},
Fragment::Text(text) => {
let text = &*text.borrow();
let style = text.inline_styles.style.borrow();
let mut hit_test_fragment_inner =
|style: &ComputedValues,
fragment_rect: PhysicalRect<Au>,
border_radius: BorderRadius| {
if style.get_inherited_ui().pointer_events == PointerEvents::None {
return false;
}
@ -268,7 +257,7 @@ impl Fragment {
return false;
}
(point_in_target, transform) =
let (point_in_spatial_node, transform) =
match hit_test.location_in_spatial_node(spatial_node_id) {
Some(point) => point,
None => return false,
@ -280,68 +269,59 @@ impl Fragment {
return false;
}
text.rect
.translate(containing_block.origin.to_vector())
.to_webrender()
.contains(point_in_target)
let fragment_rect = fragment_rect.translate(containing_block.origin.to_vector());
if !rounded_rect_contains_point(
fragment_rect.to_webrender(),
&border_radius,
point_in_spatial_node,
) {
return false;
}
let point_in_target = point_in_spatial_node.cast_unit() -
Vector2D::new(
fragment_rect.origin.x.to_f32_px(),
fragment_rect.origin.y.to_f32_px(),
);
hit_test.results.push(ElementsFromPointResult {
node: tag.node,
point_in_target,
});
!hit_test.flags.contains(ElementsFromPointFlags::FindAll)
};
match self {
Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => {
let box_fragment = box_fragment.borrow();
hit_test_fragment_inner(
&box_fragment.style,
box_fragment.border_rect(),
box_fragment.border_radius(),
)
},
Fragment::AbsoluteOrFixedPositioned(_) |
Fragment::IFrame(_) |
Fragment::Image(_) |
Fragment::Positioning(_) => return false,
};
if !hit {
return false;
Fragment::Text(text) => {
let text = &*text.borrow();
hit_test_fragment_inner(
&text.inline_styles.style.borrow(),
text.rect,
BorderRadius::zero(),
)
},
_ => false,
}
hit_test.results.push(ElementsFromPointResult {
node: tag.node,
point_in_target,
});
!hit_test.flags.contains(ElementsFromPointFlags::FindAll)
}
}
impl BoxFragment {
fn hit_test(
&self,
point_in_fragment: LayoutPoint,
containing_block: &PhysicalRect<Au>,
transform: &LayoutTransform,
) -> bool {
if self.style.get_inherited_ui().pointer_events == PointerEvents::None {
return false;
}
if self.style.get_inherited_box().visibility != Visibility::Visible {
return false;
}
if self.style.get_box().backface_visibility == BackfaceVisibility::Hidden &&
transform.is_backface_visible()
{
return false;
}
let border_rect = self
.border_rect()
.translate(containing_block.origin.to_vector())
.to_webrender();
rounded_rect_contains_point(border_rect, || self.border_radius(), point_in_fragment)
}
}
fn rounded_rect_contains_point(
rect: LayoutRect,
border_radius: impl FnOnce() -> BorderRadius,
border_radius: &BorderRadius,
point: LayoutPoint,
) -> bool {
if !rect.contains(point) {
return false;
}
let border_radius = border_radius();
if border_radius.is_zero() {
return true;
}

View file

@ -6,7 +6,6 @@ use std::cell::{OnceCell, RefCell};
use std::sync::Arc;
use app_units::{AU_PER_PX, Au};
use base::WebRenderEpochToU16;
use base::id::ScrollTreeNodeId;
use clip::{Clip, ClipId};
use compositing_traits::display_list::{CompositorDisplayListInfo, SpatialTreeNodeInfo};
@ -72,9 +71,6 @@ use background::BackgroundPainter;
pub(crate) use hit_test::HitTest;
pub(crate) use stacking_context::*;
// webrender's `ItemTag` is private.
type ItemTag = (u64, u16);
type HitInfo = Option<ItemTag>;
const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(AU_PER_PX);
pub(crate) struct DisplayListBuilder<'a> {
@ -168,8 +164,6 @@ impl DisplayListBuilder<'_> {
) -> BuiltDisplayList {
// Build the rest of the display list which inclues all of the WebRender primitives.
let compositor_info = &mut stacking_context_tree.compositor_info;
compositor_info.hit_test_info.clear();
let mut webrender_display_list_builder =
webrender_api::DisplayListBuilder::new(compositor_info.pipeline_id);
webrender_display_list_builder.begin();
@ -396,27 +390,6 @@ impl DisplayListBuilder<'_> {
}
}
fn hit_info(
&mut self,
style: &ComputedValues,
tag: Option<Tag>,
auto_cursor: Cursor,
) -> HitInfo {
use style::computed_values::pointer_events::T as PointerEvents;
let inherited_ui = style.get_inherited_ui();
if inherited_ui.pointer_events == PointerEvents::None {
return None;
}
let hit_test_index = self.compositor_info.add_hit_test_info(
tag?.node.0 as u64,
Some(cursor(inherited_ui.cursor.keyword, auto_cursor)),
self.current_scroll_node_id,
);
Some((hit_test_index as u64, self.compositor_info.epoch.as_u16()))
}
/// Draw highlights around the node that is currently hovered in the devtools.
fn paint_dom_inspector_highlight(&mut self) {
let Some(highlight) = self
@ -616,7 +589,6 @@ impl Fragment {
self.maybe_push_hit_test_for_style_and_tag(
builder,
&positioning_fragment.style,
positioning_fragment.base.tag,
rect,
Cursor::Default,
);
@ -706,24 +678,20 @@ impl Fragment {
&self,
builder: &mut DisplayListBuilder,
style: &ComputedValues,
tag: Option<Tag>,
rect: PhysicalRect<Au>,
cursor: Cursor,
) {
let hit_info = builder.hit_info(style, tag, cursor);
let hit_info = match hit_info {
Some(hit_info) => hit_info,
None => return,
};
let clip_chain_id = builder.clip_chain_id(builder.current_clip_id);
let spatial_id = builder.spatial_id(builder.current_scroll_node_id);
let external_scroll_id = builder
.compositor_info
.external_scroll_id_for_scroll_tree_node(builder.current_scroll_node_id);
builder.wr().push_hit_test(
rect.to_webrender(),
clip_chain_id,
spatial_id,
style.get_webrender_primitive_flags(),
hit_info,
(external_scroll_id.0, cursor as u16), /* tag */
);
}
@ -756,13 +724,7 @@ impl Fragment {
}
let parent_style = fragment.inline_styles.style.borrow();
self.maybe_push_hit_test_for_style_and_tag(
builder,
&parent_style,
fragment.base.tag,
rect,
Cursor::Text,
);
self.maybe_push_hit_test_for_style_and_tag(builder, &parent_style, rect, Cursor::Text);
let color = parent_style.clone_color();
let font_metrics = &fragment.font_metrics;
@ -1116,15 +1078,13 @@ impl<'a> BuilderForBoxFragment<'a> {
}
fn build_hit_test(&self, builder: &mut DisplayListBuilder, rect: LayoutRect) {
let hit_info = builder.hit_info(
&self.fragment.style,
self.fragment.base.tag,
let cursor = cursor(
self.fragment.style.get_inherited_ui().cursor.keyword,
Cursor::Default,
);
let hit_info = match hit_info {
Some(hit_info) => hit_info,
None => return,
};
let external_scroll_node_id = builder
.compositor_info
.external_scroll_id_for_scroll_tree_node(builder.current_scroll_node_id);
let mut common = builder.common_properties(rect, &self.fragment.style);
if let Some(clip_chain_id) = self.border_edge_clip(builder, false) {
@ -1135,7 +1095,7 @@ impl<'a> BuilderForBoxFragment<'a> {
common.clip_chain_id,
common.spatial_id,
common.flags,
hit_info,
(external_scroll_node_id.0, cursor as u16), /* tag */
);
}
@ -1902,17 +1862,25 @@ pub(super) fn compute_margin_box_radius(
impl BoxFragment {
fn border_radius(&self) -> BorderRadius {
let resolve =
|radius: &LengthPercentage, box_size: Au| radius.to_used_value(box_size).to_f32_px();
let border = self.style.get_border();
if border.border_top_left_radius.0.is_zero() &&
border.border_top_right_radius.0.is_zero() &&
border.border_bottom_right_radius.0.is_zero() &&
border.border_bottom_left_radius.0.is_zero()
{
return BorderRadius::zero();
}
let border_rect = self.border_rect();
let resolve =
|radius: &LengthPercentage, box_size: Au| radius.to_used_value(box_size).to_f32_px();
let corner = |corner: &style::values::computed::BorderCornerRadius| {
Size2D::new(
resolve(&corner.0.width.0, border_rect.size.width),
resolve(&corner.0.height.0, border_rect.size.height),
)
};
let border = self.style.get_border();
let mut radius = wr::BorderRadius {
top_left: corner(&border.border_top_left_radius),
top_right: corner(&border.border_top_right_radius),

View file

@ -28,10 +28,10 @@ use data_url::mime::Mime;
use devtools_traits::ScriptToDevtoolsControlMsg;
use dom_struct::dom_struct;
use embedder_traits::{
AllowOrDeny, AnimationState, CompositorHitTestResult, ContextMenuResult, EditingActionEvent,
EmbedderMsg, FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton,
MouseButtonAction, MouseButtonEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId,
UntrustedNodeAddress, WheelEvent,
AllowOrDeny, AnimationState, ContextMenuResult, EditingActionEvent, EmbedderMsg,
FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction,
MouseButtonEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId, UntrustedNodeAddress,
WheelEvent,
};
use encoding_rs::{Encoding, UTF_8};
use euclid::Point2D;
@ -172,6 +172,7 @@ use crate::dom::htmlinputelement::HTMLInputElement;
use crate::dom::htmlscriptelement::{HTMLScriptElement, ScriptResult};
use crate::dom::htmltextareaelement::HTMLTextAreaElement;
use crate::dom::htmltitleelement::HTMLTitleElement;
use crate::dom::inputevent::HitTestResult;
use crate::dom::intersectionobserver::IntersectionObserver;
use crate::dom::keyboardevent::KeyboardEvent;
use crate::dom::location::{Location, NavigationType};
@ -1542,7 +1543,6 @@ impl Document {
}
}
#[allow(unsafe_code)]
pub(crate) fn handle_mouse_button_event(
&self,
event: MouseButtonEvent,
@ -1550,17 +1550,17 @@ impl Document {
can_gc: CanGc,
) {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = &input_event.hit_test_result else {
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
return;
};
debug!(
"{:?}: at {:?}",
event.action, hit_test_result.point_in_viewport
event.action, hit_test_result.point_in_frame
);
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) };
let Some(el) = node
let Some(el) = hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::Yes)
.filter_map(DomRoot::downcast::<Element>)
.next()
@ -1591,7 +1591,7 @@ impl Document {
event,
input_event.pressed_mouse_buttons,
&self.window,
hit_test_result,
&hit_test_result,
input_event.active_keyboard_modifiers,
can_gc,
));
@ -1626,13 +1626,13 @@ impl Document {
if self.focus_transaction.borrow().is_some() {
self.commit_focus_transaction(FocusInitiator::Local, can_gc);
}
self.maybe_fire_dblclick(node, hit_test_result, input_event, can_gc);
self.maybe_fire_dblclick(node, &hit_test_result, input_event, can_gc);
}
// When the contextmenu event is triggered by right mouse button
// the contextmenu event MUST be dispatched after the mousedown event.
if let (MouseButtonAction::Down, MouseButton::Right) = (event.action, event.button) {
self.maybe_show_context_menu(node.upcast(), hit_test_result, input_event, can_gc);
self.maybe_show_context_menu(node.upcast(), &hit_test_result, input_event, can_gc);
}
}
@ -1640,7 +1640,7 @@ impl Document {
fn maybe_show_context_menu(
&self,
target: &EventTarget,
hit_test_result: &CompositorHitTestResult,
hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
@ -1652,8 +1652,8 @@ impl Document {
EventCancelable::Cancelable, // cancelable
Some(&self.window), // view
0, // detail
hit_test_result.point_in_viewport.to_i32(),
hit_test_result.point_in_viewport.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result
.point_relative_to_initial_containing_block
.to_i32(),
@ -1698,13 +1698,13 @@ impl Document {
fn maybe_fire_dblclick(
&self,
target: &Node,
hit_test_result: &CompositorHitTestResult,
hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
// https://w3c.github.io/uievents/#event-type-dblclick
let now = Instant::now();
let point_in_viewport = hit_test_result.point_in_viewport;
let point_in_frame = hit_test_result.point_in_frame;
let opt = self.last_click_info.borrow_mut().take();
if let Some((last_time, last_pos)) = opt {
@ -1713,7 +1713,7 @@ impl Document {
let DBL_CLICK_DIST_THRESHOLD = pref!(dom_document_dblclick_dist) as u64;
// Calculate distance between this click and the previous click.
let line = point_in_viewport - last_pos;
let line = point_in_frame - last_pos;
let dist = (line.dot(line) as f64).sqrt();
if now.duration_since(last_time) < DBL_CLICK_TIMEOUT &&
@ -1729,8 +1729,8 @@ impl Document {
EventCancelable::Cancelable,
Some(&self.window),
click_count,
point_in_viewport.to_i32(),
point_in_viewport.to_i32(),
point_in_frame.to_i32(),
point_in_frame.to_i32(),
hit_test_result
.point_relative_to_initial_containing_block
.to_i32(),
@ -1750,7 +1750,7 @@ impl Document {
}
// Update last_click_info with the time and position of the click.
*self.last_click_info.borrow_mut() = Some((now, point_in_viewport));
*self.last_click_info.borrow_mut() = Some((now, point_in_frame));
}
#[allow(clippy::too_many_arguments)]
@ -1760,7 +1760,7 @@ impl Document {
event_name: FireMouseEventType,
can_bubble: EventBubbles,
cancelable: EventCancelable,
hit_test_result: &CompositorHitTestResult,
hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
@ -1771,8 +1771,8 @@ impl Document {
cancelable,
Some(&self.window),
0i32,
hit_test_result.point_in_viewport.to_i32(),
hit_test_result.point_in_viewport.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result
.point_relative_to_initial_containing_block
.to_i32(),
@ -2004,12 +2004,12 @@ impl Document {
can_gc: CanGc,
) {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = &input_event.hit_test_result else {
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
return;
};
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) };
let Some(new_target) = node
let Some(new_target) = hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
@ -2049,7 +2049,7 @@ impl Document {
FireMouseEventType::Out,
EventBubbles::Bubbles,
EventCancelable::Cancelable,
hit_test_result,
&hit_test_result,
input_event,
can_gc,
);
@ -2061,7 +2061,7 @@ impl Document {
event_target,
moving_into,
FireMouseEventType::Leave,
hit_test_result,
&hit_test_result,
input_event,
can_gc,
);
@ -2085,7 +2085,7 @@ impl Document {
FireMouseEventType::Over,
EventBubbles::Bubbles,
EventCancelable::Cancelable,
hit_test_result,
&hit_test_result,
input_event,
can_gc,
);
@ -2098,7 +2098,7 @@ impl Document {
event_target,
moving_from,
FireMouseEventType::Enter,
hit_test_result,
&hit_test_result,
input_event,
can_gc,
);
@ -2111,7 +2111,7 @@ impl Document {
FireMouseEventType::Move,
EventBubbles::Bubbles,
EventCancelable::Cancelable,
hit_test_result,
&hit_test_result,
input_event,
can_gc,
);
@ -2129,15 +2129,15 @@ impl Document {
can_gc: CanGc,
) {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = &input_event.hit_test_result else {
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
return;
};
self.window()
.send_to_embedder(EmbedderMsg::Status(self.webview_id(), None));
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) };
for element in node
for element in hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
{
@ -2146,19 +2146,19 @@ impl Document {
}
self.fire_mouse_event(
node.upcast(),
hit_test_result.node.upcast(),
FireMouseEventType::Out,
EventBubbles::Bubbles,
EventCancelable::Cancelable,
hit_test_result,
&hit_test_result,
input_event,
can_gc,
);
self.handle_mouse_enter_leave_event(
node,
hit_test_result.node.clone(),
None,
FireMouseEventType::Leave,
hit_test_result,
&hit_test_result,
input_event,
can_gc,
);
@ -2169,7 +2169,7 @@ impl Document {
event_target: DomRoot<Node>,
related_target: Option<DomRoot<Node>>,
event_type: FireMouseEventType,
hit_test_result: &CompositorHitTestResult,
hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
@ -2224,12 +2224,12 @@ impl Document {
can_gc: CanGc,
) {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = &input_event.hit_test_result else {
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
return;
};
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) };
let Some(el) = node
let Some(el) = hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
@ -2243,7 +2243,7 @@ impl Document {
"{}: on {:?} at {:?}",
wheel_event_type_string,
node.debug_str(),
hit_test_result.point_in_viewport
hit_test_result.point_in_frame
);
// https://w3c.github.io/uievents/#event-wheelevents
@ -2254,8 +2254,8 @@ impl Document {
EventCancelable::Cancelable,
Some(&self.window),
0i32,
hit_test_result.point_in_viewport.to_i32(),
hit_test_result.point_in_viewport.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result
.point_relative_to_initial_containing_block
.to_i32(),
@ -2286,11 +2286,11 @@ impl Document {
pub(crate) fn handle_touch_event(
&self,
event: TouchEvent,
hit_test_result: Option<CompositorHitTestResult>,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) -> TouchEventResult {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = hit_test_result else {
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
self.update_active_touch_points_when_early_return(event);
return TouchEventResult::Forwarded;
};
@ -2303,8 +2303,8 @@ impl Document {
TouchEventType::Cancel => "touchcancel",
};
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) };
let Some(el) = node
let Some(el) = hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
@ -2316,12 +2316,12 @@ impl Document {
let target = DomRoot::upcast::<EventTarget>(el);
let window = &*self.window;
let client_x = Finite::wrap(hit_test_result.point_in_viewport.x as f64);
let client_y = Finite::wrap(hit_test_result.point_in_viewport.y as f64);
let client_x = Finite::wrap(hit_test_result.point_in_frame.x as f64);
let client_y = Finite::wrap(hit_test_result.point_in_frame.y as f64);
let page_x =
Finite::wrap(hit_test_result.point_in_viewport.x as f64 + window.PageXOffset() as f64);
Finite::wrap(hit_test_result.point_in_frame.x as f64 + window.PageXOffset() as f64);
let page_y =
Finite::wrap(hit_test_result.point_in_viewport.y as f64 + window.PageYOffset() as f64);
Finite::wrap(hit_test_result.point_in_frame.y as f64 + window.PageYOffset() as f64);
let touch = Touch::new(
window, identifier, &target, client_x,

View file

@ -8,7 +8,7 @@ use std::fmt;
use embedder_traits::UntrustedNodeAddress;
use js::rust::HandleValue;
use layout_api::{ElementsFromPointFlags, ElementsFromPointResult, QueryMsg};
use layout_api::ElementsFromPointFlags;
use script_bindings::error::{Error, ErrorResult};
use script_bindings::script_runtime::JSContext;
use servo_arc::Arc;
@ -137,15 +137,6 @@ impl DocumentOrShadowRoot {
}
}
pub(crate) fn query_elements_from_point(
&self,
point: LayoutPoint,
flags: ElementsFromPointFlags,
) -> Vec<ElementsFromPointResult> {
self.window.layout_reflow(QueryMsg::ElementsFromPoint);
self.window.layout().query_elements_from_point(point, flags)
}
#[allow(unsafe_code)]
// https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint
pub(crate) fn element_from_point(
@ -168,7 +159,8 @@ impl DocumentOrShadowRoot {
}
match self
.query_elements_from_point(LayoutPoint::new(x, y), ElementsFromPointFlags::empty())
.window
.elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::empty())
.first()
{
Some(result) => {
@ -218,8 +210,9 @@ impl DocumentOrShadowRoot {
}
// Step 1 and Step 3
let nodes =
self.query_elements_from_point(LayoutPoint::new(x, y), ElementsFromPointFlags::FindAll);
let nodes = self
.window
.elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::FindAll);
let mut elements: Vec<DomRoot<Element>> = nodes
.iter()
.flat_map(|result| {

View file

@ -3,7 +3,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use dom_struct::dom_struct;
use euclid::Point2D;
use js::rust::HandleObject;
use style_traits::CSSPixel;
use crate::dom::bindings::codegen::Bindings::InputEventBinding::{self, InputEventMethods};
use crate::dom::bindings::codegen::Bindings::UIEventBinding::UIEvent_Binding::UIEventMethods;
@ -11,6 +13,7 @@ use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::reflector::reflect_dom_object_with_proto;
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::node::Node;
use crate::dom::uievent::UIEvent;
use crate::dom::window::Window;
use crate::script_runtime::CanGc;
@ -91,3 +94,12 @@ impl InputEventMethods<crate::DomTypeHolder> for InputEvent {
self.uievent.IsTrusted()
}
}
/// A [`HitTestResult`] that is the result of doing a hit test based on a less-fine-grained
/// `CompositorHitTestResult` against our current layout.
pub(crate) struct HitTestResult {
pub node: DomRoot<Node>,
pub point_in_node: Point2D<f32, CSSPixel>,
pub point_in_frame: Point2D<f32, CSSPixel>,
pub point_relative_to_initial_containing_block: Point2D<f32, CSSPixel>,
}

View file

@ -6,7 +6,6 @@ use std::cell::Cell;
use std::default::Default;
use dom_struct::dom_struct;
use embedder_traits::CompositorHitTestResult;
use euclid::Point2D;
use js::rust::HandleObject;
use keyboard_types::Modifiers;
@ -25,6 +24,7 @@ use crate::dom::bindings::root::{DomRoot, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::event::{Event, EventBubbles, EventCancelable};
use crate::dom::eventtarget::EventTarget;
use crate::dom::inputevent::HitTestResult;
use crate::dom::node::Node;
use crate::dom::uievent::UIEvent;
use crate::dom::window::Window;
@ -226,7 +226,7 @@ impl MouseEvent {
event: embedder_traits::MouseButtonEvent,
pressed_mouse_buttons: u16,
window: &Window,
hit_test_result: &CompositorHitTestResult,
hit_test_result: &HitTestResult,
modifiers: Modifiers,
can_gc: CanGc,
) -> DomRoot<Self> {
@ -236,7 +236,7 @@ impl MouseEvent {
embedder_traits::MouseButtonAction::Down => "mousedown",
};
let client_point = hit_test_result.point_in_viewport.to_i32();
let client_point = hit_test_result.point_in_frame.to_i32();
let page_point = hit_test_result
.point_relative_to_initial_containing_block
.to_i32();
@ -256,7 +256,7 @@ impl MouseEvent {
event.button.into(),
pressed_mouse_buttons,
None,
Some(hit_test_result.point_relative_to_item),
Some(hit_test_result.point_in_node),
can_gc,
);

View file

@ -8,6 +8,7 @@ use std::cmp;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::default::Default;
use std::ffi::c_void;
use std::io::{Write, stderr, stdout};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
@ -33,8 +34,8 @@ use dom_struct::dom_struct;
use embedder_traits::user_content_manager::{UserContentManager, UserScript};
use embedder_traits::{
AlertResponse, ConfirmResponse, EmbedderMsg, GamepadEvent, GamepadSupportedHapticEffects,
GamepadUpdateType, PromptResponse, SimpleDialog, Theme, ViewportDetails, WebDriverJSError,
WebDriverJSResult, WebDriverLoadStatus,
GamepadUpdateType, PromptResponse, SimpleDialog, Theme, UntrustedNodeAddress, ViewportDetails,
WebDriverJSError, WebDriverJSResult, WebDriverLoadStatus,
};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D};
use euclid::{Point2D, Scale, Size2D, Vector2D};
@ -51,9 +52,10 @@ use js::rust::{
MutableHandleValue,
};
use layout_api::{
FragmentType, Layout, PendingImage, PendingImageState, PendingRasterizationImage, QueryMsg,
ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, RestyleReason,
TrustedNodeAddress, combine_id_with_fragment_type,
ElementsFromPointFlags, ElementsFromPointResult, FragmentType, Layout, PendingImage,
PendingImageState, PendingRasterizationImage, QueryMsg, ReflowGoal, ReflowPhasesRun,
ReflowRequest, ReflowRequestRestyle, RestyleReason, TrustedNodeAddress,
combine_id_with_fragment_type,
};
use malloc_size_of::MallocSizeOf;
use media::WindowGLContext;
@ -72,7 +74,7 @@ use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMe
use script_bindings::conversions::SafeToJSValConvertible;
use script_bindings::interfaces::WindowHelpers;
use script_bindings::root::Root;
use script_traits::ScriptThreadMessage;
use script_traits::{ConstellationInputEvent, ScriptThreadMessage};
use selectors::attr::CaseSensitivity;
use servo_arc::Arc as ServoArc;
use servo_config::{opts, pref};
@ -88,7 +90,7 @@ use style_traits::CSSPixel;
use stylo_atoms::Atom;
use url::Position;
use webrender_api::ExternalScrollId;
use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel};
use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel, LayoutPoint};
use super::bindings::codegen::Bindings::MessagePortBinding::StructuredSerializeOptions;
use super::bindings::trace::HashMapTracedValues;
@ -138,6 +140,7 @@ use crate::dom::history::History;
use crate::dom::htmlcollection::{CollectionFilter, HTMLCollection};
use crate::dom::htmliframeelement::HTMLIFrameElement;
use crate::dom::idbfactory::IDBFactory;
use crate::dom::inputevent::HitTestResult;
use crate::dom::location::Location;
use crate::dom::medialist::MediaList;
use crate::dom::mediaquerylist::{MediaQueryList, MediaQueryListMatchState};
@ -2589,6 +2592,41 @@ impl Window {
.query_text_indext(node.to_opaque(), point_in_node)
}
pub(crate) fn elements_from_point_query(
&self,
point: LayoutPoint,
flags: ElementsFromPointFlags,
) -> Vec<ElementsFromPointResult> {
self.layout_reflow(QueryMsg::ElementsFromPoint);
self.layout().query_elements_from_point(point, flags)
}
#[allow(unsafe_code)]
pub(crate) fn hit_test_from_input_event(
&self,
input_event: &ConstellationInputEvent,
) -> Option<HitTestResult> {
let compositor_hit_test_result = input_event.hit_test_result.as_ref()?;
let result = self
.elements_from_point_query(
compositor_hit_test_result.point_in_viewport.cast_unit(),
ElementsFromPointFlags::empty(),
)
.into_iter()
.nth(0)?;
// SAFETY: This is safe because `Window::query_elements_from_point` has ensured that
// layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
let address = UntrustedNodeAddress(result.node.0 as *const c_void);
Some(HitTestResult {
node: unsafe { from_untrusted_node_address(address) },
point_in_node: result.point_in_target,
point_in_frame: compositor_hit_test_result.point_in_viewport,
point_relative_to_initial_containing_block: compositor_hit_test_result
.point_relative_to_initial_containing_block,
})
}
#[allow(unsafe_code)]
pub(crate) fn init_window_proxy(&self, window_proxy: &WindowProxy) {
assert!(self.window_proxy.get().is_none());

View file

@ -1137,8 +1137,7 @@ impl ScriptThread {
document.handle_mouse_leave_event(&event, can_gc);
},
InputEvent::Touch(touch_event) => {
let touch_result =
document.handle_touch_event(touch_event, event.hit_test_result, can_gc);
let touch_result = document.handle_touch_event(touch_event, &event, can_gc);
if let (TouchEventResult::Processed(handled), true) =
(touch_result, touch_event.is_cancelable())
{

View file

@ -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))
}
}

View file

@ -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();

View file

@ -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(),

View file

@ -20,7 +20,7 @@ use std::hash::Hash;
use std::path::PathBuf;
use std::sync::Arc;
use base::id::{PipelineId, ScrollTreeNodeId, WebViewId};
use base::id::{PipelineId, WebViewId};
use crossbeam_channel::Sender;
use euclid::{Point2D, Scale, Size2D};
use http::{HeaderMap, Method, StatusCode};
@ -38,6 +38,7 @@ use style::queries::values::PrefersColorScheme;
use style_traits::CSSPixel;
use url::Url;
use uuid::Uuid;
use webrender_api::ExternalScrollId;
use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, LayoutSize};
pub use crate::input_events::*;
@ -888,17 +889,11 @@ pub struct CompositorHitTestResult {
/// containing block.
pub point_relative_to_initial_containing_block: Point2D<f32, CSSPixel>,
/// The hit test point relative to the item itself.
pub point_relative_to_item: Point2D<f32, CSSPixel>,
/// The node address of the hit test result.
pub node: UntrustedNodeAddress,
/// The cursor that should be used when hovering the item hit by the hit test.
pub cursor: Option<Cursor>,
/// The scroll tree node associated with this hit test item.
pub scroll_tree_node: ScrollTreeNodeId,
/// The [`ExternalScrollId`] of the scroll tree node associated with this hit test item.
pub external_scroll_id: ExternalScrollId,
}
/// Whether the default action for a touch event was prevented by web content

View file

@ -39,5 +39,6 @@ selectors = { workspace = true }
serde = { workspace = true }
servo_arc = { workspace = true }
servo_url = { path = "../../url" }
stylo_traits = { workspace = true }
stylo = { workspace = true }
webrender_api = { workspace = true }

View file

@ -25,7 +25,8 @@ use bitflags::bitflags;
use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::LoadData;
use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D, Rect};
use euclid::Point2D;
use euclid::default::{Point2D as UntypedPoint2D, Rect};
use fnv::FnvHashMap;
use fonts::{FontContext, SystemFontServiceProxy};
use fxhash::FxHashMap;
@ -54,6 +55,7 @@ use style::properties::PropertyId;
use style::properties::style_structs::Font;
use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot};
use style::stylesheets::Stylesheet;
use style_traits::CSSPixel;
use webrender_api::units::{DeviceIntSize, LayoutPoint, LayoutVector2D};
use webrender_api::{ExternalScrollId, ImageKey};
@ -288,7 +290,7 @@ pub trait Layout {
animation_timeline_value: f64,
) -> Option<ServoArc<Font>>;
fn query_scrolling_area(&self, node: Option<TrustedNodeAddress>) -> Rect<i32>;
fn query_text_indext(&self, node: OpaqueNode, point: Point2D<f32>) -> Option<usize>;
fn query_text_indext(&self, node: OpaqueNode, point: UntypedPoint2D<f32>) -> Option<usize>;
fn query_elements_from_point(
&self,
point: LayoutPoint,
@ -608,9 +610,9 @@ pub struct ElementsFromPointResult {
/// An [`OpaqueNode`] that contains a pointer to the node hit by
/// this hit test result.
pub node: OpaqueNode,
/// The [`LayoutPoint`] of the original query point relative to the
/// The [`Point2D`] of the original query point relative to the
/// node fragment rectangle.
pub point_in_target: LayoutPoint,
pub point_in_target: Point2D<f32, CSSPixel>,
}
bitflags! {