script/compositor: Handle cursor updates from script (#38518)

Instead of using WebRender hit testing to update the cursor, base it on
layout hit tests. This allows removing the majority of WebRender hit
test items and finally opens up the possibility of adding support for
custom cursors. In addition, this change fixes an issue where cursors
were not set properly on areas of the viewport that extended past the
page content.

Testing: This is difficult to test as verifying that the cursor changed
properly is beyond the capabilities of Servo's test harnesses.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Martin Robinson 2025-08-07 20:49:38 +02:00 committed by GitHub
parent 87538282db
commit 6651f37c05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 279 additions and 259 deletions

4
Cargo.lock generated
View file

@ -1390,7 +1390,6 @@ dependencies = [
"ipc-channel", "ipc-channel",
"libc", "libc",
"log", "log",
"num-traits",
"pixels", "pixels",
"profile_traits", "profile_traits",
"servo-tracing", "servo-tracing",
@ -1481,6 +1480,7 @@ dependencies = [
"servo_config", "servo_config",
"servo_rand", "servo_rand",
"servo_url", "servo_url",
"stylo_traits",
"tracing", "tracing",
"webgpu", "webgpu",
"webgpu_traits", "webgpu_traits",
@ -1511,6 +1511,7 @@ dependencies = [
"servo_url", "servo_url",
"strum", "strum",
"strum_macros", "strum_macros",
"stylo_traits",
"uuid", "uuid",
"webgpu_traits", "webgpu_traits",
"webrender_api", "webrender_api",
@ -2250,7 +2251,6 @@ dependencies = [
"log", "log",
"malloc_size_of_derive", "malloc_size_of_derive",
"num-derive", "num-derive",
"num-traits",
"pixels", "pixels",
"serde", "serde",
"servo_geometry", "servo_geometry",

View file

@ -31,7 +31,6 @@ 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

@ -24,13 +24,10 @@ use compositing_traits::{
use constellation_traits::{EmbedderToConstellationMessage, PaintMetricEvent}; 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, InputEvent, ShutdownState, ViewportDetails};
CompositorHitTestResult, Cursor, InputEvent, ShutdownState, 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 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};
@ -122,11 +119,9 @@ pub struct ServoRenderer {
/// True to translate mouse input into touch events. /// True to translate mouse input into touch events.
pub(crate) convert_mouse_to_touch: bool, pub(crate) convert_mouse_to_touch: bool,
/// Current mouse cursor. /// The last position in the rendered view that the mouse moved over. This becomes `None`
cursor: Cursor, /// when the mouse leaves the rendered view.
pub(crate) last_mouse_move_position: Option<DevicePoint>,
/// Current cursor position.
cursor_pos: DevicePoint,
} }
/// NB: Never block on the constellation, because sometimes the constellation blocks on us. /// NB: Never block on the constellation, because sometimes the constellation blocks on us.
@ -303,15 +298,12 @@ impl ServoRenderer {
(item.point_in_viewport + offset).to_untyped(); (item.point_in_viewport + offset).to_untyped();
let external_scroll_id = ExternalScrollId(item.tag.0, item.pipeline); 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,
), ),
cursor,
external_scroll_id, external_scroll_id,
}) })
}) })
@ -322,46 +314,6 @@ impl ServoRenderer {
self.webrender_api self.webrender_api
.send_transaction(self.webrender_document, transaction); .send_transaction(self.webrender_document, transaction);
} }
pub(crate) fn update_cursor_from_hittest(
&mut self,
pos: DevicePoint,
result: &CompositorHitTestResult,
) {
if let Some(webview_id) = self
.pipeline_to_webview_map
.get(&result.pipeline_id)
.copied()
{
self.update_cursor(pos, webview_id, result.cursor);
} else {
warn!("Couldn't update cursor for non-WebView-associated pipeline");
};
}
pub(crate) fn update_cursor(
&mut self,
pos: DevicePoint,
webview_id: WebViewId,
cursor: Option<Cursor>,
) {
self.cursor_pos = pos;
let cursor = match cursor {
Some(cursor) if cursor != self.cursor => cursor,
_ => return,
};
self.cursor = cursor;
if let Err(e) = self
.constellation_sender
.send(EmbedderToConstellationMessage::SetCursor(
webview_id, cursor,
))
{
warn!("Sending event to constellation failed ({:?}).", e);
}
}
} }
impl IOCompositor { impl IOCompositor {
@ -388,8 +340,7 @@ impl IOCompositor {
#[cfg(feature = "webxr")] #[cfg(feature = "webxr")]
webxr_main_thread: state.webxr_main_thread, webxr_main_thread: state.webxr_main_thread,
convert_mouse_to_touch, convert_mouse_to_touch,
cursor: Cursor::None, last_mouse_move_position: None,
cursor_pos: DevicePoint::new(0.0, 0.0),
})), })),
webview_renderers: WebViewManager::default(), webview_renderers: WebViewManager::default(),
needs_repaint: Cell::default(), needs_repaint: Cell::default(),
@ -584,26 +535,7 @@ impl IOCompositor {
}, },
CompositorMsg::NewWebRenderFrameReady(_document_id, recomposite_needed) => { CompositorMsg::NewWebRenderFrameReady(_document_id, recomposite_needed) => {
self.pending_frames -= 1; self.handle_new_webrender_frame_ready(recomposite_needed);
let point: DevicePoint = self.global.borrow().cursor_pos;
if recomposite_needed {
let details_for_pipeline = |pipeline_id| self.details_for_pipeline(pipeline_id);
let result = self
.global
.borrow()
.hit_test_at_point(point, details_for_pipeline);
if let Some(result) = result.first() {
self.global
.borrow_mut()
.update_cursor_from_hittest(point, result);
}
}
if recomposite_needed || self.animation_callbacks_running() {
self.set_needs_repaint(RepaintReason::NewWebRenderFrame);
}
}, },
CompositorMsg::LoadComplete(_) => { CompositorMsg::LoadComplete(_) => {
@ -1665,4 +1597,41 @@ impl IOCompositor {
fn shutdown_state(&self) -> ShutdownState { fn shutdown_state(&self) -> ShutdownState {
self.global.borrow().shutdown_state() self.global.borrow().shutdown_state()
} }
fn refresh_cursor(&self) {
let global = self.global.borrow();
let Some(last_mouse_move_position) = global.last_mouse_move_position else {
return;
};
let details_for_pipeline = |pipeline_id| self.details_for_pipeline(pipeline_id);
let Some(hit_test_result) = global
.hit_test_at_point(last_mouse_move_position, details_for_pipeline)
.first()
.cloned()
else {
return;
};
if let Err(error) =
global
.constellation_sender
.send(EmbedderToConstellationMessage::RefreshCursor(
hit_test_result.pipeline_id,
hit_test_result.point_in_viewport,
))
{
warn!("Sending event to constellation failed ({:?}).", error);
}
}
fn handle_new_webrender_frame_ready(&mut self, recomposite_needed: bool) {
self.pending_frames -= 1;
if recomposite_needed {
self.refresh_cursor();
}
if recomposite_needed || self.animation_callbacks_running() {
self.set_needs_repaint(RepaintReason::NewWebRenderFrame);
}
}
} }

View file

@ -353,14 +353,13 @@ impl WebViewRenderer {
InputEvent::Touch(ref mut touch_event) => { InputEvent::Touch(ref mut touch_event) => {
touch_event.init_sequence_id(self.touch_handler.current_sequence_id); touch_event.init_sequence_id(self.touch_handler.current_sequence_id);
}, },
InputEvent::MouseButton(_) | InputEvent::MouseMove(_) => {
InputEvent::MouseLeave(_) | self.global.borrow_mut().last_mouse_move_position = Some(point);
InputEvent::MouseMove(_) |
InputEvent::Wheel(_) => {
self.global
.borrow_mut()
.update_cursor_from_hittest(point, &result);
}, },
InputEvent::MouseLeave(_) => {
self.global.borrow_mut().last_mouse_move_position = None;
},
InputEvent::MouseButton(_) | InputEvent::Wheel(_) => {},
_ => unreachable!("Unexpected input event type: {event:?}"), _ => unreachable!("Unexpected input event type: {event:?}"),
} }

