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

View file

@ -31,6 +31,7 @@ gleam = { workspace = true }
ipc-channel = { workspace = true } ipc-channel = { workspace = true }
libc = { workspace = true } libc = { workspace = true }
log = { workspace = true } log = { workspace = true }
num-traits = { workspace = true }
pixels = { path = "../pixels" } pixels = { path = "../pixels" }
profile_traits = { workspace = true } profile_traits = { workspace = true }
servo_allocator = { path = "../allocator" } servo_allocator = { path = "../allocator" }

View file

@ -11,13 +11,11 @@ use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant; use base::cross_process_instant::CrossProcessInstant;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
use base::{Epoch, WebRenderEpochToU16};
use bitflags::bitflags; use bitflags::bitflags;
use compositing_traits::display_list::{ use compositing_traits::display_list::{CompositorDisplayListInfo, ScrollTree, ScrollType};
CompositorDisplayListInfo, HitTestInfo, ScrollTree, ScrollType,
};
use compositing_traits::rendering_context::RenderingContext; use compositing_traits::rendering_context::RenderingContext;
use compositing_traits::{ use compositing_traits::{
CompositionPipeline, CompositorMsg, ImageUpdate, PipelineExitSource, SendableFrameTree, CompositionPipeline, CompositorMsg, ImageUpdate, PipelineExitSource, SendableFrameTree,
@ -27,13 +25,12 @@ use constellation_traits::{EmbedderToConstellationMessage, PaintMetricEvent};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use dpi::PhysicalSize; use dpi::PhysicalSize;
use embedder_traits::{ use embedder_traits::{
CompositorHitTestResult, Cursor, InputEvent, ShutdownState, UntrustedNodeAddress, CompositorHitTestResult, Cursor, InputEvent, ShutdownState, ViewportDetails,
ViewportDetails,
}; };
use euclid::{Point2D, Rect, Scale, Size2D, Transform3D}; use euclid::{Point2D, Rect, Scale, Size2D, Transform3D};
use ipc_channel::ipc::{self, IpcSharedMemory}; use ipc_channel::ipc::{self, IpcSharedMemory};
use libc::c_void;
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use num_traits::cast::FromPrimitive;
use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage}; use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage};
use profile_traits::mem::{ProcessReports, ProfilerRegistration, Report, ReportKind}; use profile_traits::mem::{ProcessReports, ProfilerRegistration, Report, ReportKind};
use profile_traits::time::{self as profile_time, ProfilerCategory}; use profile_traits::time::{self as profile_time, ProfilerCategory};
@ -48,10 +45,10 @@ use webrender_api::units::{
}; };
use webrender_api::{ use webrender_api::{
self, BuiltDisplayList, DirtyRect, DisplayListPayload, DocumentId, Epoch as WebRenderEpoch, self, BuiltDisplayList, DirtyRect, DisplayListPayload, DocumentId, Epoch as WebRenderEpoch,
FontInstanceFlags, FontInstanceKey, FontInstanceOptions, FontKey, HitTestFlags, ExternalScrollId, FontInstanceFlags, FontInstanceKey, FontInstanceOptions, FontKey,
PipelineId as WebRenderPipelineId, PropertyBinding, ReferenceFrameKind, RenderReasons, HitTestFlags, PipelineId as WebRenderPipelineId, PropertyBinding, ReferenceFrameKind,
SampledScrollOffset, ScrollLocation, SpaceAndClipInfo, SpatialId, SpatialTreeItemKey, RenderReasons, SampledScrollOffset, ScrollLocation, SpaceAndClipInfo, SpatialId,
TransformStyle, SpatialTreeItemKey, TransformStyle,
}; };
use crate::InitialCompositorState; use crate::InitialCompositorState;
@ -200,10 +197,6 @@ pub(crate) struct PipelineDetails {
/// The id of the parent pipeline, if any. /// The id of the parent pipeline, if any.
pub parent_pipeline_id: Option<PipelineId>, 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 /// Whether animations are running
pub animations_running: bool, pub animations_running: bool,
@ -213,10 +206,6 @@ pub(crate) struct PipelineDetails {
/// Whether to use less resources by stopping animations. /// Whether to use less resources by stopping animations.
pub throttled: bool, 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 /// The compositor-side [ScrollTree]. This is used to allow finding and scrolling
/// nodes in the compositor before forwarding new offsets to WebRender. /// nodes in the compositor before forwarding new offsets to WebRender.
pub scroll_tree: ScrollTree, pub scroll_tree: ScrollTree,
@ -252,12 +241,10 @@ impl PipelineDetails {
PipelineDetails { PipelineDetails {
pipeline: None, pipeline: None,
parent_pipeline_id: None, parent_pipeline_id: None,
most_recent_display_list_epoch: None,
viewport_scale: None, viewport_scale: None,
animations_running: false, animations_running: false,
animation_callbacks_running: false, animation_callbacks_running: false,
throttled: false, throttled: false,
hit_test_items: Vec::new(),
scroll_tree: ScrollTree::default(), scroll_tree: ScrollTree::default(),
first_paint_metric: PaintMetricState::Waiting, first_paint_metric: PaintMetricState::Waiting,
first_contentful_paint_metric: PaintMetricState::Waiting, first_contentful_paint_metric: PaintMetricState::Waiting,
@ -272,11 +259,6 @@ impl PipelineDetails {
} }
} }
pub enum HitTestError {
EpochMismatch,
Others,
}
impl ServoRenderer { impl ServoRenderer {
pub fn shutdown_state(&self) -> ShutdownState { pub fn shutdown_state(&self) -> ShutdownState {
self.shutdown_state.get() self.shutdown_state.get()
@ -286,57 +268,33 @@ impl ServoRenderer {
&self, &self,
point: DevicePoint, point: DevicePoint,
details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>, details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>,
) -> Result<CompositorHitTestResult, HitTestError> { ) -> Vec<CompositorHitTestResult> {
match self.hit_test_at_point_with_flags_and_pipeline( self.hit_test_at_point_with_flags(point, HitTestFlags::empty(), details_for_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),
}
} }
// TODO: split this into first half (global) and second half (one for whole compositor, one for webview) // 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, &self,
point: DevicePoint, point: DevicePoint,
flags: HitTestFlags, flags: HitTestFlags,
pipeline_id: Option<WebRenderPipelineId>,
details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>, details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>,
) -> Result<Vec<CompositorHitTestResult>, HitTestError> { ) -> Vec<CompositorHitTestResult> {
// DevicePoint and WorldPoint are the same for us. // DevicePoint and WorldPoint are the same for us.
let world_point = WorldPoint::from_untyped(point.to_untyped()); let world_point = WorldPoint::from_untyped(point.to_untyped());
let results = let results = self.webrender_api.hit_test(
self.webrender_api self.webrender_document,
.hit_test(self.webrender_document, pipeline_id, world_point, flags); None, /* pipeline_id */
world_point,
flags,
);
let mut epoch_mismatch = false; results
let results = results
.items .items
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
let pipeline_id = item.pipeline.into(); let pipeline_id = item.pipeline.into();
let details = details_for_pipeline(pipeline_id)?; 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 let offset = details
.scroll_tree .scroll_tree
.scroll_offset(pipeline_id.root_scroll_id()) .scroll_offset(pipeline_id.root_scroll_id())
@ -344,28 +302,20 @@ impl ServoRenderer {
let point_in_initial_containing_block = let point_in_initial_containing_block =
(item.point_in_viewport + offset).to_untyped(); (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 { Some(CompositorHitTestResult {
pipeline_id, pipeline_id,
point_in_viewport: Point2D::from_untyped(item.point_in_viewport.to_untyped()), point_in_viewport: Point2D::from_untyped(item.point_in_viewport.to_untyped()),
point_relative_to_initial_containing_block: Point2D::from_untyped( point_relative_to_initial_containing_block: Point2D::from_untyped(
point_in_initial_containing_block, point_in_initial_containing_block,
), ),
point_relative_to_item: Point2D::from_untyped( cursor,
item.point_relative_to_item.to_untyped(), external_scroll_id,
),
node: UntrustedNodeAddress(info.node as *const c_void),
cursor: info.cursor,
scroll_tree_node: info.scroll_tree_node,
}) })
}) })
.collect(); .collect()
if epoch_mismatch {
return Err(HitTestError::EpochMismatch);
}
Ok(results)
} }
pub(crate) fn send_transaction(&mut self, transaction: Transaction) { pub(crate) fn send_transaction(&mut self, transaction: Transaction) {
@ -643,10 +593,11 @@ impl IOCompositor {
.global .global
.borrow() .borrow()
.hit_test_at_point(point, details_for_pipeline); .hit_test_at_point(point, details_for_pipeline);
if let Ok(result) = result {
if let Some(result) = result.first() {
self.global self.global
.borrow_mut() .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"); 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 old_scale = webview_renderer.device_pixels_per_page_pixel();
let pipeline_id = display_list_info.pipeline_id; let pipeline_id = display_list_info.pipeline_id;
let details = webview_renderer.ensure_pipeline_details(pipeline_id.into()); 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.install_new_scroll_tree(display_list_info.scroll_tree);
details.viewport_scale = details.viewport_scale =
Some(display_list_info.viewport_details.hidpi_scale_factor); Some(display_list_info.viewport_details.hidpi_scale_factor);
@ -808,33 +755,6 @@ impl IOCompositor {
self.global.borrow_mut().send_transaction(transaction); 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) => { CompositorMsg::GenerateImageKey(sender) => {
let _ = sender.send(self.global.borrow().webrender_api.generate_image_key()); let _ = sender.send(self.global.borrow().webrender_api.generate_image_key());
}, },
@ -1566,15 +1486,6 @@ impl IOCompositor {
}, },
CompositorMsg::NewWebRenderFrameReady(..) => { CompositorMsg::NewWebRenderFrameReady(..) => {
found_recomposite_msg = true; 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
}, },
_ => true, _ => true,

View file

@ -42,7 +42,6 @@ mod from_constellation {
Self::SendInitialTransaction(..) => target!("SendInitialTransaction"), Self::SendInitialTransaction(..) => target!("SendInitialTransaction"),
Self::SendScrollNode(..) => target!("SendScrollNode"), Self::SendScrollNode(..) => target!("SendScrollNode"),
Self::SendDisplayList { .. } => target!("SendDisplayList"), Self::SendDisplayList { .. } => target!("SendDisplayList"),
Self::HitTest(..) => target!("HitTest"),
Self::GenerateImageKey(..) => target!("GenerateImageKey"), Self::GenerateImageKey(..) => target!("GenerateImageKey"),
Self::UpdateImages(..) => target!("UpdateImages"), Self::UpdateImages(..) => target!("UpdateImages"),
Self::GenerateFontKeys(..) => target!("GenerateFontKeys"), 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 * 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/. */ * 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::hash_map::{Entry, Keys};
use std::collections::{HashMap, VecDeque};
use std::rc::Rc; use std::rc::Rc;
use base::id::{PipelineId, WebViewId}; 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::units::{DeviceIntPoint, DevicePixel, DevicePoint, DeviceRect, LayoutVector2D};
use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation}; use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation};
use crate::compositor::{HitTestError, PipelineDetails, ServoRenderer}; use crate::compositor::{PipelineDetails, ServoRenderer};
use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState}; use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -96,10 +96,6 @@ pub(crate) struct WebViewRenderer {
/// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with /// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with
/// active animations or animation frame callbacks. /// active animations or animation frame callbacks.
animating: bool, 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 /// A [`ViewportDescription`] for this [`WebViewRenderer`], which contains the limitations
/// and initial values for zoom derived from the `viewport` meta tag in web content. /// and initial values for zoom derived from the `viewport` meta tag in web content.
viewport_description: Option<ViewportDescription>, viewport_description: Option<ViewportDescription>,
@ -135,8 +131,6 @@ impl WebViewRenderer {
pinch_zoom: PinchZoomFactor::new(1.0), pinch_zoom: PinchZoomFactor::new(1.0),
hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
animating: false, animating: false,
pending_point_input_events: Default::default(),
webrender_frame_ready: Cell::default(),
viewport_description: None, viewport_description: None,
} }
} }
@ -335,55 +329,24 @@ impl WebViewRenderer {
} }
} }
pub(crate) fn dispatch_point_input_event(&self, event: InputEvent) -> bool { pub(crate) fn dispatch_point_input_event(&self, mut 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 {
// Events that do not need to do hit testing are sent directly to the // Events that do not need to do hit testing are sent directly to the
// constellation to filter down. // constellation to filter down.
let Some(point) = event.point() else { let Some(point) = event.point() else {
return false; 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. // 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 get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let result = match self let Some(result) = self
.global .global
.borrow() .borrow()
.hit_test_at_point(point, get_pipeline_details) .hit_test_at_point(point, get_pipeline_details)
{ .into_iter()
Ok(hit_test_results) => Some(hit_test_results), .nth(0)
Err(HitTestError::EpochMismatch) if retry_on_error => { else {
self.pending_point_input_events warn!("Empty hit test result for input event, ignoring.");
.borrow_mut()
.push_back(event.clone());
return false; return false;
},
_ => None,
}; };
match event { match event {
@ -394,19 +357,15 @@ impl WebViewRenderer {
InputEvent::MouseLeave(_) | InputEvent::MouseLeave(_) |
InputEvent::MouseMove(_) | InputEvent::MouseMove(_) |
InputEvent::Wheel(_) => { InputEvent::Wheel(_) => {
if let Some(ref result) = result {
self.global self.global
.borrow_mut() .borrow_mut()
.update_cursor_from_hittest(point, result); .update_cursor_from_hittest(point, &result);
} else {
warn!("Not hit test result.");
}
}, },
_ => unreachable!("Unexpected input event type: {event:?}"), _ => unreachable!("Unexpected input event type: {event:?}"),
} }
if let Err(error) = self.global.borrow().constellation_sender.send( 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:?})."); warn!("Sending event to constellation failed ({error:?}).");
false false
@ -895,16 +854,11 @@ impl WebViewRenderer {
}; };
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let hit_test_results = self let hit_test_results = self.global.borrow().hit_test_at_point_with_flags(
.global
.borrow()
.hit_test_at_point_with_flags_and_pipeline(
cursor, cursor,
HitTestFlags::FIND_ALL, HitTestFlags::FIND_ALL,
None,
get_pipeline_details, get_pipeline_details,
) );
.unwrap_or_default();
// Iterate through all hit test results, processing only the first node of each pipeline. // 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 // 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) Some(&hit_test_result.pipeline_id)
{ {
let scroll_result = pipeline_details.scroll_tree.scroll_node_or_ancestor( 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, scroll_location,
ScrollType::InputEvents, ScrollType::InputEvents,
); );

View file

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

View file

@ -6,7 +6,6 @@ use std::cell::{OnceCell, RefCell};
use std::sync::Arc; use std::sync::Arc;
use app_units::{AU_PER_PX, Au}; use app_units::{AU_PER_PX, Au};
use base::WebRenderEpochToU16;
use base::id::ScrollTreeNodeId; use base::id::ScrollTreeNodeId;
use clip::{Clip, ClipId}; use clip::{Clip, ClipId};
use compositing_traits::display_list::{CompositorDisplayListInfo, SpatialTreeNodeInfo}; use compositing_traits::display_list::{CompositorDisplayListInfo, SpatialTreeNodeInfo};
@ -72,9 +71,6 @@ use background::BackgroundPainter;
pub(crate) use hit_test::HitTest; pub(crate) use hit_test::HitTest;
pub(crate) use stacking_context::*; 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); const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(AU_PER_PX);
pub(crate) struct DisplayListBuilder<'a> { pub(crate) struct DisplayListBuilder<'a> {
@ -168,8 +164,6 @@ impl DisplayListBuilder<'_> {
) -> BuiltDisplayList { ) -> BuiltDisplayList {
// Build the rest of the display list which inclues all of the WebRender primitives. // Build the rest of the display list which inclues all of the WebRender primitives.
let compositor_info = &mut stacking_context_tree.compositor_info; let compositor_info = &mut stacking_context_tree.compositor_info;
compositor_info.hit_test_info.clear();
let mut webrender_display_list_builder = let mut webrender_display_list_builder =
webrender_api::DisplayListBuilder::new(compositor_info.pipeline_id); webrender_api::DisplayListBuilder::new(compositor_info.pipeline_id);
webrender_display_list_builder.begin(); 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. /// Draw highlights around the node that is currently hovered in the devtools.
fn paint_dom_inspector_highlight(&mut self) { fn paint_dom_inspector_highlight(&mut self) {
let Some(highlight) = self let Some(highlight) = self
@ -616,7 +589,6 @@ impl Fragment {
self.maybe_push_hit_test_for_style_and_tag( self.maybe_push_hit_test_for_style_and_tag(
builder, builder,
&positioning_fragment.style, &positioning_fragment.style,
positioning_fragment.base.tag,
rect, rect,
Cursor::Default, Cursor::Default,
); );
@ -706,24 +678,20 @@ impl Fragment {
&self, &self,
builder: &mut DisplayListBuilder, builder: &mut DisplayListBuilder,
style: &ComputedValues, style: &ComputedValues,
tag: Option<Tag>,
rect: PhysicalRect<Au>, rect: PhysicalRect<Au>,
cursor: Cursor, 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 clip_chain_id = builder.clip_chain_id(builder.current_clip_id);
let spatial_id = builder.spatial_id(builder.current_scroll_node_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( builder.wr().push_hit_test(
rect.to_webrender(), rect.to_webrender(),
clip_chain_id, clip_chain_id,
spatial_id, spatial_id,
style.get_webrender_primitive_flags(), 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(); let parent_style = fragment.inline_styles.style.borrow();
self.maybe_push_hit_test_for_style_and_tag( self.maybe_push_hit_test_for_style_and_tag(builder, &parent_style, rect, Cursor::Text);
builder,
&parent_style,
fragment.base.tag,
rect,
Cursor::Text,
);
let color = parent_style.clone_color(); let color = parent_style.clone_color();
let font_metrics = &fragment.font_metrics; let font_metrics = &fragment.font_metrics;
@ -1116,15 +1078,13 @@ impl<'a> BuilderForBoxFragment<'a> {
} }
fn build_hit_test(&self, builder: &mut DisplayListBuilder, rect: LayoutRect) { fn build_hit_test(&self, builder: &mut DisplayListBuilder, rect: LayoutRect) {
let hit_info = builder.hit_info( let cursor = cursor(
&self.fragment.style, self.fragment.style.get_inherited_ui().cursor.keyword,
self.fragment.base.tag,
Cursor::Default, Cursor::Default,
); );
let hit_info = match hit_info { let external_scroll_node_id = builder
Some(hit_info) => hit_info, .compositor_info
None => return, .external_scroll_id_for_scroll_tree_node(builder.current_scroll_node_id);
};
let mut common = builder.common_properties(rect, &self.fragment.style); let mut common = builder.common_properties(rect, &self.fragment.style);
if let Some(clip_chain_id) = self.border_edge_clip(builder, false) { 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.clip_chain_id,
common.spatial_id, common.spatial_id,
common.flags, 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 { impl BoxFragment {
fn border_radius(&self) -> BorderRadius { fn border_radius(&self) -> BorderRadius {
let resolve = let border = self.style.get_border();
|radius: &LengthPercentage, box_size: Au| radius.to_used_value(box_size).to_f32_px(); 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 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| { let corner = |corner: &style::values::computed::BorderCornerRadius| {
Size2D::new( Size2D::new(
resolve(&corner.0.width.0, border_rect.size.width), resolve(&corner.0.width.0, border_rect.size.width),
resolve(&corner.0.height.0, border_rect.size.height), resolve(&corner.0.height.0, border_rect.size.height),
) )
}; };
let border = self.style.get_border();
let mut radius = wr::BorderRadius { let mut radius = wr::BorderRadius {
top_left: corner(&border.border_top_left_radius), top_left: corner(&border.border_top_left_radius),
top_right: corner(&border.border_top_right_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 devtools_traits::ScriptToDevtoolsControlMsg;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use embedder_traits::{ use embedder_traits::{
AllowOrDeny, AnimationState, CompositorHitTestResult, ContextMenuResult, EditingActionEvent, AllowOrDeny, AnimationState, ContextMenuResult, EditingActionEvent, EmbedderMsg,
EmbedderMsg, FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction,
MouseButtonAction, MouseButtonEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId, MouseButtonEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId, UntrustedNodeAddress,
UntrustedNodeAddress, WheelEvent, WheelEvent,
}; };
use encoding_rs::{Encoding, UTF_8}; use encoding_rs::{Encoding, UTF_8};
use euclid::Point2D; use euclid::Point2D;
@ -172,6 +172,7 @@ use crate::dom::htmlinputelement::HTMLInputElement;
use crate::dom::htmlscriptelement::{HTMLScriptElement, ScriptResult}; use crate::dom::htmlscriptelement::{HTMLScriptElement, ScriptResult};
use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::htmltextareaelement::HTMLTextAreaElement;
use crate::dom::htmltitleelement::HTMLTitleElement; use crate::dom::htmltitleelement::HTMLTitleElement;
use crate::dom::inputevent::HitTestResult;
use crate::dom::intersectionobserver::IntersectionObserver; use crate::dom::intersectionobserver::IntersectionObserver;
use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::keyboardevent::KeyboardEvent;
use crate::dom::location::{Location, NavigationType}; use crate::dom::location::{Location, NavigationType};
@ -1542,7 +1543,6 @@ impl Document {
} }
} }
#[allow(unsafe_code)]
pub(crate) fn handle_mouse_button_event( pub(crate) fn handle_mouse_button_event(
&self, &self,
event: MouseButtonEvent, event: MouseButtonEvent,
@ -1550,17 +1550,17 @@ impl Document {
can_gc: CanGc, can_gc: CanGc,
) { ) {
// Ignore all incoming events without a hit test. // 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; return;
}; };
debug!( debug!(
"{:?}: at {:?}", "{:?}: 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) = hit_test_result
let Some(el) = node .node
.inclusive_ancestors(ShadowIncluding::Yes) .inclusive_ancestors(ShadowIncluding::Yes)
.filter_map(DomRoot::downcast::<Element>) .filter_map(DomRoot::downcast::<Element>)
.next() .next()
@ -1591,7 +1591,7 @@ impl Document {
event, event,
input_event.pressed_mouse_buttons, input_event.pressed_mouse_buttons,
&self.window, &self.window,
hit_test_result, &hit_test_result,
input_event.active_keyboard_modifiers, input_event.active_keyboard_modifiers,
can_gc, can_gc,
)); ));
@ -1626,13 +1626,13 @@ impl Document {
if self.focus_transaction.borrow().is_some() { if self.focus_transaction.borrow().is_some() {
self.commit_focus_transaction(FocusInitiator::Local, can_gc); 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 // When the contextmenu event is triggered by right mouse button
// the contextmenu event MUST be dispatched after the mousedown event. // the contextmenu event MUST be dispatched after the mousedown event.
if let (MouseButtonAction::Down, MouseButton::Right) = (event.action, event.button) { 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( fn maybe_show_context_menu(
&self, &self,
target: &EventTarget, target: &EventTarget,
hit_test_result: &CompositorHitTestResult, hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent, input_event: &ConstellationInputEvent,
can_gc: CanGc, can_gc: CanGc,
) { ) {
@ -1652,8 +1652,8 @@ impl Document {
EventCancelable::Cancelable, // cancelable EventCancelable::Cancelable, // cancelable
Some(&self.window), // view Some(&self.window), // view
0, // detail 0, // detail
hit_test_result.point_in_viewport.to_i32(), hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_viewport.to_i32(), hit_test_result.point_in_frame.to_i32(),
hit_test_result hit_test_result
.point_relative_to_initial_containing_block .point_relative_to_initial_containing_block
.to_i32(), .to_i32(),
@ -1698,13 +1698,13 @@ impl Document {
fn maybe_fire_dblclick( fn maybe_fire_dblclick(
&self, &self,
target: &Node, target: &Node,
hit_test_result: &CompositorHitTestResult, hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent, input_event: &ConstellationInputEvent,
can_gc: CanGc, can_gc: CanGc,
) { ) {
// https://w3c.github.io/uievents/#event-type-dblclick // https://w3c.github.io/uievents/#event-type-dblclick
let now = Instant::now(); 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(); let opt = self.last_click_info.borrow_mut().take();
if let Some((last_time, last_pos)) = opt { 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; let DBL_CLICK_DIST_THRESHOLD = pref!(dom_document_dblclick_dist) as u64;
// Calculate distance between this click and the previous click. // 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(); let dist = (line.dot(line) as f64).sqrt();
if now.duration_since(last_time) < DBL_CLICK_TIMEOUT && if now.duration_since(last_time) < DBL_CLICK_TIMEOUT &&
@ -1729,8 +1729,8 @@ impl Document {
EventCancelable::Cancelable, EventCancelable::Cancelable,
Some(&self.window), Some(&self.window),
click_count, click_count,
point_in_viewport.to_i32(), point_in_frame.to_i32(),
point_in_viewport.to_i32(), point_in_frame.to_i32(),
hit_test_result hit_test_result
.point_relative_to_initial_containing_block .point_relative_to_initial_containing_block
.to_i32(), .to_i32(),
@ -1750,7 +1750,7 @@ impl Document {
} }
// Update last_click_info with the time and position of the click. // 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)] #[allow(clippy::too_many_arguments)]
@ -1760,7 +1760,7 @@ impl Document {
event_name: FireMouseEventType, event_name: FireMouseEventType,
can_bubble: EventBubbles, can_bubble: EventBubbles,
cancelable: EventCancelable, cancelable: EventCancelable,
hit_test_result: &CompositorHitTestResult, hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent, input_event: &ConstellationInputEvent,
can_gc: CanGc, can_gc: CanGc,
) { ) {
@ -1771,8 +1771,8 @@ impl Document {
cancelable, cancelable,
Some(&self.window), Some(&self.window),
0i32, 0i32,
hit_test_result.point_in_viewport.to_i32(), hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_viewport.to_i32(), hit_test_result.point_in_frame.to_i32(),
hit_test_result hit_test_result
.point_relative_to_initial_containing_block .point_relative_to_initial_containing_block
.to_i32(), .to_i32(),
@ -2004,12 +2004,12 @@ impl Document {
can_gc: CanGc, can_gc: CanGc,
) { ) {
// Ignore all incoming events without a hit test. // 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; return;
}; };
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(new_target) = hit_test_result
let Some(new_target) = node .node
.inclusive_ancestors(ShadowIncluding::No) .inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>) .filter_map(DomRoot::downcast::<Element>)
.next() .next()
@ -2049,7 +2049,7 @@ impl Document {
FireMouseEventType::Out, FireMouseEventType::Out,
EventBubbles::Bubbles, EventBubbles::Bubbles,
EventCancelable::Cancelable, EventCancelable::Cancelable,
hit_test_result, &hit_test_result,
input_event, input_event,
can_gc, can_gc,
); );
@ -2061,7 +2061,7 @@ impl Document {
event_target, event_target,
moving_into, moving_into,
FireMouseEventType::Leave, FireMouseEventType::Leave,
hit_test_result, &hit_test_result,
input_event, input_event,
can_gc, can_gc,
); );
@ -2085,7 +2085,7 @@ impl Document {
FireMouseEventType::Over, FireMouseEventType::Over,
EventBubbles::Bubbles, EventBubbles::Bubbles,
EventCancelable::Cancelable, EventCancelable::Cancelable,
hit_test_result, &hit_test_result,
input_event, input_event,
can_gc, can_gc,
); );
@ -2098,7 +2098,7 @@ impl Document {
event_target, event_target,
moving_from, moving_from,
FireMouseEventType::Enter, FireMouseEventType::Enter,
hit_test_result, &hit_test_result,
input_event, input_event,
can_gc, can_gc,
); );
@ -2111,7 +2111,7 @@ impl Document {
FireMouseEventType::Move, FireMouseEventType::Move,
EventBubbles::Bubbles, EventBubbles::Bubbles,
EventCancelable::Cancelable, EventCancelable::Cancelable,
hit_test_result, &hit_test_result,
input_event, input_event,
can_gc, can_gc,
); );
@ -2129,15 +2129,15 @@ impl Document {
can_gc: CanGc, can_gc: CanGc,
) { ) {
// Ignore all incoming events without a hit test. // 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; return;
}; };
self.window() self.window()
.send_to_embedder(EmbedderMsg::Status(self.webview_id(), None)); .send_to_embedder(EmbedderMsg::Status(self.webview_id(), None));
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; for element in hit_test_result
for element in node .node
.inclusive_ancestors(ShadowIncluding::No) .inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>) .filter_map(DomRoot::downcast::<Element>)
{ {
@ -2146,19 +2146,19 @@ impl Document {
} }
self.fire_mouse_event( self.fire_mouse_event(
node.upcast(), hit_test_result.node.upcast(),
FireMouseEventType::Out, FireMouseEventType::Out,
EventBubbles::Bubbles, EventBubbles::Bubbles,
EventCancelable::Cancelable, EventCancelable::Cancelable,
hit_test_result, &hit_test_result,
input_event, input_event,
can_gc, can_gc,
); );
self.handle_mouse_enter_leave_event( self.handle_mouse_enter_leave_event(
node, hit_test_result.node.clone(),
None, None,
FireMouseEventType::Leave, FireMouseEventType::Leave,
hit_test_result, &hit_test_result,
input_event, input_event,
can_gc, can_gc,
); );
@ -2169,7 +2169,7 @@ impl Document {
event_target: DomRoot<Node>, event_target: DomRoot<Node>,
related_target: Option<DomRoot<Node>>, related_target: Option<DomRoot<Node>>,
event_type: FireMouseEventType, event_type: FireMouseEventType,
hit_test_result: &CompositorHitTestResult, hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent, input_event: &ConstellationInputEvent,
can_gc: CanGc, can_gc: CanGc,
) { ) {
@ -2224,12 +2224,12 @@ impl Document {
can_gc: CanGc, can_gc: CanGc,
) { ) {
// Ignore all incoming events without a hit test. // 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; return;
}; };
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(el) = hit_test_result
let Some(el) = node .node
.inclusive_ancestors(ShadowIncluding::No) .inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>) .filter_map(DomRoot::downcast::<Element>)
.next() .next()
@ -2243,7 +2243,7 @@ impl Document {
"{}: on {:?} at {:?}", "{}: on {:?} at {:?}",
wheel_event_type_string, wheel_event_type_string,
node.debug_str(), node.debug_str(),
hit_test_result.point_in_viewport hit_test_result.point_in_frame
); );
// https://w3c.github.io/uievents/#event-wheelevents // https://w3c.github.io/uievents/#event-wheelevents
@ -2254,8 +2254,8 @@ impl Document {
EventCancelable::Cancelable, EventCancelable::Cancelable,
Some(&self.window), Some(&self.window),
0i32, 0i32,
hit_test_result.point_in_viewport.to_i32(), hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_viewport.to_i32(), hit_test_result.point_in_frame.to_i32(),
hit_test_result hit_test_result
.point_relative_to_initial_containing_block .point_relative_to_initial_containing_block
.to_i32(), .to_i32(),
@ -2286,11 +2286,11 @@ impl Document {
pub(crate) fn handle_touch_event( pub(crate) fn handle_touch_event(
&self, &self,
event: TouchEvent, event: TouchEvent,
hit_test_result: Option<CompositorHitTestResult>, input_event: &ConstellationInputEvent,
can_gc: CanGc, can_gc: CanGc,
) -> TouchEventResult { ) -> TouchEventResult {
// Ignore all incoming events without a hit test. // 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); self.update_active_touch_points_when_early_return(event);
return TouchEventResult::Forwarded; return TouchEventResult::Forwarded;
}; };
@ -2303,8 +2303,8 @@ impl Document {
TouchEventType::Cancel => "touchcancel", TouchEventType::Cancel => "touchcancel",
}; };
let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(el) = hit_test_result
let Some(el) = node .node
.inclusive_ancestors(ShadowIncluding::No) .inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>) .filter_map(DomRoot::downcast::<Element>)
.next() .next()
@ -2316,12 +2316,12 @@ impl Document {
let target = DomRoot::upcast::<EventTarget>(el); let target = DomRoot::upcast::<EventTarget>(el);
let window = &*self.window; let window = &*self.window;
let client_x = Finite::wrap(hit_test_result.point_in_viewport.x 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_viewport.y as f64); let client_y = Finite::wrap(hit_test_result.point_in_frame.y as f64);
let page_x = 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 = 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( let touch = Touch::new(
window, identifier, &target, client_x, window, identifier, &target, client_x,

View file

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

View file

@ -3,7 +3,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use dom_struct::dom_struct; use dom_struct::dom_struct;
use euclid::Point2D;
use js::rust::HandleObject; use js::rust::HandleObject;
use style_traits::CSSPixel;
use crate::dom::bindings::codegen::Bindings::InputEventBinding::{self, InputEventMethods}; use crate::dom::bindings::codegen::Bindings::InputEventBinding::{self, InputEventMethods};
use crate::dom::bindings::codegen::Bindings::UIEventBinding::UIEvent_Binding::UIEventMethods; 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::reflector::reflect_dom_object_with_proto;
use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString; use crate::dom::bindings::str::DOMString;
use crate::dom::node::Node;
use crate::dom::uievent::UIEvent; use crate::dom::uievent::UIEvent;
use crate::dom::window::Window; use crate::dom::window::Window;
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
@ -91,3 +94,12 @@ impl InputEventMethods<crate::DomTypeHolder> for InputEvent {
self.uievent.IsTrusted() 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 std::default::Default;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use embedder_traits::CompositorHitTestResult;
use euclid::Point2D; use euclid::Point2D;
use js::rust::HandleObject; use js::rust::HandleObject;
use keyboard_types::Modifiers; use keyboard_types::Modifiers;
@ -25,6 +24,7 @@ use crate::dom::bindings::root::{DomRoot, MutNullableDom};
use crate::dom::bindings::str::DOMString; use crate::dom::bindings::str::DOMString;
use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::event::{Event, EventBubbles, EventCancelable};
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::inputevent::HitTestResult;
use crate::dom::node::Node; use crate::dom::node::Node;
use crate::dom::uievent::UIEvent; use crate::dom::uievent::UIEvent;
use crate::dom::window::Window; use crate::dom::window::Window;
@ -226,7 +226,7 @@ impl MouseEvent {
event: embedder_traits::MouseButtonEvent, event: embedder_traits::MouseButtonEvent,
pressed_mouse_buttons: u16, pressed_mouse_buttons: u16,
window: &Window, window: &Window,
hit_test_result: &CompositorHitTestResult, hit_test_result: &HitTestResult,
modifiers: Modifiers, modifiers: Modifiers,
can_gc: CanGc, can_gc: CanGc,
) -> DomRoot<Self> { ) -> DomRoot<Self> {
@ -236,7 +236,7 @@ impl MouseEvent {
embedder_traits::MouseButtonAction::Down => "mousedown", 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 let page_point = hit_test_result
.point_relative_to_initial_containing_block .point_relative_to_initial_containing_block
.to_i32(); .to_i32();
@ -256,7 +256,7 @@ impl MouseEvent {
event.button.into(), event.button.into(),
pressed_mouse_buttons, pressed_mouse_buttons,
None, None,
Some(hit_test_result.point_relative_to_item), Some(hit_test_result.point_in_node),
can_gc, can_gc,
); );

View file

@ -8,6 +8,7 @@ use std::cmp;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::default::Default; use std::default::Default;
use std::ffi::c_void;
use std::io::{Write, stderr, stdout}; use std::io::{Write, stderr, stdout};
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, Mutex}; 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::user_content_manager::{UserContentManager, UserScript};
use embedder_traits::{ use embedder_traits::{
AlertResponse, ConfirmResponse, EmbedderMsg, GamepadEvent, GamepadSupportedHapticEffects, AlertResponse, ConfirmResponse, EmbedderMsg, GamepadEvent, GamepadSupportedHapticEffects,
GamepadUpdateType, PromptResponse, SimpleDialog, Theme, ViewportDetails, WebDriverJSError, GamepadUpdateType, PromptResponse, SimpleDialog, Theme, UntrustedNodeAddress, ViewportDetails,
WebDriverJSResult, WebDriverLoadStatus, WebDriverJSError, WebDriverJSResult, WebDriverLoadStatus,
}; };
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D}; use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D};
use euclid::{Point2D, Scale, Size2D, Vector2D}; use euclid::{Point2D, Scale, Size2D, Vector2D};
@ -51,9 +52,10 @@ use js::rust::{
MutableHandleValue, MutableHandleValue,
}; };
use layout_api::{ use layout_api::{
FragmentType, Layout, PendingImage, PendingImageState, PendingRasterizationImage, QueryMsg, ElementsFromPointFlags, ElementsFromPointResult, FragmentType, Layout, PendingImage,
ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, RestyleReason, PendingImageState, PendingRasterizationImage, QueryMsg, ReflowGoal, ReflowPhasesRun,
TrustedNodeAddress, combine_id_with_fragment_type, ReflowRequest, ReflowRequestRestyle, RestyleReason, TrustedNodeAddress,
combine_id_with_fragment_type,
}; };
use malloc_size_of::MallocSizeOf; use malloc_size_of::MallocSizeOf;
use media::WindowGLContext; use media::WindowGLContext;
@ -72,7 +74,7 @@ use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMe
use script_bindings::conversions::SafeToJSValConvertible; use script_bindings::conversions::SafeToJSValConvertible;
use script_bindings::interfaces::WindowHelpers; use script_bindings::interfaces::WindowHelpers;
use script_bindings::root::Root; use script_bindings::root::Root;
use script_traits::ScriptThreadMessage; use script_traits::{ConstellationInputEvent, ScriptThreadMessage};
use selectors::attr::CaseSensitivity; use selectors::attr::CaseSensitivity;
use servo_arc::Arc as ServoArc; use servo_arc::Arc as ServoArc;
use servo_config::{opts, pref}; use servo_config::{opts, pref};
@ -88,7 +90,7 @@ use style_traits::CSSPixel;
use stylo_atoms::Atom; use stylo_atoms::Atom;
use url::Position; use url::Position;
use webrender_api::ExternalScrollId; 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::codegen::Bindings::MessagePortBinding::StructuredSerializeOptions;
use super::bindings::trace::HashMapTracedValues; use super::bindings::trace::HashMapTracedValues;
@ -138,6 +140,7 @@ use crate::dom::history::History;
use crate::dom::htmlcollection::{CollectionFilter, HTMLCollection}; use crate::dom::htmlcollection::{CollectionFilter, HTMLCollection};
use crate::dom::htmliframeelement::HTMLIFrameElement; use crate::dom::htmliframeelement::HTMLIFrameElement;
use crate::dom::idbfactory::IDBFactory; use crate::dom::idbfactory::IDBFactory;
use crate::dom::inputevent::HitTestResult;
use crate::dom::location::Location; use crate::dom::location::Location;
use crate::dom::medialist::MediaList; use crate::dom::medialist::MediaList;
use crate::dom::mediaquerylist::{MediaQueryList, MediaQueryListMatchState}; use crate::dom::mediaquerylist::{MediaQueryList, MediaQueryListMatchState};
@ -2589,6 +2592,41 @@ impl Window {
.query_text_indext(node.to_opaque(), point_in_node) .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)] #[allow(unsafe_code)]
pub(crate) fn init_window_proxy(&self, window_proxy: &WindowProxy) { pub(crate) fn init_window_proxy(&self, window_proxy: &WindowProxy) {
assert!(self.window_proxy.get().is_none()); assert!(self.window_proxy.get().is_none());

View file

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

View file

@ -10,7 +10,7 @@ use std::collections::HashMap;
use base::id::ScrollTreeNodeId; use base::id::ScrollTreeNodeId;
use base::print_tree::PrintTree; use base::print_tree::PrintTree;
use bitflags::bitflags; use bitflags::bitflags;
use embedder_traits::{Cursor, ViewportDetails}; use embedder_traits::ViewportDetails;
use euclid::{SideOffsets2D, Transform3D}; use euclid::{SideOffsets2D, Transform3D};
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -57,20 +57,6 @@ pub struct AxesScrollSensitivity {
pub y: ScrollType, 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)] #[derive(Debug, Deserialize, Serialize)]
pub enum SpatialTreeNodeInfo { pub enum SpatialTreeNodeInfo {
ReferenceFrame(ReferenceFrameNodeInfo), ReferenceFrame(ReferenceFrameNodeInfo),
@ -514,17 +500,33 @@ impl ScrollTree {
}) })
} }
/// Scroll the given scroll node on this scroll tree. If the node cannot be scrolled, fn node_with_external_scroll_node_id(
/// because it isn't a scrollable node or it's already scrolled to the maximum scroll &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 /// 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. /// new offset if a scroll was performed, otherwise returns None.
pub fn scroll_node_or_ancestor( pub fn scroll_node_or_ancestor(
&mut self, &mut self,
scroll_node_id: &ScrollTreeNodeId, external_id: &ExternalScrollId,
scroll_location: ScrollLocation, scroll_location: ScrollLocation,
context: ScrollType, context: ScrollType,
) -> Option<(ExternalScrollId, LayoutVector2D)> { ) -> 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() { if result.is_some() {
self.invalidate_cached_transforms(); self.invalidate_cached_transforms();
} }
@ -713,6 +715,22 @@ impl ScrollTree {
}; };
root_node.invalidate_cached_transforms(self, false /* ancestors_invalid */); 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 /// In order to pretty print the [ScrollTree] structure, we are converting
@ -787,11 +805,6 @@ pub struct CompositorDisplayListInfo {
/// The epoch of the display list. /// The epoch of the display list.
pub epoch: Epoch, 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 /// A ScrollTree used by the compositor to scroll the contents of the
/// display list. /// display list.
pub scroll_tree: ScrollTree, pub scroll_tree: ScrollTree,
@ -856,7 +869,6 @@ impl CompositorDisplayListInfo {
viewport_details, viewport_details,
content_size, content_size,
epoch, epoch,
hit_test_info: Default::default(),
scroll_tree, scroll_tree,
root_reference_frame_id, root_reference_frame_id,
root_scroll_node_id, root_scroll_node_id,
@ -865,27 +877,12 @@ impl CompositorDisplayListInfo {
} }
} }
/// Add or re-use a duplicate HitTestInfo entry in this `CompositorHitTestInfo` pub fn external_scroll_id_for_scroll_tree_node(
/// and return the index. &self,
pub fn add_hit_test_info( id: ScrollTreeNodeId,
&mut self, ) -> ExternalScrollId {
node: u64, self.scroll_tree
cursor: Option<Cursor>, .external_scroll_id_for_scroll_tree_node(id)
scroll_tree_node: ScrollTreeNodeId, .unwrap_or(ExternalScrollId(0, self.pipeline_id))
) -> 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
} }
} }

View file

@ -25,17 +25,17 @@ use std::sync::{Arc, Mutex};
use bitflags::bitflags; use bitflags::bitflags;
use display_list::CompositorDisplayListInfo; use display_list::CompositorDisplayListInfo;
use embedder_traits::{CompositorHitTestResult, ScreenGeometry}; use embedder_traits::ScreenGeometry;
use euclid::default::Size2D as UntypedSize2D; use euclid::default::Size2D as UntypedSize2D;
use ipc_channel::ipc::{self, IpcSharedMemory}; use ipc_channel::ipc::{self, IpcSharedMemory};
use profile_traits::mem::{OpaqueSender, ReportsChan}; use profile_traits::mem::{OpaqueSender, ReportsChan};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use webrender_api::units::{DevicePoint, LayoutVector2D, TexelRect}; use webrender_api::units::{LayoutVector2D, TexelRect};
use webrender_api::{ use webrender_api::{
BuiltDisplayList, BuiltDisplayListDescriptor, ExternalImage, ExternalImageData, BuiltDisplayList, BuiltDisplayListDescriptor, ExternalImage, ExternalImageData,
ExternalImageHandler, ExternalImageId, ExternalImageSource, ExternalScrollId, ExternalImageHandler, ExternalImageId, ExternalImageSource, ExternalScrollId,
FontInstanceFlags, FontInstanceKey, FontKey, HitTestFlags, ImageData, ImageDescriptor, FontInstanceFlags, FontInstanceKey, FontKey, ImageData, ImageDescriptor, ImageKey,
ImageKey, NativeFontHandle, PipelineId as WebRenderPipelineId, NativeFontHandle, PipelineId as WebRenderPipelineId,
}; };
use crate::viewport_description::ViewportDescription; 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. /// An [ipc::IpcBytesReceiver] used to send the raw data of the display list.
display_list_receiver: ipc::IpcBytesReceiver, 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 /// Create a new image key. The result will be returned via the
/// provided channel sender. /// provided channel sender.
GenerateImageKey(IpcSender<ImageKey>), 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. /// Create a new image key. Blocks until the key is available.
pub fn generate_image_key_blocking(&self) -> Option<ImageKey> { pub fn generate_image_key_blocking(&self) -> Option<ImageKey> {
let (sender, receiver) = ipc::channel().unwrap(); let (sender, receiver) = ipc::channel().unwrap();

View file

@ -7,13 +7,12 @@ use std::cell::Cell;
use base::id::ScrollTreeNodeId; use base::id::ScrollTreeNodeId;
use compositing_traits::display_list::{ use compositing_traits::display_list::{
AxesScrollSensitivity, ScrollTree, ScrollType, ScrollableNodeInfo, SpatialTreeNodeInfo, AxesScrollSensitivity, ScrollTree, ScrollType, ScrollableNodeInfo, SpatialTreeNodeInfo,
StickyNodeInfo,
}; };
use euclid::{SideOffsets2D, Size2D}; use euclid::Size2D;
use webrender_api::units::LayoutVector2D; 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 pipeline_id = PipelineId(0, 0);
let num_nodes = tree.nodes.len(); let num_nodes = tree.nodes.len();
let parent = if num_nodes > 0 { let parent = if num_nodes > 0 {
@ -24,10 +23,11 @@ fn add_mock_scroll_node(tree: &mut ScrollTree) -> ScrollTreeNodeId {
None 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(), parent.as_ref(),
SpatialTreeNodeInfo::Scroll(ScrollableNodeInfo { SpatialTreeNodeInfo::Scroll(ScrollableNodeInfo {
external_id: ExternalScrollId(num_nodes as u64, pipeline_id), external_id,
content_rect: Size2D::new(200.0, 200.0).into(), content_rect: Size2D::new(200.0, 200.0).into(),
clip_rect: Size2D::new(100.0, 100.0).into(), clip_rect: Size2D::new(100.0, 100.0).into(),
scroll_sensitivity: AxesScrollSensitivity { scroll_sensitivity: AxesScrollSensitivity {
@ -37,42 +37,42 @@ fn add_mock_scroll_node(tree: &mut ScrollTree) -> ScrollTreeNodeId {
offset: LayoutVector2D::zero(), offset: LayoutVector2D::zero(),
offset_changed: Cell::new(false), offset_changed: Cell::new(false),
}), }),
) );
(scroll_node_id, external_id)
} }
#[test] #[test]
fn test_scroll_tree_simple_scroll() { fn test_scroll_tree_simple_scroll() {
let mut scroll_tree = ScrollTree::default(); let mut scroll_tree = ScrollTree::default();
let pipeline_id = PipelineId(0, 0); let (id, external_id) = add_mock_scroll_node(&mut scroll_tree);
let id = add_mock_scroll_node(&mut scroll_tree);
let (scrolled_id, offset) = scroll_tree let (scrolled_id, offset) = scroll_tree
.scroll_node_or_ancestor( .scroll_node_or_ancestor(
&id, &external_id,
ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)), ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)),
ScrollType::Script, ScrollType::Script,
) )
.unwrap(); .unwrap();
let expected_offset = LayoutVector2D::new(20.0, 40.0); 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!(offset, expected_offset);
assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset)); assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset));
let (scrolled_id, offset) = scroll_tree let (scrolled_id, offset) = scroll_tree
.scroll_node_or_ancestor( .scroll_node_or_ancestor(
&id, &external_id,
ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)), ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)),
ScrollType::Script, ScrollType::Script,
) )
.unwrap(); .unwrap();
let expected_offset = LayoutVector2D::new(0.0, 0.0); 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!(offset, expected_offset);
assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset)); assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset));
// Scroll offsets must be positive. // Scroll offsets must be positive.
let result = scroll_tree.scroll_node_or_ancestor( let result = scroll_tree.scroll_node_or_ancestor(
&id, &external_id,
ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)), ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)),
ScrollType::Script, ScrollType::Script,
); );
@ -88,26 +88,33 @@ fn test_scroll_tree_simple_scroll_chaining() {
let mut scroll_tree = ScrollTree::default(); let mut scroll_tree = ScrollTree::default();
let pipeline_id = PipelineId(0, 0); 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( let unscrollable_child_id = scroll_tree.add_scroll_tree_node(
Some(&parent_id), Some(&parent_id),
SpatialTreeNodeInfo::Sticky(StickyNodeInfo { SpatialTreeNodeInfo::Scroll(ScrollableNodeInfo {
frame_rect: Size2D::new(100.0, 100.0).into(), external_id: unscrollable_external_id,
margins: SideOffsets2D::default(), content_rect: Size2D::new(100.0, 100.0).into(),
vertical_offset_bounds: StickyOffsetBounds::new(0.0, 0.0), clip_rect: Size2D::new(100.0, 100.0).into(),
horizontal_offset_bounds: StickyOffsetBounds::new(0.0, 0.0), 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 let (scrolled_id, offset) = scroll_tree
.scroll_node_or_ancestor( .scroll_node_or_ancestor(
&unscrollable_child_id, &unscrollable_external_id,
ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)), ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)),
ScrollType::Script, ScrollType::Script,
) )
.unwrap(); .unwrap();
let expected_offset = LayoutVector2D::new(20.0, 40.0); 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!(offset, expected_offset);
assert_eq!( assert_eq!(
scroll_tree.get_node(&parent_id).offset(), scroll_tree.get_node(&parent_id).offset(),
@ -116,35 +123,37 @@ fn test_scroll_tree_simple_scroll_chaining() {
let (scrolled_id, offset) = scroll_tree let (scrolled_id, offset) = scroll_tree
.scroll_node_or_ancestor( .scroll_node_or_ancestor(
&unscrollable_child_id, &unscrollable_external_id,
ScrollLocation::Delta(LayoutVector2D::new(10.0, 15.0)), ScrollLocation::Delta(LayoutVector2D::new(10.0, 15.0)),
ScrollType::Script, ScrollType::Script,
) )
.unwrap(); .unwrap();
let expected_offset = LayoutVector2D::new(30.0, 55.0); 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!(offset, expected_offset);
assert_eq!( assert_eq!(
scroll_tree.get_node(&parent_id).offset(), scroll_tree.get_node(&parent_id).offset(),
Some(expected_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] #[test]
fn test_scroll_tree_chain_when_at_extent() { fn test_scroll_tree_chain_when_at_extent() {
let mut scroll_tree = ScrollTree::default(); let mut scroll_tree = ScrollTree::default();
let pipeline_id = PipelineId(0, 0); let (parent_id, parent_external_id) = add_mock_scroll_node(&mut scroll_tree);
let parent_id = add_mock_scroll_node(&mut scroll_tree); let (child_id, child_external_id) = add_mock_scroll_node(&mut scroll_tree);
let child_id = add_mock_scroll_node(&mut scroll_tree);
let (scrolled_id, offset) = 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(); .unwrap();
let expected_offset = LayoutVector2D::new(0.0, 100.0); 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!(offset, expected_offset);
assert_eq!( assert_eq!(
scroll_tree.get_node(&child_id).offset(), 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. // of its scroll area in the y axis.
let (scrolled_id, offset) = scroll_tree let (scrolled_id, offset) = scroll_tree
.scroll_node_or_ancestor( .scroll_node_or_ancestor(
&child_id, &child_external_id,
ScrollLocation::Delta(LayoutVector2D::new(0.0, 10.0)), ScrollLocation::Delta(LayoutVector2D::new(0.0, 10.0)),
ScrollType::Script, ScrollType::Script,
) )
.unwrap(); .unwrap();
let expected_offset = LayoutVector2D::new(0.0, 10.0); 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!(offset, expected_offset);
assert_eq!( assert_eq!(
scroll_tree.get_node(&parent_id).offset(), 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` // Create a tree with a scrollable leaf, but make its `scroll_sensitivity`
// reflect `overflow: hidden` ie not responsive to non-script scroll events. // reflect `overflow: hidden` ie not responsive to non-script scroll events.
let pipeline_id = PipelineId(0, 0); let (parent_id, parent_external_id) = add_mock_scroll_node(&mut scroll_tree);
let parent_id = add_mock_scroll_node(&mut scroll_tree); let (overflow_hidden_id, overflow_hidden_external_id) = add_mock_scroll_node(&mut scroll_tree);
let overflow_hidden_id = add_mock_scroll_node(&mut scroll_tree);
let node = scroll_tree.get_node_mut(&overflow_hidden_id); let node = scroll_tree.get_node_mut(&overflow_hidden_id);
if let SpatialTreeNodeInfo::Scroll(ref mut scroll_node_info) = node.info { 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 let (scrolled_id, offset) = scroll_tree
.scroll_node_or_ancestor( .scroll_node_or_ancestor(
&overflow_hidden_id, &overflow_hidden_external_id,
ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)), ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)),
ScrollType::InputEvents, ScrollType::InputEvents,
) )
.unwrap(); .unwrap();
let expected_offset = LayoutVector2D::new(20.0, 40.0); 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!(offset, expected_offset);
assert_eq!( assert_eq!(
scroll_tree.get_node(&parent_id).offset(), scroll_tree.get_node(&parent_id).offset(),

View file

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

View file

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