diff --git a/Cargo.lock b/Cargo.lock index 00ec82d5aa2..323ddee7656 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1390,7 +1390,6 @@ dependencies = [ "ipc-channel", "libc", "log", - "num-traits", "pixels", "profile_traits", "servo-tracing", @@ -1481,6 +1480,7 @@ dependencies = [ "servo_config", "servo_rand", "servo_url", + "stylo_traits", "tracing", "webgpu", "webgpu_traits", @@ -1511,6 +1511,7 @@ dependencies = [ "servo_url", "strum", "strum_macros", + "stylo_traits", "uuid", "webgpu_traits", "webrender_api", @@ -2250,7 +2251,6 @@ dependencies = [ "log", "malloc_size_of_derive", "num-derive", - "num-traits", "pixels", "serde", "servo_geometry", diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml index f988a895edc..648d60b497f 100644 --- a/components/compositing/Cargo.toml +++ b/components/compositing/Cargo.toml @@ -31,7 +31,6 @@ gleam = { workspace = true } ipc-channel = { workspace = true } libc = { workspace = true } log = { workspace = true } -num-traits = { workspace = true } pixels = { path = "../pixels" } profile_traits = { workspace = true } servo_allocator = { path = "../allocator" } diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 2f321c7952c..e353cbbfe63 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -24,13 +24,10 @@ use compositing_traits::{ use constellation_traits::{EmbedderToConstellationMessage, PaintMetricEvent}; use crossbeam_channel::{Receiver, Sender}; use dpi::PhysicalSize; -use embedder_traits::{ - CompositorHitTestResult, Cursor, InputEvent, ShutdownState, ViewportDetails, -}; +use embedder_traits::{CompositorHitTestResult, InputEvent, ShutdownState, ViewportDetails}; use euclid::{Point2D, Rect, Scale, Size2D, Transform3D}; use ipc_channel::ipc::{self, IpcSharedMemory}; use log::{debug, info, trace, warn}; -use num_traits::cast::FromPrimitive; use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage}; use profile_traits::mem::{ProcessReports, ProfilerRegistration, Report, ReportKind}; use profile_traits::time::{self as profile_time, ProfilerCategory}; @@ -122,11 +119,9 @@ pub struct ServoRenderer { /// True to translate mouse input into touch events. pub(crate) convert_mouse_to_touch: bool, - /// Current mouse cursor. - cursor: Cursor, - - /// Current cursor position. - cursor_pos: DevicePoint, + /// The last position in the rendered view that the mouse moved over. This becomes `None` + /// when the mouse leaves the rendered view. + pub(crate) last_mouse_move_position: Option, } /// 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(); let external_scroll_id = ExternalScrollId(item.tag.0, item.pipeline); - let cursor = Cursor::from_u16(item.tag.1); - Some(CompositorHitTestResult { pipeline_id, point_in_viewport: Point2D::from_untyped(item.point_in_viewport.to_untyped()), point_relative_to_initial_containing_block: Point2D::from_untyped( point_in_initial_containing_block, ), - cursor, external_scroll_id, }) }) @@ -322,46 +314,6 @@ impl ServoRenderer { self.webrender_api .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, - ) { - 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 { @@ -388,8 +340,7 @@ impl IOCompositor { #[cfg(feature = "webxr")] webxr_main_thread: state.webxr_main_thread, convert_mouse_to_touch, - cursor: Cursor::None, - cursor_pos: DevicePoint::new(0.0, 0.0), + last_mouse_move_position: None, })), webview_renderers: WebViewManager::default(), needs_repaint: Cell::default(), @@ -584,26 +535,7 @@ impl IOCompositor { }, CompositorMsg::NewWebRenderFrameReady(_document_id, recomposite_needed) => { - self.pending_frames -= 1; - 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); - } + self.handle_new_webrender_frame_ready(recomposite_needed); }, CompositorMsg::LoadComplete(_) => { @@ -1665,4 +1597,41 @@ impl IOCompositor { fn shutdown_state(&self) -> ShutdownState { 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); + } + } } diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs index f2ea42932c1..9ed7ea3af40 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -353,14 +353,13 @@ impl WebViewRenderer { InputEvent::Touch(ref mut touch_event) => { touch_event.init_sequence_id(self.touch_handler.current_sequence_id); }, - InputEvent::MouseButton(_) | - InputEvent::MouseLeave(_) | - InputEvent::MouseMove(_) | - InputEvent::Wheel(_) => { - self.global - .borrow_mut() - .update_cursor_from_hittest(point, &result); + InputEvent::MouseMove(_) => { + self.global.borrow_mut().last_mouse_move_position = Some(point); }, + InputEvent::MouseLeave(_) => { + self.global.borrow_mut().last_mouse_move_position = None; + }, + InputEvent::MouseButton(_) | InputEvent::Wheel(_) => {}, _ => unreachable!("Unexpected input event type: {event:?}"), } diff --git a/components/constellation/Cargo.toml b/components/constellation/Cargo.toml index f0dd72ef3cb..4b01b0cbce3 100644 --- a/components/constellation/Cargo.toml +++ b/components/constellation/Cargo.toml @@ -50,6 +50,7 @@ serde = { workspace = true } servo_config = { path = "../config" } servo_rand = { path = "../rand" } servo_url = { path = "../url" } +stylo_traits = { workspace = true } tracing = { workspace = true, optional = true } webgpu = { path = "../webgpu" } webgpu_traits = { workspace = true } diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index e23e18a89fc..400e15f2344 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -130,14 +130,14 @@ use devtools_traits::{ use embedder_traits::resources::{self, Resource}; use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::{ - AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy, FocusId, + AnimationState, CompositorHitTestResult, EmbedderMsg, EmbedderProxy, FocusId, FocusSequenceNumber, InputEvent, JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId, KeyboardEvent, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg, WebDriverCommandResponse, WebDriverLoadStatus, WebDriverScriptCommand, }; -use euclid::Size2D; use euclid::default::Size2D as UntypedSize2D; +use euclid::{Point2D, Size2D}; use fonts::SystemFontServiceProxy; use ipc_channel::Error as IpcError; use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; @@ -163,6 +163,7 @@ use serde::{Deserialize, Serialize}; use servo_config::{opts, pref}; use servo_rand::{Rng, ServoRng, SliceRandom, random}; use servo_url::{Host, ImmutableOrigin, ServoUrl}; +use style_traits::CSSPixel; #[cfg(feature = "webgpu")] use webgpu::swapchain::WGPUImageMap; #[cfg(feature = "webgpu")] @@ -1445,8 +1446,8 @@ where EmbedderToConstellationMessage::ForwardInputEvent(webview_id, event, hit_test) => { self.forward_input_event(webview_id, event, hit_test); }, - EmbedderToConstellationMessage::SetCursor(webview_id, cursor) => { - self.handle_set_cursor_msg(webview_id, cursor) + EmbedderToConstellationMessage::RefreshCursor(pipeline_id, point) => { + self.handle_refresh_cursor(pipeline_id, point) }, EmbedderToConstellationMessage::ToggleProfiler(rate, max_duration) => { for background_monitor_control_sender in &self.background_monitor_control_senders { @@ -3438,9 +3439,17 @@ where } #[servo_tracing::instrument(skip_all)] - fn handle_set_cursor_msg(&mut self, webview_id: WebViewId, cursor: Cursor) { - self.embedder_proxy - .send(EmbedderMsg::SetCursor(webview_id, cursor)); + fn handle_refresh_cursor(&self, pipeline_id: PipelineId, point: Point2D) { + let Some(pipeline) = self.pipelines.get(&pipeline_id) else { + 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)] diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 9c6395b01b2..15c856f00a2 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -67,7 +67,7 @@ mod from_compositor { Self::FocusWebView(..) => target!("FocusWebView"), Self::BlurWebView => target!("BlurWebView"), Self::ForwardInputEvent(_webview_id, event, ..) => event.log_target(), - Self::SetCursor(..) => target!("SetCursor"), + Self::RefreshCursor(..) => target!("RefreshCursor"), Self::ToggleProfiler(..) => target!("EnableProfiler"), Self::ExitFullScreen(_) => target!("ExitFullScreen"), Self::MediaSessionAction(_) => target!("MediaSessionAction"), diff --git a/components/layout/display_list/hit_test.rs b/components/layout/display_list/hit_test.rs index a6c5adf9e55..4ce1c29333c 100644 --- a/components/layout/display_list/hit_test.rs +++ b/components/layout/display_list/hit_test.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use app_units::Au; use base::id::ScrollTreeNodeId; +use embedder_traits::Cursor; use euclid::{Box2D, Point2D, Point3D, Vector2D}; use kurbo::{Ellipse, Shape}; 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::visibility::T as Visibility; use style::properties::ComputedValues; +use style::values::computed::ui::CursorKind; use webrender_api::BorderRadius; use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSize, LayoutTransform, RectExt}; @@ -21,7 +23,7 @@ use crate::display_list::stacking_context::StackingContextSection; use crate::display_list::{ StackingContext, StackingContextContent, StackingContextTree, ToWebRender, }; -use crate::fragment_tree::Fragment; +use crate::fragment_tree::{Fragment, FragmentFlags}; use crate::geom::PhysicalRect; pub(crate) struct HitTest<'a> { @@ -249,12 +251,18 @@ impl Fragment { let mut hit_test_fragment_inner = |style: &ComputedValues, fragment_rect: PhysicalRect, - border_radius: BorderRadius| { - if style.get_inherited_ui().pointer_events == PointerEvents::None { - return false; - } - if style.get_inherited_box().visibility != Visibility::Visible { - return false; + border_radius: BorderRadius, + fragment_flags: FragmentFlags, + auto_cursor: Cursor| { + let is_root_element = fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT); + + 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) = @@ -263,14 +271,28 @@ impl Fragment { 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() { return false; } 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(), &border_radius, point_in_spatial_node, @@ -283,11 +305,12 @@ impl Fragment { fragment_rect.origin.x.to_f32_px(), fragment_rect.origin.y.to_f32_px(), ); + hit_test.results.push(ElementsFromPointResult { node: tag.node, point_in_target, + cursor: cursor(style.get_inherited_ui().cursor.keyword, auto_cursor), }); - !hit_test.flags.contains(ElementsFromPointFlags::FindAll) }; @@ -298,6 +321,8 @@ impl Fragment { &box_fragment.style, box_fragment.border_rect(), box_fragment.border_radius(), + box_fragment.base.flags, + Cursor::Default, ) }, Fragment::Text(text) => { @@ -306,6 +331,8 @@ impl Fragment { &text.inline_styles.style.borrow(), text.rect, BorderRadius::zero(), + FragmentFlags::empty(), + Cursor::Text, ) }, _ => 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_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, + } +} diff --git a/components/layout/display_list/mod.rs b/components/layout/display_list/mod.rs index 49fb89a811f..bec719f106c 100644 --- a/components/layout/display_list/mod.rs +++ b/components/layout/display_list/mod.rs @@ -9,7 +9,6 @@ use app_units::{AU_PER_PX, Au}; use base::id::ScrollTreeNodeId; use clip::{Clip, ClipId}; use compositing_traits::display_list::{CompositorDisplayListInfo, SpatialTreeNodeInfo}; -use embedder_traits::Cursor; use euclid::{Point2D, Scale, SideOffsets2D, Size2D, UnknownUnit, Vector2D}; use fonts::GlyphStore; use gradient::WebRenderGradient; @@ -36,13 +35,12 @@ use style::values::computed::{ use style::values::generics::NonNegative; use style::values::generics::rect::Rect; use style::values::specified::text::TextDecorationLine; -use style::values::specified::ui::CursorKind; use style_traits::{CSSPixel as StyloCSSPixel, DevicePixel as StyloDevicePixel}; use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel, LayoutRect, LayoutSize}; use webrender_api::{ self as wr, BorderDetails, BorderRadius, BoxShadowClipMode, BuiltDisplayList, ClipChainId, ClipMode, CommonItemProperties, ComplexClipRegion, NinePatchBorder, NinePatchBorderSource, - PropertyBinding, SpatialId, SpatialTreeItemKey, units, + PrimitiveFlags, PropertyBinding, SpatialId, SpatialTreeItemKey, units, }; use wr::units::LayoutVector2D; @@ -164,8 +162,9 @@ impl DisplayListBuilder<'_> { ) -> BuiltDisplayList { // Build the rest of the display list which inclues all of the WebRender primitives. let compositor_info = &mut stacking_context_tree.compositor_info; + let pipeline_id = compositor_info.pipeline_id; 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(); // `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.push_hit_tests_for_scrollable_areas(&stacking_context_tree.hit_test_items); + // Paint the canvas’ background (if any) before/under everything else stacking_context_tree .root_stacking_context @@ -309,6 +310,39 @@ impl DisplayListBuilder<'_> { 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 /// its [`ClipId`] to a WebRender [`ClipChainId`]. This happens: /// - When WebRender display list construction starts: All clips created during the @@ -548,7 +582,6 @@ impl Fragment { builder: &mut DisplayListBuilder, containing_block: &PhysicalRect, section: StackingContextSection, - is_hit_test_for_scrollable_overflow: bool, is_collapsed_table_borders: bool, text_decorations: &Arc>, ) { @@ -572,7 +605,6 @@ impl Fragment { Visibility::Visible => BuilderForBoxFragment::new( box_fragment, containing_block, - is_hit_test_for_scrollable_overflow, is_collapsed_table_borders, ) .build(builder, section), @@ -580,19 +612,7 @@ impl Fragment { Visibility::Collapse => (), } }, - Fragment::AbsoluteOrFixedPositioned(_) => {}, - 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::AbsoluteOrFixedPositioned(_) | Fragment::Positioning(_) => {}, Fragment::Image(image) => { let image = image.borrow(); 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, - 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( &self, fragment: &TextFragment, @@ -724,8 +723,6 @@ impl Fragment { } 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 font_metrics = &fragment.font_metrics; let dppx = builder.device_pixel_ratio.get(); @@ -939,7 +936,6 @@ struct BuilderForBoxFragment<'a> { border_edge_clip_chain_id: RefCell>, padding_edge_clip_chain_id: RefCell>, content_edge_clip_chain_id: RefCell>, - is_hit_test_for_scrollable_overflow: bool, is_collapsed_table_borders: bool, } @@ -947,7 +943,6 @@ impl<'a> BuilderForBoxFragment<'a> { fn new( fragment: &'a BoxFragment, containing_block: &'a PhysicalRect, - is_hit_test_for_scrollable_overflow: bool, is_collapsed_table_borders: bool, ) -> Self { let border_rect = fragment @@ -964,7 +959,6 @@ impl<'a> BuilderForBoxFragment<'a> { border_edge_clip_chain_id: RefCell::new(None), padding_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, } } @@ -1047,11 +1041,6 @@ impl<'a> BuilderForBoxFragment<'a> { } 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 { self.build_collapsed_table_borders(builder); return; @@ -1062,7 +1051,6 @@ impl<'a> BuilderForBoxFragment<'a> { return; } - self.build_hit_test(builder, self.border_rect); if self .fragment .base @@ -1077,28 +1065,6 @@ impl<'a> BuilderForBoxFragment<'a> { 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( &mut self, builder: &mut DisplayListBuilder, @@ -1623,47 +1589,6 @@ fn glyphs_advance_by_index( 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 fn inner_radii(mut radii: wr::BorderRadius, insets: units::LayoutSideOffsets) -> wr::BorderRadius { assert!(insets.left >= 0.0, "left inset must not be negative"); diff --git a/components/layout/display_list/stacking_context.rs b/components/layout/display_list/stacking_context.rs index a2e6f762c7a..132a014b2fc 100644 --- a/components/layout/display_list/stacking_context.rs +++ b/components/layout/display_list/stacking_context.rs @@ -33,7 +33,7 @@ use style::values::generics::box_::Perspective; use style::values::generics::transform::{self, GenericRotate, GenericScale, GenericTranslate}; use style::values::specified::box_::DisplayOutside; 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::units::{LayoutPixel, LayoutSize}; @@ -100,6 +100,22 @@ pub(crate) enum StackingContextSection { 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 { /// The root stacking context of this [`StackingContextTree`]. 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 /// display list construction, but they are never added here. 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, } impl StackingContextTree { @@ -174,6 +194,7 @@ impl StackingContextTree { root_stacking_context: StackingContext::create_root(root_scroll_node_id, debug), compositor_info, clip_store: Default::default(), + hit_test_items: Vec::new(), }; let mut root_stacking_context = StackingContext::create_root(root_scroll_node_id, debug); @@ -281,7 +302,6 @@ pub(crate) enum StackingContextContent { section: StackingContextSection, containing_block: PhysicalRect, fragment: Fragment, - is_hit_test_for_scrollable_overflow: bool, is_collapsed_table_borders: bool, text_decorations: Arc>, }, @@ -313,7 +333,6 @@ impl StackingContextContent { section, containing_block, fragment, - is_hit_test_for_scrollable_overflow, is_collapsed_table_borders, text_decorations, } => { @@ -324,7 +343,6 @@ impl StackingContextContent { builder, containing_block, *section, - *is_hit_test_for_scrollable_overflow, *is_collapsed_table_borders, text_decorations, ); @@ -640,7 +658,6 @@ impl StackingContext { let mut fragment_builder = BuilderForBoxFragment::new( &root_fragment, &fragment_tree.initial_containing_block, - false, /* is_hit_test_for_scrollable_overflow */ false, /* is_collapsed_table_borders */ ); let painter = super::background::BackgroundPainter { @@ -888,7 +905,6 @@ impl Fragment { clip_id: containing_block.clip_id, containing_block: containing_block.rect, fragment: fragment_clone, - is_hit_test_for_scrollable_overflow: false, is_collapsed_table_borders: false, text_decorations: text_decorations.clone(), }); @@ -1088,7 +1104,6 @@ impl BoxFragment { BuilderForBoxFragment::new( self, &containing_block.rect, - false, /* is_hit_test_for_scrollable_overflow */ false, /* is_collapsed_table_borders */ ), ) @@ -1172,7 +1187,6 @@ impl BoxFragment { BuilderForBoxFragment::new( self, &containing_block.rect, - false, /* is_hit_test_for_scrollable_overflow*/ false, /* is_collapsed_table_borders */ ), ) { @@ -1205,7 +1219,6 @@ impl BoxFragment { section, containing_block: containing_block.rect, fragment: fragment.clone(), - is_hit_test_for_scrollable_overflow: false, is_collapsed_table_borders: false, text_decorations: text_decorations.clone(), }); @@ -1234,21 +1247,6 @@ impl BoxFragment { 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_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, containing_block: containing_block.rect, fragment: fragment.clone(), - is_hit_test_for_scrollable_overflow: false, is_collapsed_table_borders: true, text_decorations: text_decorations.clone(), }); @@ -1417,7 +1414,7 @@ impl BoxFragment { // https://drafts.csswg.org/css-overflow-3/#corner-clipping let radii; 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); } else if overflow.x != ComputedOverflow::Clip { overflow_clip_rect.min.x = f32::MIN; @@ -1467,14 +1464,14 @@ impl BoxFragment { .to_webrender(); 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, *parent_scroll_node_id, parent_clip_id, ); let tag = self.base.tag?; - let external_id = wr::ExternalScrollId( + let external_scroll_id = wr::ExternalScrollId( tag.to_display_list_fragment_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( parent_scroll_node_id, - external_id, + external_scroll_id, self.scrollable_overflow().to_webrender(), scroll_frame_rect, 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 { clip_id, scroll_frame_data: Some(ScrollFrameData { diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 96fc13eb499..9de12104e01 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -28,7 +28,7 @@ use data_url::mime::Mime; use devtools_traits::ScriptToDevtoolsControlMsg; use dom_struct::dom_struct; use embedder_traits::{ - AllowOrDeny, AnimationState, ContextMenuResult, EditingActionEvent, EmbedderMsg, + AllowOrDeny, AnimationState, ContextMenuResult, Cursor, EditingActionEvent, EmbedderMsg, FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction, MouseButtonEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId, UntrustedNodeAddress, WheelEvent, @@ -2008,6 +2008,9 @@ impl Document { 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 .node .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)] pub(crate) fn handle_mouse_leave_event( &self, diff --git a/components/script/dom/inputevent.rs b/components/script/dom/inputevent.rs index 4fc57d1d6d8..c7bd4c3f5cc 100644 --- a/components/script/dom/inputevent.rs +++ b/components/script/dom/inputevent.rs @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use dom_struct::dom_struct; +use embedder_traits::Cursor; use euclid::Point2D; use js::rust::HandleObject; use style_traits::CSSPixel; @@ -99,6 +100,7 @@ impl InputEventMethods for InputEvent { /// `CompositorHitTestResult` against our current layout. pub(crate) struct HitTestResult { pub node: DomRoot, + pub cursor: Cursor, pub point_in_node: Point2D, pub point_in_frame: Point2D, pub point_relative_to_initial_containing_block: Point2D, diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index c62d17583bf..148fb53c959 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -2620,6 +2620,7 @@ impl Window { let address = UntrustedNodeAddress(result.node.0 as *const c_void); Some(HitTestResult { node: unsafe { from_untrusted_node_address(address) }, + cursor: result.cursor, 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 @@ -3089,6 +3090,20 @@ impl Window { } } } + + pub fn handle_refresh_cursor(&self, cursor_position: Point2D) { + 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 { diff --git a/components/script/messaging.rs b/components/script/messaging.rs index 57665183f67..f35704d666c 100644 --- a/components/script/messaging.rs +++ b/components/script/messaging.rs @@ -62,6 +62,7 @@ impl MixedMessage { ScriptThreadMessage::ExitPipeline(_webview_id, id, ..) => Some(*id), ScriptThreadMessage::ExitScriptThread => None, ScriptThreadMessage::SendInputEvent(id, ..) => Some(*id), + ScriptThreadMessage::RefreshCursor(id, ..) => Some(*id), ScriptThreadMessage::Viewport(id, ..) => Some(*id), ScriptThreadMessage::GetTitle(id) => Some(*id), ScriptThreadMessage::SetDocumentActivity(id, ..) => Some(*id), diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 7fb5d33192a..ed9d1b3980c 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -92,6 +92,7 @@ use script_traits::{ use servo_config::opts; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use style::thread_state::{self, ThreadState}; +use style_traits::CSSPixel; use stylo_atoms::Atom; use timers::{TimerEventRequest, TimerId, TimerScheduler}; 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), )); } + + fn handle_refresh_cursor( + &self, + pipeline_id: PipelineId, + cursor_position: Point2D, + ) { + let Some(window) = self.documents.borrow().find_window(pipeline_id) else { + return; + }; + window.handle_refresh_cursor(cursor_position); + } } impl Drop for ScriptThread { diff --git a/components/shared/constellation/Cargo.toml b/components/shared/constellation/Cargo.toml index eff0732f60b..7a1132ecd63 100644 --- a/components/shared/constellation/Cargo.toml +++ b/components/shared/constellation/Cargo.toml @@ -34,6 +34,7 @@ serde = { workspace = true } servo_url = { path = "../../url" } strum = { workspace = true } strum_macros = { workspace = true } +stylo_traits = { workspace = true } uuid = { workspace = true } webgpu_traits = { workspace = true } webrender_api = { workspace = true } diff --git a/components/shared/constellation/lib.rs b/components/shared/constellation/lib.rs index 99219d62c11..966d6b88897 100644 --- a/components/shared/constellation/lib.rs +++ b/components/shared/constellation/lib.rs @@ -19,10 +19,10 @@ use base::Epoch; use base::cross_process_instant::CrossProcessInstant; use base::id::{MessagePortId, PipelineId, WebViewId}; use embedder_traits::{ - CompositorHitTestResult, Cursor, FocusId, InputEvent, JavaScriptEvaluationId, - MediaSessionActionType, Theme, TraversalId, ViewportDetails, WebDriverCommandMsg, - WebDriverCommandResponse, + CompositorHitTestResult, FocusId, InputEvent, JavaScriptEvaluationId, MediaSessionActionType, + Theme, TraversalId, ViewportDetails, WebDriverCommandMsg, WebDriverCommandResponse, }; +use euclid::Point2D; pub use from_script_message::*; use ipc_channel::ipc::IpcSender; use malloc_size_of_derive::MallocSizeOf; @@ -31,6 +31,7 @@ use serde::{Deserialize, Serialize}; use servo_url::{ImmutableOrigin, ServoUrl}; pub use structured_data::*; use strum_macros::IntoStaticStr; +use style_traits::CSSPixel; use webrender_api::units::LayoutVector2D; use webrender_api::{ExternalScrollId, ImageKey}; @@ -76,8 +77,9 @@ pub enum EmbedderToConstellationMessage { BlurWebView, /// Forward an input event to an appropriate ScriptTask. ForwardInputEvent(WebViewId, InputEvent, Option), - /// Requesting a change to the onscreen cursor. - SetCursor(WebViewId, Cursor), + /// Request that the given pipeline do a hit test at the location and reset the + /// cursor accordingly. This happens after a display list update is rendered. + RefreshCursor(PipelineId, Point2D), /// Enable the sampling profiler, with a given sampling rate and max total sampling duration. ToggleProfiler(Duration, Duration), /// Request to exit from fullscreen mode diff --git a/components/shared/embedder/Cargo.toml b/components/shared/embedder/Cargo.toml index 22764934808..2ff427a4057 100644 --- a/components/shared/embedder/Cargo.toml +++ b/components/shared/embedder/Cargo.toml @@ -29,7 +29,6 @@ log = { workspace = true } malloc_size_of = { workspace = true } malloc_size_of_derive = { workspace = true } num-derive = "0.4" -num-traits = { workspace = true } pixels = { path = "../../pixels" } serde = { workspace = true } servo_url = { path = "../../url" } diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index 2247bb70067..378ebf55ff3 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -28,7 +28,6 @@ use ipc_channel::ipc::IpcSender; use log::warn; use malloc_size_of::malloc_size_of_is_0; use malloc_size_of_derive::MallocSizeOf; -use num_derive::FromPrimitive; use pixels::RasterImage; use serde::{Deserialize, Deserializer, Serialize, Serializer}; 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 /// `CursorKind`) in that it has no `Auto` value. #[repr(u8)] -#[derive(Clone, Copy, Debug, Deserialize, Eq, FromPrimitive, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)] pub enum Cursor { None, Default, @@ -889,9 +888,6 @@ pub struct CompositorHitTestResult { /// containing block. pub point_relative_to_initial_containing_block: Point2D, - /// The cursor that should be used when hovering the item hit by the hit test. - pub cursor: Option, - /// The [`ExternalScrollId`] of the scroll tree node associated with this hit test item. pub external_scroll_id: ExternalScrollId, } diff --git a/components/shared/layout/lib.rs b/components/shared/layout/lib.rs index 72bb1910241..422a1f27b98 100644 --- a/components/shared/layout/lib.rs +++ b/components/shared/layout/lib.rs @@ -24,7 +24,7 @@ use base::id::{BrowsingContextId, PipelineId, WebViewId}; use bitflags::bitflags; use compositing_traits::CrossProcessCompositorApi; use constellation_traits::LoadData; -use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails}; +use embedder_traits::{Cursor, Theme, UntrustedNodeAddress, ViewportDetails}; use euclid::Point2D; use euclid::default::{Point2D as UntypedPoint2D, Rect}; use fnv::FnvHashMap; @@ -613,6 +613,9 @@ pub struct ElementsFromPointResult { /// The [`Point2D`] of the original query point relative to the /// node fragment rectangle. pub point_in_target: Point2D, + /// The [`Cursor`] that's defined on the item that is hit by this + /// hit test result. + pub cursor: Cursor, } bitflags! { diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 74d264e3697..a1bceff8ae5 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -31,7 +31,7 @@ use embedder_traits::{ CompositorHitTestResult, FocusSequenceNumber, InputEvent, JavaScriptEvaluationId, MediaSessionActionType, Theme, ViewportDetails, WebDriverScriptCommand, }; -use euclid::{Rect, Scale, Size2D, UnknownUnit}; +use euclid::{Point2D, Rect, Scale, Size2D, UnknownUnit}; use ipc_channel::ipc::{IpcReceiver, IpcSender}; use keyboard_types::Modifiers; use malloc_size_of_derive::MallocSizeOf; @@ -145,6 +145,9 @@ pub enum ScriptThreadMessage { ExitScriptThread, /// Sends a DOM event. 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), /// Notifies script of the viewport. Viewport(PipelineId, Rect), /// Requests that the script thread immediately send the constellation the title of a pipeline.