View file

@ -50,6 +50,7 @@ serde = { workspace = true }
servo_config = { path = "../config" } servo_config = { path = "../config" }
servo_rand = { path = "../rand" } servo_rand = { path = "../rand" }
servo_url = { path = "../url" } servo_url = { path = "../url" }
stylo_traits = { workspace = true }
tracing = { workspace = true, optional = true } tracing = { workspace = true, optional = true }
webgpu = { path = "../webgpu" } webgpu = { path = "../webgpu" }
webgpu_traits = { workspace = true } webgpu_traits = { workspace = true }

View file

@ -130,14 +130,14 @@ use devtools_traits::{
use embedder_traits::resources::{self, Resource}; use embedder_traits::resources::{self, Resource};
use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::user_content_manager::UserContentManager;
use embedder_traits::{ use embedder_traits::{
AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy, FocusId, AnimationState, CompositorHitTestResult, EmbedderMsg, EmbedderProxy, FocusId,
FocusSequenceNumber, InputEvent, JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId, FocusSequenceNumber, InputEvent, JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId,
KeyboardEvent, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState, KeyboardEvent, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState,
MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg, MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg,
WebDriverCommandResponse, WebDriverLoadStatus, WebDriverScriptCommand, WebDriverCommandResponse, WebDriverLoadStatus, WebDriverScriptCommand,
}; };
use euclid::Size2D;
use euclid::default::Size2D as UntypedSize2D; use euclid::default::Size2D as UntypedSize2D;
use euclid::{Point2D, Size2D};
use fonts::SystemFontServiceProxy; use fonts::SystemFontServiceProxy;
use ipc_channel::Error as IpcError; use ipc_channel::Error as IpcError;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
@ -163,6 +163,7 @@ use serde::{Deserialize, Serialize};
use servo_config::{opts, pref}; use servo_config::{opts, pref};
use servo_rand::{Rng, ServoRng, SliceRandom, random}; use servo_rand::{Rng, ServoRng, SliceRandom, random};
use servo_url::{Host, ImmutableOrigin, ServoUrl}; use servo_url::{Host, ImmutableOrigin, ServoUrl};
use style_traits::CSSPixel;
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
use webgpu::swapchain::WGPUImageMap; use webgpu::swapchain::WGPUImageMap;
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
@ -1445,8 +1446,8 @@ where
EmbedderToConstellationMessage::ForwardInputEvent(webview_id, event, hit_test) => { EmbedderToConstellationMessage::ForwardInputEvent(webview_id, event, hit_test) => {
self.forward_input_event(webview_id, event, hit_test); self.forward_input_event(webview_id, event, hit_test);
}, },
EmbedderToConstellationMessage::SetCursor(webview_id, cursor) => { EmbedderToConstellationMessage::RefreshCursor(pipeline_id, point) => {
self.handle_set_cursor_msg(webview_id, cursor) self.handle_refresh_cursor(pipeline_id, point)
}, },
EmbedderToConstellationMessage::ToggleProfiler(rate, max_duration) => { EmbedderToConstellationMessage::ToggleProfiler(rate, max_duration) => {
for background_monitor_control_sender in &self.background_monitor_control_senders { for background_monitor_control_sender in &self.background_monitor_control_senders {
@ -3438,9 +3439,17 @@ where
} }
#[servo_tracing::instrument(skip_all)] #[servo_tracing::instrument(skip_all)]
fn handle_set_cursor_msg(&mut self, webview_id: WebViewId, cursor: Cursor) { fn handle_refresh_cursor(&self, pipeline_id: PipelineId, point: Point2D<f32, CSSPixel>) {
self.embedder_proxy let Some(pipeline) = self.pipelines.get(&pipeline_id) else {
.send(EmbedderMsg::SetCursor(webview_id, cursor)); return;
};
if let Err(error) = pipeline
.event_loop
.send(ScriptThreadMessage::RefreshCursor(pipeline_id, point))
{
warn!("Could not send RefreshCursor message to pipeline: {error:?}");
}
} }
#[servo_tracing::instrument(skip_all)] #[servo_tracing::instrument(skip_all)]

View file

@ -67,7 +67,7 @@ mod from_compositor {
Self::FocusWebView(..) => target!("FocusWebView"), Self::FocusWebView(..) => target!("FocusWebView"),
Self::BlurWebView => target!("BlurWebView"), Self::BlurWebView => target!("BlurWebView"),
Self::ForwardInputEvent(_webview_id, event, ..) => event.log_target(), Self::ForwardInputEvent(_webview_id, event, ..) => event.log_target(),
Self::SetCursor(..) => target!("SetCursor"), Self::RefreshCursor(..) => target!("RefreshCursor"),
Self::ToggleProfiler(..) => target!("EnableProfiler"), Self::ToggleProfiler(..) => target!("EnableProfiler"),
Self::ExitFullScreen(_) => target!("ExitFullScreen"), Self::ExitFullScreen(_) => target!("ExitFullScreen"),
Self::MediaSessionAction(_) => target!("MediaSessionAction"), Self::MediaSessionAction(_) => target!("MediaSessionAction"),

View file

@ -6,6 +6,7 @@ use std::collections::HashMap;
use app_units::Au; use app_units::Au;
use base::id::ScrollTreeNodeId; use base::id::ScrollTreeNodeId;
use embedder_traits::Cursor;
use euclid::{Box2D, Point2D, Point3D, Vector2D}; use euclid::{Box2D, Point2D, Point3D, Vector2D};
use kurbo::{Ellipse, Shape}; use kurbo::{Ellipse, Shape};
use layout_api::{ElementsFromPointFlags, ElementsFromPointResult}; use layout_api::{ElementsFromPointFlags, ElementsFromPointResult};
@ -13,6 +14,7 @@ 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 style::properties::ComputedValues;
use style::values::computed::ui::CursorKind;
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};
@ -21,7 +23,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::Fragment; use crate::fragment_tree::{Fragment, FragmentFlags};
use crate::geom::PhysicalRect; use crate::geom::PhysicalRect;
pub(crate) struct HitTest<'a> { pub(crate) struct HitTest<'a> {
@ -249,12 +251,18 @@ impl Fragment {
let mut hit_test_fragment_inner = let mut hit_test_fragment_inner =
|style: &ComputedValues, |style: &ComputedValues,
fragment_rect: PhysicalRect<Au>, fragment_rect: PhysicalRect<Au>,
border_radius: BorderRadius| { border_radius: BorderRadius,
if style.get_inherited_ui().pointer_events == PointerEvents::None { fragment_flags: FragmentFlags,
return false; auto_cursor: Cursor| {
} let is_root_element = fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT);
if style.get_inherited_box().visibility != Visibility::Visible {
return false; if !is_root_element {
if style.get_inherited_ui().pointer_events == PointerEvents::None {
return false;
}
if style.get_inherited_box().visibility != Visibility::Visible {
return false;
}
} }
let (point_in_spatial_node, transform) = let (point_in_spatial_node, transform) =
@ -263,14 +271,28 @@ impl Fragment {
None => return false, None => return false,
}; };
if style.get_box().backface_visibility == BackfaceVisibility::Hidden && if !is_root_element &&
style.get_box().backface_visibility == BackfaceVisibility::Hidden &&
transform.is_backface_visible() transform.is_backface_visible()
{ {
return false; return false;
} }
let fragment_rect = fragment_rect.translate(containing_block.origin.to_vector()); let fragment_rect = fragment_rect.translate(containing_block.origin.to_vector());
if !rounded_rect_contains_point( if is_root_element {
let viewport_size = hit_test
.stacking_context_tree
.compositor_info
.viewport_details
.size;
let viewport_rect = LayoutRect::from_origin_and_size(
Default::default(),
viewport_size.cast_unit(),
);
if !viewport_rect.contains(hit_test.point_to_test) {
return false;
}
} else if !rounded_rect_contains_point(
fragment_rect.to_webrender(), fragment_rect.to_webrender(),
&border_radius, &border_radius,
point_in_spatial_node, point_in_spatial_node,
@ -283,11 +305,12 @@ impl Fragment {
fragment_rect.origin.x.to_f32_px(), fragment_rect.origin.x.to_f32_px(),
fragment_rect.origin.y.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,
cursor: cursor(style.get_inherited_ui().cursor.keyword, auto_cursor),
}); });
!hit_test.flags.contains(ElementsFromPointFlags::FindAll) !hit_test.flags.contains(ElementsFromPointFlags::FindAll)
}; };
@ -298,6 +321,8 @@ impl Fragment {
&box_fragment.style, &box_fragment.style,
box_fragment.border_rect(), box_fragment.border_rect(),
box_fragment.border_radius(), box_fragment.border_radius(),
box_fragment.base.flags,
Cursor::Default,
) )
}, },
Fragment::Text(text) => { Fragment::Text(text) => {
@ -306,6 +331,8 @@ impl Fragment {
&text.inline_styles.style.borrow(), &text.inline_styles.style.borrow(),
text.rect, text.rect,
BorderRadius::zero(), BorderRadius::zero(),
FragmentFlags::empty(),
Cursor::Text,
) )
}, },
_ => false, _ => false,
@ -358,3 +385,44 @@ fn rounded_rect_contains_point(
check_corner(rect.bottom_right(), &border_radius.bottom_right, true, true) && check_corner(rect.bottom_right(), &border_radius.bottom_right, true, true) &&
check_corner(rect.bottom_left(), &border_radius.bottom_left, false, true) check_corner(rect.bottom_left(), &border_radius.bottom_left, false, true)
} }
fn cursor(kind: CursorKind, auto_cursor: Cursor) -> Cursor {
match kind {
CursorKind::Auto => auto_cursor,
CursorKind::None => Cursor::None,
CursorKind::Default => Cursor::Default,
CursorKind::Pointer => Cursor::Pointer,
CursorKind::ContextMenu => Cursor::ContextMenu,
CursorKind::Help => Cursor::Help,
CursorKind::Progress => Cursor::Progress,
CursorKind::Wait => Cursor::Wait,
CursorKind::Cell => Cursor::Cell,
CursorKind::Crosshair => Cursor::Crosshair,
CursorKind::Text => Cursor::Text,
CursorKind::VerticalText => Cursor::VerticalText,
CursorKind::Alias => Cursor::Alias,
CursorKind::Copy => Cursor::Copy,
CursorKind::Move => Cursor::Move,
CursorKind::NoDrop => Cursor::NoDrop,
CursorKind::NotAllowed => Cursor::NotAllowed,
CursorKind::Grab => Cursor::Grab,
CursorKind::Grabbing => Cursor::Grabbing,
CursorKind::EResize => Cursor::EResize,
CursorKind::NResize => Cursor::NResize,
CursorKind::NeResize => Cursor::NeResize,
CursorKind::NwResize => Cursor::NwResize,
CursorKind::SResize => Cursor::SResize,
CursorKind::SeResize => Cursor::SeResize,
CursorKind::SwResize => Cursor::SwResize,
CursorKind::WResize => Cursor::WResize,
CursorKind::EwResize => Cursor::EwResize,
CursorKind::NsResize => Cursor::NsResize,
CursorKind::NeswResize => Cursor::NeswResize,
CursorKind::NwseResize => Cursor::NwseResize,
CursorKind::ColResize => Cursor::ColResize,
CursorKind::RowResize => Cursor::RowResize,
CursorKind::AllScroll => Cursor::AllScroll,
CursorKind::ZoomIn => Cursor::ZoomIn,
CursorKind::ZoomOut => Cursor::ZoomOut,
}
}

View file

@ -9,7 +9,6 @@ use app_units::{AU_PER_PX, Au};
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};
use embedder_traits::Cursor;
use euclid::{Point2D, Scale, SideOffsets2D, Size2D, UnknownUnit, Vector2D}; use euclid::{Point2D, Scale, SideOffsets2D, Size2D, UnknownUnit, Vector2D};
use fonts::GlyphStore; use fonts::GlyphStore;
use gradient::WebRenderGradient; use gradient::WebRenderGradient;
@ -36,13 +35,12 @@ use style::values::computed::{
use style::values::generics::NonNegative; use style::values::generics::NonNegative;
use style::values::generics::rect::Rect; use style::values::generics::rect::Rect;
use style::values::specified::text::TextDecorationLine; use style::values::specified::text::TextDecorationLine;
use style::values::specified::ui::CursorKind;
use style_traits::{CSSPixel as StyloCSSPixel, DevicePixel as StyloDevicePixel}; use style_traits::{CSSPixel as StyloCSSPixel, DevicePixel as StyloDevicePixel};
use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel, LayoutRect, LayoutSize}; use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel, LayoutRect, LayoutSize};
use webrender_api::{ use webrender_api::{
self as wr, BorderDetails, BorderRadius, BoxShadowClipMode, BuiltDisplayList, ClipChainId, self as wr, BorderDetails, BorderRadius, BoxShadowClipMode, BuiltDisplayList, ClipChainId,
ClipMode, CommonItemProperties, ComplexClipRegion, NinePatchBorder, NinePatchBorderSource, ClipMode, CommonItemProperties, ComplexClipRegion, NinePatchBorder, NinePatchBorderSource,
PropertyBinding, SpatialId, SpatialTreeItemKey, units, PrimitiveFlags, PropertyBinding, SpatialId, SpatialTreeItemKey, units,
}; };
use wr::units::LayoutVector2D; use wr::units::LayoutVector2D;
@ -164,8 +162,9 @@ 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;
let pipeline_id = compositor_info.pipeline_id;
let mut webrender_display_list_builder = let mut webrender_display_list_builder =
webrender_api::DisplayListBuilder::new(compositor_info.pipeline_id); webrender_api::DisplayListBuilder::new(pipeline_id);
webrender_display_list_builder.begin(); webrender_display_list_builder.begin();
// `dump_serialized_display_list` doesn't actually print anything. It sets up // `dump_serialized_display_list` doesn't actually print anything. It sets up
@ -200,6 +199,8 @@ impl DisplayListBuilder<'_> {
builder.add_clip_to_display_list(clip); builder.add_clip_to_display_list(clip);
} }
builder.push_hit_tests_for_scrollable_areas(&stacking_context_tree.hit_test_items);
// Paint the canvas background (if any) before/under everything else // Paint the canvas background (if any) before/under everything else
stacking_context_tree stacking_context_tree
.root_stacking_context .root_stacking_context
@ -309,6 +310,39 @@ impl DisplayListBuilder<'_> {
self.compositor_info.scroll_tree = scroll_tree; self.compositor_info.scroll_tree = scroll_tree;
} }
fn push_hit_tests_for_scrollable_areas(
&mut self,
scroll_frame_hit_test_items: &[ScrollFrameHitTestItem],
) {
// Add a single hit test that covers the entire viewport, so that WebRender knows
// which pipeline it hits when doing hit testing.
let pipeline_id = self.compositor_info.pipeline_id;
let viewport_size = self.compositor_info.viewport_details.size;
let viewport_rect = LayoutRect::from_size(viewport_size.cast_unit());
self.wr().push_hit_test(
viewport_rect,
ClipChainId::INVALID,
SpatialId::root_reference_frame(pipeline_id),
PrimitiveFlags::default(),
(0, 0), /* tag */
);
for item in scroll_frame_hit_test_items {
let spatial_id = self
.compositor_info
.scroll_tree
.webrender_id(&item.scroll_node_id);
let clip_chain_id = self.clip_chain_id(item.clip_id);
self.wr().push_hit_test(
item.rect,
clip_chain_id,
spatial_id,
PrimitiveFlags::default(),
(item.external_scroll_id.0, 0), /* tag */
);
}
}
/// Add the given [`Clip`] to the WebRender display list and create a mapping from /// Add the given [`Clip`] to the WebRender display list and create a mapping from
/// its [`ClipId`] to a WebRender [`ClipChainId`]. This happens: /// its [`ClipId`] to a WebRender [`ClipChainId`]. This happens:
/// - When WebRender display list construction starts: All clips created during the /// - When WebRender display list construction starts: All clips created during the
@ -548,7 +582,6 @@ impl Fragment {
builder: &mut DisplayListBuilder, builder: &mut DisplayListBuilder,
containing_block: &PhysicalRect<Au>, containing_block: &PhysicalRect<Au>,
section: StackingContextSection, section: StackingContextSection,
is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool, is_collapsed_table_borders: bool,
text_decorations: &Arc<Vec<FragmentTextDecoration>>, text_decorations: &Arc<Vec<FragmentTextDecoration>>,
) { ) {
@ -572,7 +605,6 @@ impl Fragment {
Visibility::Visible => BuilderForBoxFragment::new( Visibility::Visible => BuilderForBoxFragment::new(
box_fragment, box_fragment,
containing_block, containing_block,
is_hit_test_for_scrollable_overflow,
is_collapsed_table_borders, is_collapsed_table_borders,
) )
.build(builder, section), .build(builder, section),
@ -580,19 +612,7 @@ impl Fragment {
Visibility::Collapse => (), Visibility::Collapse => (),
} }
}, },
Fragment::AbsoluteOrFixedPositioned(_) => {}, Fragment::AbsoluteOrFixedPositioned(_) | Fragment::Positioning(_) => {},
Fragment::Positioning(positioning_fragment) => {
let positioning_fragment = positioning_fragment.borrow();
let rect = positioning_fragment
.rect
.translate(containing_block.origin.to_vector());
self.maybe_push_hit_test_for_style_and_tag(
builder,
&positioning_fragment.style,
rect,
Cursor::Default,
);
},
Fragment::Image(image) => { Fragment::Image(image) => {
let image = image.borrow(); let image = image.borrow();
match image.style.get_inherited_box().visibility { match image.style.get_inherited_box().visibility {
@ -674,27 +694,6 @@ impl Fragment {
} }
} }
fn maybe_push_hit_test_for_style_and_tag(
&self,
builder: &mut DisplayListBuilder,
style: &ComputedValues,
rect: PhysicalRect<Au>,
cursor: Cursor,
) {
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(),
(external_scroll_id.0, cursor as u16), /* tag */
);
}
fn build_display_list_for_text_fragment( fn build_display_list_for_text_fragment(
&self, &self,
fragment: &TextFragment, fragment: &TextFragment,
@ -724,8 +723,6 @@ 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(builder, &parent_style, 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;
let dppx = builder.device_pixel_ratio.get(); let dppx = builder.device_pixel_ratio.get();
@ -939,7 +936,6 @@ struct BuilderForBoxFragment<'a> {
border_edge_clip_chain_id: RefCell<Option<ClipChainId>>, border_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
padding_edge_clip_chain_id: RefCell<Option<ClipChainId>>, padding_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
content_edge_clip_chain_id: RefCell<Option<ClipChainId>>, content_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool, is_collapsed_table_borders: bool,
} }
@ -947,7 +943,6 @@ impl<'a> BuilderForBoxFragment<'a> {
fn new( fn new(
fragment: &'a BoxFragment, fragment: &'a BoxFragment,
containing_block: &'a PhysicalRect<Au>, containing_block: &'a PhysicalRect<Au>,
is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool, is_collapsed_table_borders: bool,
) -> Self { ) -> Self {
let border_rect = fragment let border_rect = fragment
@ -964,7 +959,6 @@ impl<'a> BuilderForBoxFragment<'a> {
border_edge_clip_chain_id: RefCell::new(None), border_edge_clip_chain_id: RefCell::new(None),
padding_edge_clip_chain_id: RefCell::new(None), padding_edge_clip_chain_id: RefCell::new(None),
content_edge_clip_chain_id: RefCell::new(None), content_edge_clip_chain_id: RefCell::new(None),
is_hit_test_for_scrollable_overflow,
is_collapsed_table_borders, is_collapsed_table_borders,
} }
} }
@ -1047,11 +1041,6 @@ impl<'a> BuilderForBoxFragment<'a> {
} }
fn build(&mut self, builder: &mut DisplayListBuilder, section: StackingContextSection) { fn build(&mut self, builder: &mut DisplayListBuilder, section: StackingContextSection) {
if self.is_hit_test_for_scrollable_overflow {
self.build_hit_test(builder, self.fragment.scrollable_overflow().to_webrender());
return;
}
if self.is_collapsed_table_borders { if self.is_collapsed_table_borders {
self.build_collapsed_table_borders(builder); self.build_collapsed_table_borders(builder);
return; return;
@ -1062,7 +1051,6 @@ impl<'a> BuilderForBoxFragment<'a> {
return; return;
} }
self.build_hit_test(builder, self.border_rect);
if self if self
.fragment .fragment
.base .base
@ -1077,28 +1065,6 @@ impl<'a> BuilderForBoxFragment<'a> {
self.build_border(builder); self.build_border(builder);
} }
fn build_hit_test(&self, builder: &mut DisplayListBuilder, rect: LayoutRect) {
let cursor = cursor(
self.fragment.style.get_inherited_ui().cursor.keyword,
Cursor::Default,
);
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) {
common.clip_chain_id = clip_chain_id;
}
builder.wr().push_hit_test(
common.clip_rect,
common.clip_chain_id,
common.spatial_id,
common.flags,
(external_scroll_node_id.0, cursor as u16), /* tag */
);
}
fn build_background_for_painter( fn build_background_for_painter(
&mut self, &mut self,
builder: &mut DisplayListBuilder, builder: &mut DisplayListBuilder,
@ -1623,47 +1589,6 @@ fn glyphs_advance_by_index(
point point
} }
fn cursor(kind: CursorKind, auto_cursor: Cursor) -> Cursor {
match kind {
CursorKind::Auto => auto_cursor,
CursorKind::None => Cursor::None,
CursorKind::Default => Cursor::Default,
CursorKind::Pointer => Cursor::Pointer,
CursorKind::ContextMenu => Cursor::ContextMenu,
CursorKind::Help => Cursor::Help,
CursorKind::Progress => Cursor::Progress,
CursorKind::Wait => Cursor::Wait,
CursorKind::Cell => Cursor::Cell,
CursorKind::Crosshair => Cursor::Crosshair,
CursorKind::Text => Cursor::Text,
CursorKind::VerticalText => Cursor::VerticalText,
CursorKind::Alias => Cursor::Alias,
CursorKind::Copy => Cursor::Copy,
CursorKind::Move => Cursor::Move,
CursorKind::NoDrop => Cursor::NoDrop,
CursorKind::NotAllowed => Cursor::NotAllowed,
CursorKind::Grab => Cursor::Grab,
CursorKind::Grabbing => Cursor::Grabbing,
CursorKind::EResize => Cursor::EResize,
CursorKind::NResize => Cursor::NResize,
CursorKind::NeResize => Cursor::NeResize,
CursorKind::NwResize => Cursor::NwResize,
CursorKind::SResize => Cursor::SResize,
CursorKind::SeResize => Cursor::SeResize,
CursorKind::SwResize => Cursor::SwResize,
CursorKind::WResize => Cursor::WResize,
CursorKind::EwResize => Cursor::EwResize,
CursorKind::NsResize => Cursor::NsResize,
CursorKind::NeswResize => Cursor::NeswResize,
CursorKind::NwseResize => Cursor::NwseResize,
CursorKind::ColResize => Cursor::ColResize,
CursorKind::RowResize => Cursor::RowResize,
CursorKind::AllScroll => Cursor::AllScroll,
CursorKind::ZoomIn => Cursor::ZoomIn,
CursorKind::ZoomOut => Cursor::ZoomOut,
}
}
/// Radii for the padding edge or content edge /// Radii for the padding edge or content edge
fn inner_radii(mut radii: wr::BorderRadius, insets: units::LayoutSideOffsets) -> wr::BorderRadius { fn inner_radii(mut radii: wr::BorderRadius, insets: units::LayoutSideOffsets) -> wr::BorderRadius {
assert!(insets.left >= 0.0, "left inset must not be negative"); assert!(insets.left >= 0.0, "left inset must not be negative");

View file

@ -33,7 +33,7 @@ use style::values::generics::box_::Perspective;
use style::values::generics::transform::{self, GenericRotate, GenericScale, GenericTranslate}; use style::values::generics::transform::{self, GenericRotate, GenericScale, GenericTranslate};
use style::values::specified::box_::DisplayOutside; use style::values::specified::box_::DisplayOutside;
use webrender_api::units::{LayoutPoint, LayoutRect, LayoutTransform, LayoutVector2D}; use webrender_api::units::{LayoutPoint, LayoutRect, LayoutTransform, LayoutVector2D};
use webrender_api::{self as wr, BorderRadius}; use webrender_api::{self as wr, BorderRadius, ExternalScrollId};
use wr::StickyOffsetBounds; use wr::StickyOffsetBounds;
use wr::units::{LayoutPixel, LayoutSize}; use wr::units::{LayoutPixel, LayoutSize};
@ -100,6 +100,22 @@ pub(crate) enum StackingContextSection {
Outline, Outline,
} }
pub(crate) struct ScrollFrameHitTestItem {
/// The [`ScrollTreeNodeId`] of the spatial node that contains this hit test item.
pub scroll_node_id: ScrollTreeNodeId,
/// The [`ClipId`] of the clip that clips this [`ScrollFrameHitTestItems`].
pub clip_id: ClipId,
/// The rectangle of the scroll frame in the coordinate space of [`Self::scroll_node_id`].
pub rect: LayoutRect,
/// The WebRender [`ExternalScrollId`] of the scrolling spatial node that
/// this [`ScrollFrameHitTestItem`] identifies. Note that this is a *different*
/// spatial node than the one identified by [`Self::scroll_node_id`] (the parent).
pub external_scroll_id: ExternalScrollId,
}
pub(crate) struct StackingContextTree { pub(crate) struct StackingContextTree {
/// The root stacking context of this [`StackingContextTree`]. /// The root stacking context of this [`StackingContextTree`].
pub root_stacking_context: StackingContext, pub root_stacking_context: StackingContext,
@ -114,6 +130,10 @@ pub(crate) struct StackingContextTree {
/// for things like `overflow`. More clips may be created later during WebRender /// for things like `overflow`. More clips may be created later during WebRender
/// display list construction, but they are never added here. /// display list construction, but they are never added here.
pub clip_store: StackingContextTreeClipStore, pub clip_store: StackingContextTreeClipStore,
/// A vector of hit test items, one per scroll frame. These are used for allowing
/// renderer-side scrolling in the Servo renderer.
pub hit_test_items: Vec<ScrollFrameHitTestItem>,
} }
impl StackingContextTree { impl StackingContextTree {
@ -174,6 +194,7 @@ impl StackingContextTree {
root_stacking_context: StackingContext::create_root(root_scroll_node_id, debug), root_stacking_context: StackingContext::create_root(root_scroll_node_id, debug),
compositor_info, compositor_info,
clip_store: Default::default(), clip_store: Default::default(),
hit_test_items: Vec::new(),
}; };
let mut root_stacking_context = StackingContext::create_root(root_scroll_node_id, debug); let mut root_stacking_context = StackingContext::create_root(root_scroll_node_id, debug);
@ -281,7 +302,6 @@ pub(crate) enum StackingContextContent {
section: StackingContextSection, section: StackingContextSection,
containing_block: PhysicalRect<Au>, containing_block: PhysicalRect<Au>,
fragment: Fragment, fragment: Fragment,
is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool, is_collapsed_table_borders: bool,
text_decorations: Arc<Vec<FragmentTextDecoration>>, text_decorations: Arc<Vec<FragmentTextDecoration>>,
}, },
@ -313,7 +333,6 @@ impl StackingContextContent {
section, section,
containing_block, containing_block,
fragment, fragment,
is_hit_test_for_scrollable_overflow,
is_collapsed_table_borders, is_collapsed_table_borders,
text_decorations, text_decorations,
} => { } => {
@ -324,7 +343,6 @@ impl StackingContextContent {
builder, builder,
containing_block, containing_block,
*section, *section,
*is_hit_test_for_scrollable_overflow,
*is_collapsed_table_borders, *is_collapsed_table_borders,
text_decorations, text_decorations,
); );
@ -640,7 +658,6 @@ impl StackingContext {
let mut fragment_builder = BuilderForBoxFragment::new( let mut fragment_builder = BuilderForBoxFragment::new(
&root_fragment, &root_fragment,
&fragment_tree.initial_containing_block, &fragment_tree.initial_containing_block,
false, /* is_hit_test_for_scrollable_overflow */
false, /* is_collapsed_table_borders */ false, /* is_collapsed_table_borders */
); );
let painter = super::background::BackgroundPainter { let painter = super::background::BackgroundPainter {
@ -888,7 +905,6 @@ impl Fragment {
clip_id: containing_block.clip_id, clip_id: containing_block.clip_id,
containing_block: containing_block.rect, containing_block: containing_block.rect,
fragment: fragment_clone, fragment: fragment_clone,
is_hit_test_for_scrollable_overflow: false,
is_collapsed_table_borders: false, is_collapsed_table_borders: false,
text_decorations: text_decorations.clone(), text_decorations: text_decorations.clone(),
}); });
@ -1088,7 +1104,6 @@ impl BoxFragment {
BuilderForBoxFragment::new( BuilderForBoxFragment::new(
self, self,
&containing_block.rect, &containing_block.rect,
false, /* is_hit_test_for_scrollable_overflow */
false, /* is_collapsed_table_borders */ false, /* is_collapsed_table_borders */
), ),
) )
@ -1172,7 +1187,6 @@ impl BoxFragment {
BuilderForBoxFragment::new( BuilderForBoxFragment::new(
self, self,
&containing_block.rect, &containing_block.rect,
false, /* is_hit_test_for_scrollable_overflow*/
false, /* is_collapsed_table_borders */ false, /* is_collapsed_table_borders */
), ),
) { ) {
@ -1205,7 +1219,6 @@ impl BoxFragment {
section, section,
containing_block: containing_block.rect, containing_block: containing_block.rect,
fragment: fragment.clone(), fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: false,
is_collapsed_table_borders: false, is_collapsed_table_borders: false,
text_decorations: text_decorations.clone(), text_decorations: text_decorations.clone(),
}); });
@ -1234,21 +1247,6 @@ impl BoxFragment {
if let Some(scroll_frame_data) = overflow_frame_data.scroll_frame_data { if let Some(scroll_frame_data) = overflow_frame_data.scroll_frame_data {
new_scroll_node_id = scroll_frame_data.scroll_tree_node_id; new_scroll_node_id = scroll_frame_data.scroll_tree_node_id;
new_scroll_frame_size = Some(scroll_frame_data.scroll_frame_rect.size()); new_scroll_frame_size = Some(scroll_frame_data.scroll_frame_rect.size());
stacking_context
.contents
.push(StackingContextContent::Fragment {
scroll_node_id: new_scroll_node_id,
reference_frame_scroll_node_id:
reference_frame_scroll_node_id_for_fragments,
clip_id: new_clip_id,
section,
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: true,
is_collapsed_table_borders: false,
text_decorations: text_decorations.clone(),
});
} }
} }
@ -1345,7 +1343,6 @@ impl BoxFragment {
section, section,
containing_block: containing_block.rect, containing_block: containing_block.rect,
fragment: fragment.clone(), fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: false,
is_collapsed_table_borders: true, is_collapsed_table_borders: true,
text_decorations: text_decorations.clone(), text_decorations: text_decorations.clone(),
}); });
@ -1417,7 +1414,7 @@ impl BoxFragment {
// https://drafts.csswg.org/css-overflow-3/#corner-clipping // https://drafts.csswg.org/css-overflow-3/#corner-clipping
let radii; let radii;
if overflow.x == ComputedOverflow::Clip && overflow.y == ComputedOverflow::Clip { if overflow.x == ComputedOverflow::Clip && overflow.y == ComputedOverflow::Clip {
let builder = BuilderForBoxFragment::new(self, containing_block_rect, false, false); let builder = BuilderForBoxFragment::new(self, containing_block_rect, false);
radii = offset_radii(builder.border_radius, clip_margin); radii = offset_radii(builder.border_radius, clip_margin);
} else if overflow.x != ComputedOverflow::Clip { } else if overflow.x != ComputedOverflow::Clip {
overflow_clip_rect.min.x = f32::MIN; overflow_clip_rect.min.x = f32::MIN;
@ -1467,14 +1464,14 @@ impl BoxFragment {
.to_webrender(); .to_webrender();
let clip_id = stacking_context_tree.clip_store.add( let clip_id = stacking_context_tree.clip_store.add(
BuilderForBoxFragment::new(self, containing_block_rect, false, false).border_radius, BuilderForBoxFragment::new(self, containing_block_rect, false).border_radius,
scroll_frame_rect, scroll_frame_rect,
*parent_scroll_node_id, *parent_scroll_node_id,
parent_clip_id, parent_clip_id,
); );
let tag = self.base.tag?; let tag = self.base.tag?;
let external_id = wr::ExternalScrollId( let external_scroll_id = wr::ExternalScrollId(
tag.to_display_list_fragment_id(), tag.to_display_list_fragment_id(),
stacking_context_tree.compositor_info.pipeline_id, stacking_context_tree.compositor_info.pipeline_id,
); );
@ -1486,12 +1483,21 @@ impl BoxFragment {
let scroll_tree_node_id = stacking_context_tree.define_scroll_frame( let scroll_tree_node_id = stacking_context_tree.define_scroll_frame(
parent_scroll_node_id, parent_scroll_node_id,
external_id, external_scroll_id,
self.scrollable_overflow().to_webrender(), self.scrollable_overflow().to_webrender(),
scroll_frame_rect, scroll_frame_rect,
sensitivity, sensitivity,
); );
stacking_context_tree
.hit_test_items
.push(ScrollFrameHitTestItem {
scroll_node_id: *parent_scroll_node_id,
clip_id,
rect: scroll_frame_rect,
external_scroll_id,
});
Some(OverflowFrameData { Some(OverflowFrameData {
clip_id, clip_id,
scroll_frame_data: Some(ScrollFrameData { scroll_frame_data: Some(ScrollFrameData {

View file

@ -28,7 +28,7 @@ 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, ContextMenuResult, EditingActionEvent, EmbedderMsg, AllowOrDeny, AnimationState, ContextMenuResult, Cursor, EditingActionEvent, EmbedderMsg,
FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction, FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction,
MouseButtonEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId, UntrustedNodeAddress, MouseButtonEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId, UntrustedNodeAddress,
WheelEvent, WheelEvent,
@ -2008,6 +2008,9 @@ impl Document {
return; return;
}; };
// Update the cursor when the mouse moves, if it has changed.
self.set_cursor(hit_test_result.cursor);
let Some(new_target) = hit_test_result let Some(new_target) = hit_test_result
.node .node
.inclusive_ancestors(ShadowIncluding::No) .inclusive_ancestors(ShadowIncluding::No)
@ -2122,6 +2125,10 @@ impl Document {
} }
} }
pub(crate) fn set_cursor(&self, cursor: Cursor) {
self.send_to_embedder(EmbedderMsg::SetCursor(self.webview_id(), cursor));
}
#[allow(unsafe_code)] #[allow(unsafe_code)]
pub(crate) fn handle_mouse_leave_event( pub(crate) fn handle_mouse_leave_event(
&self, &self,

View file

@ -3,6 +3,7 @@
* 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 embedder_traits::Cursor;
use euclid::Point2D; use euclid::Point2D;
use js::rust::HandleObject; use js::rust::HandleObject;
use style_traits::CSSPixel; use style_traits::CSSPixel;
@ -99,6 +100,7 @@ impl InputEventMethods<crate::DomTypeHolder> for InputEvent {
/// `CompositorHitTestResult` against our current layout. /// `CompositorHitTestResult` against our current layout.
pub(crate) struct HitTestResult { pub(crate) struct HitTestResult {
pub node: DomRoot<Node>, pub node: DomRoot<Node>,
pub cursor: Cursor,
pub point_in_node: Point2D<f32, CSSPixel>, pub point_in_node: Point2D<f32, CSSPixel>,
pub point_in_frame: Point2D<f32, CSSPixel>, pub point_in_frame: Point2D<f32, CSSPixel>,
pub point_relative_to_initial_containing_block: Point2D<f32, CSSPixel>, pub point_relative_to_initial_containing_block: Point2D<f32, CSSPixel>,

View file

@ -2620,6 +2620,7 @@ impl Window {
let address = UntrustedNodeAddress(result.node.0 as *const c_void); let address = UntrustedNodeAddress(result.node.0 as *const c_void);
Some(HitTestResult { Some(HitTestResult {
node: unsafe { from_untrusted_node_address(address) }, node: unsafe { from_untrusted_node_address(address) },
cursor: result.cursor,
point_in_node: result.point_in_target, point_in_node: result.point_in_target,
point_in_frame: compositor_hit_test_result.point_in_viewport, 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: compositor_hit_test_result
@ -3089,6 +3090,20 @@ impl Window {
} }
} }
} }
pub fn handle_refresh_cursor(&self, cursor_position: Point2D<f32, CSSPixel>) {
let layout = self.layout.borrow();
layout.ensure_stacking_context_tree(self.viewport_details.get());
let Some(hit_test_result) = layout
.query_elements_from_point(cursor_position.cast_unit(), ElementsFromPointFlags::empty())
.into_iter()
.nth(0)
else {
return;
};
self.Document().set_cursor(hit_test_result.cursor);
}
} }
impl Window { impl Window {

View file

@ -62,6 +62,7 @@ impl MixedMessage {
ScriptThreadMessage::ExitPipeline(_webview_id, id, ..) => Some(*id), ScriptThreadMessage::ExitPipeline(_webview_id, id, ..) => Some(*id),
ScriptThreadMessage::ExitScriptThread => None, ScriptThreadMessage::ExitScriptThread => None,
ScriptThreadMessage::SendInputEvent(id, ..) => Some(*id), ScriptThreadMessage::SendInputEvent(id, ..) => Some(*id),
ScriptThreadMessage::RefreshCursor(id, ..) => Some(*id),
ScriptThreadMessage::Viewport(id, ..) => Some(*id), ScriptThreadMessage::Viewport(id, ..) => Some(*id),
ScriptThreadMessage::GetTitle(id) => Some(*id), ScriptThreadMessage::GetTitle(id) => Some(*id),
ScriptThreadMessage::SetDocumentActivity(id, ..) => Some(*id), ScriptThreadMessage::SetDocumentActivity(id, ..) => Some(*id),

View file

@ -92,6 +92,7 @@ use script_traits::{
use servo_config::opts; use servo_config::opts;
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
use style::thread_state::{self, ThreadState}; use style::thread_state::{self, ThreadState};
use style_traits::CSSPixel;
use stylo_atoms::Atom; use stylo_atoms::Atom;
use timers::{TimerEventRequest, TimerId, TimerScheduler}; use timers::{TimerEventRequest, TimerId, TimerScheduler};
use url::Position; use url::Position;
@ -2042,6 +2043,9 @@ impl ScriptThread {
); );
} }
}, },
ScriptThreadMessage::RefreshCursor(pipeline_id, cursor_position) => {
self.handle_refresh_cursor(pipeline_id, cursor_position);
},
} }
} }
@ -4106,6 +4110,17 @@ impl ScriptThread {
ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result), ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result),
)); ));
} }
fn handle_refresh_cursor(
&self,
pipeline_id: PipelineId,
cursor_position: Point2D<f32, CSSPixel>,
) {
let Some(window) = self.documents.borrow().find_window(pipeline_id) else {
return;
};
window.handle_refresh_cursor(cursor_position);
}
} }
impl Drop for ScriptThread { impl Drop for ScriptThread {

View file

@ -34,6 +34,7 @@ serde = { workspace = true }
servo_url = { path = "../../url" } servo_url = { path = "../../url" }
strum = { workspace = true } strum = { workspace = true }
strum_macros = { workspace = true } strum_macros = { workspace = true }
stylo_traits = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
webgpu_traits = { workspace = true } webgpu_traits = { workspace = true }
webrender_api = { workspace = true } webrender_api = { workspace = true }

View file

@ -19,10 +19,10 @@ use base::Epoch;
use base::cross_process_instant::CrossProcessInstant; use base::cross_process_instant::CrossProcessInstant;
use base::id::{MessagePortId, PipelineId, WebViewId}; use base::id::{MessagePortId, PipelineId, WebViewId};
use embedder_traits::{ use embedder_traits::{
CompositorHitTestResult, Cursor, FocusId, InputEvent, JavaScriptEvaluationId, CompositorHitTestResult, FocusId, InputEvent, JavaScriptEvaluationId, MediaSessionActionType,
MediaSessionActionType, Theme, TraversalId, ViewportDetails, WebDriverCommandMsg, Theme, TraversalId, ViewportDetails, WebDriverCommandMsg, WebDriverCommandResponse,
WebDriverCommandResponse,
}; };
use euclid::Point2D;
pub use from_script_message::*; pub use from_script_message::*;
use ipc_channel::ipc::IpcSender; use ipc_channel::ipc::IpcSender;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
@ -31,6 +31,7 @@ use serde::{Deserialize, Serialize};
use servo_url::{ImmutableOrigin, ServoUrl}; use servo_url::{ImmutableOrigin, ServoUrl};
pub use structured_data::*; pub use structured_data::*;
use strum_macros::IntoStaticStr; use strum_macros::IntoStaticStr;
use style_traits::CSSPixel;
use webrender_api::units::LayoutVector2D; use webrender_api::units::LayoutVector2D;
use webrender_api::{ExternalScrollId, ImageKey}; use webrender_api::{ExternalScrollId, ImageKey};
@ -76,8 +77,9 @@ pub enum EmbedderToConstellationMessage {
BlurWebView, BlurWebView,
/// Forward an input event to an appropriate ScriptTask. /// Forward an input event to an appropriate ScriptTask.
ForwardInputEvent(WebViewId, InputEvent, Option<CompositorHitTestResult>), ForwardInputEvent(WebViewId, InputEvent, Option<CompositorHitTestResult>),
/// Requesting a change to the onscreen cursor. /// Request that the given pipeline do a hit test at the location and reset the
SetCursor(WebViewId, Cursor), /// cursor accordingly. This happens after a display list update is rendered.
RefreshCursor(PipelineId, Point2D<f32, CSSPixel>),
/// Enable the sampling profiler, with a given sampling rate and max total sampling duration. /// Enable the sampling profiler, with a given sampling rate and max total sampling duration.
ToggleProfiler(Duration, Duration), ToggleProfiler(Duration, Duration),
/// Request to exit from fullscreen mode /// Request to exit from fullscreen mode

View file

@ -29,7 +29,6 @@ log = { workspace = true }
malloc_size_of = { workspace = true } malloc_size_of = { workspace = true }
malloc_size_of_derive = { workspace = true } malloc_size_of_derive = { workspace = true }
num-derive = "0.4" num-derive = "0.4"
num-traits = { workspace = true }
pixels = { path = "../../pixels" } pixels = { path = "../../pixels" }
serde = { workspace = true } serde = { workspace = true }
servo_url = { path = "../../url" } servo_url = { path = "../../url" }

View file

@ -28,7 +28,6 @@ use ipc_channel::ipc::IpcSender;
use log::warn; use log::warn;
use malloc_size_of::malloc_size_of_is_0; use malloc_size_of::malloc_size_of_is_0;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use num_derive::FromPrimitive;
use pixels::RasterImage; use pixels::RasterImage;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use servo_geometry::{DeviceIndependentIntRect, DeviceIndependentIntSize}; use servo_geometry::{DeviceIndependentIntRect, DeviceIndependentIntSize};
@ -56,7 +55,7 @@ pub enum ShutdownState {
/// A cursor for the window. This is different from a CSS cursor (see /// A cursor for the window. This is different from a CSS cursor (see
/// `CursorKind`) in that it has no `Auto` value. /// `CursorKind`) in that it has no `Auto` value.
#[repr(u8)] #[repr(u8)]
#[derive(Clone, Copy, Debug, Deserialize, Eq, FromPrimitive, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
pub enum Cursor { pub enum Cursor {
None, None,
Default, Default,
@ -889,9 +888,6 @@ 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 cursor that should be used when hovering the item hit by the hit test.
pub cursor: Option<Cursor>,
/// The [`ExternalScrollId`] of the scroll tree node associated with this hit test item. /// The [`ExternalScrollId`] of the scroll tree node associated with this hit test item.
pub external_scroll_id: ExternalScrollId, pub external_scroll_id: ExternalScrollId,
} }

View file

@ -24,7 +24,7 @@ use base::id::{BrowsingContextId, PipelineId, WebViewId};
use bitflags::bitflags; 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::{Cursor, Theme, UntrustedNodeAddress, ViewportDetails};
use euclid::Point2D; use euclid::Point2D;
use euclid::default::{Point2D as UntypedPoint2D, Rect}; use euclid::default::{Point2D as UntypedPoint2D, Rect};
use fnv::FnvHashMap; use fnv::FnvHashMap;
@ -613,6 +613,9 @@ pub struct ElementsFromPointResult {
/// The [`Point2D`] 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: Point2D<f32, CSSPixel>, pub point_in_target: Point2D<f32, CSSPixel>,
/// The [`Cursor`] that's defined on the item that is hit by this
/// hit test result.
pub cursor: Cursor,
} }
bitflags! { bitflags! {

View file

@ -31,7 +31,7 @@ use embedder_traits::{
CompositorHitTestResult, FocusSequenceNumber, InputEvent, JavaScriptEvaluationId, CompositorHitTestResult, FocusSequenceNumber, InputEvent, JavaScriptEvaluationId,
MediaSessionActionType, Theme, ViewportDetails, WebDriverScriptCommand, MediaSessionActionType, Theme, ViewportDetails, WebDriverScriptCommand,
}; };
use euclid::{Rect, Scale, Size2D, UnknownUnit}; use euclid::{Point2D, Rect, Scale, Size2D, UnknownUnit};
use ipc_channel::ipc::{IpcReceiver, IpcSender}; use ipc_channel::ipc::{IpcReceiver, IpcSender};
use keyboard_types::Modifiers; use keyboard_types::Modifiers;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
@ -145,6 +145,9 @@ pub enum ScriptThreadMessage {
ExitScriptThread, ExitScriptThread,
/// Sends a DOM event. /// Sends a DOM event.
SendInputEvent(PipelineId, ConstellationInputEvent), SendInputEvent(PipelineId, ConstellationInputEvent),
/// Ask that the given pipeline refreshes the cursor (after a display list render) based
/// on the hit test at the given point.
RefreshCursor(PipelineId, Point2D<f32, CSSPixel>),
/// Notifies script of the viewport. /// Notifies script of the viewport.
Viewport(PipelineId, Rect<f32, UnknownUnit>), Viewport(PipelineId, Rect<f32, UnknownUnit>),
/// Requests that the script thread immediately send the constellation the title of a pipeline. /// Requests that the script thread immediately send the constellation the title of a pipeline.