From b9fcc959920055484f77af70762a46486a62b479 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:59:11 +0800 Subject: [PATCH] libservo: Allow embedders to signal when the cursor has left the `WebView` (#37317) Currently, the hover state will stay when the mouse moves out of the webview, this PR fixes it Testing: Hover on the `About` on servo.org, and then move the mouse up to the browser UI, see the hover state resets Signed-off-by: Tony --- components/compositing/compositor.rs | 40 +++++++++++++------- components/compositing/webview_renderer.rs | 13 +++++-- components/constellation/tracing.rs | 1 + components/script/dom/document.rs | 43 ++++++++++++++++++++++ components/script/script_thread.rs | 8 ++++ components/shared/embedder/input_events.rs | 15 ++++++++ ports/servoshell/desktop/headed_window.rs | 32 ++++++++++------ 7 files changed, 125 insertions(+), 27 deletions(-) diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 37b7bd2a6ba..270f984c8b3 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -360,21 +360,33 @@ impl ServoRenderer { .send_transaction(self.webrender_document, transaction); } - pub(crate) fn update_cursor(&mut self, pos: DevicePoint, result: &CompositorHitTestResult) { - self.cursor_pos = pos; - - let cursor = match result.cursor { - Some(cursor) if cursor != self.cursor => cursor, - _ => return, - }; - - let Some(webview_id) = self + 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) - .cloned() - else { + .copied() + { + self.update_cursor(pos, webview_id, result.cursor); + } else { warn!("Couldn't update cursor for non-WebView-associated pipeline"); - return; + }; + } + + 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; @@ -631,7 +643,9 @@ impl IOCompositor { .borrow() .hit_test_at_point(point, details_for_pipeline); if let Ok(result) = result { - self.global.borrow_mut().update_cursor(point, &result); + self.global + .borrow_mut() + .update_cursor_from_hittest(point, &result); } } diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs index 438fdce83d6..59a8bb29e90 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -385,8 +385,13 @@ impl WebViewRenderer { InputEvent::Touch(ref mut touch_event) => { touch_event.init_sequence_id(self.touch_handler.current_sequence_id); }, - InputEvent::MouseButton(_) | InputEvent::MouseMove(_) | InputEvent::Wheel(_) => { - self.global.borrow_mut().update_cursor(point, &result); + InputEvent::MouseButton(_) | + InputEvent::MouseLeave(_) | + InputEvent::MouseMove(_) | + InputEvent::Wheel(_) => { + self.global + .borrow_mut() + .update_cursor_from_hittest(point, &result); }, _ => unreachable!("Unexpected input event type: {event:?}"), } @@ -428,7 +433,9 @@ impl WebViewRenderer { touch_event.init_sequence_id(self.touch_handler.current_sequence_id); }, InputEvent::MouseButton(_) | InputEvent::MouseMove(_) | InputEvent::Wheel(_) => { - self.global.borrow_mut().update_cursor(point, &result); + self.global + .borrow_mut() + .update_cursor_from_hittest(point, &result); }, _ => unreachable!("Unexpected input event type: {event:?}"), } diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 640341d1eef..c97b8722b6e 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -97,6 +97,7 @@ mod from_compositor { InputEvent::Keyboard(..) => target_variant!("Keyboard"), InputEvent::MouseButton(..) => target_variant!("MouseButton"), InputEvent::MouseMove(..) => target_variant!("MouseMove"), + InputEvent::MouseLeave(..) => target_variant!("MouseLeave"), InputEvent::Touch(..) => target_variant!("Touch"), InputEvent::Wheel(..) => target_variant!("Wheel"), InputEvent::Scroll(..) => target_variant!("Scroll"), diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 1b30dd3ac8e..f015bd1be2a 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -2113,6 +2113,49 @@ impl Document { } } + #[allow(unsafe_code)] + pub(crate) fn handle_mouse_leave_event( + &self, + hit_test_result: Option, + pressed_mouse_buttons: u16, + can_gc: CanGc, + ) { + // Ignore all incoming events without a hit test. + let Some(hit_test_result) = hit_test_result else { + return; + }; + + self.window() + .send_to_embedder(EmbedderMsg::Status(self.webview_id(), None)); + + let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; + for element in node + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + { + element.set_hover_state(false); + element.set_active_state(false); + } + + self.fire_mouse_event( + hit_test_result.point_in_viewport, + node.upcast(), + FireMouseEventType::Out, + EventBubbles::Bubbles, + EventCancelable::Cancelable, + pressed_mouse_buttons, + can_gc, + ); + self.handle_mouse_enter_leave_event( + hit_test_result.point_in_viewport, + FireMouseEventType::Leave, + None, + node, + pressed_mouse_buttons, + can_gc, + ); + } + fn handle_mouse_enter_leave_event( &self, client_point: Point2D, diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 5b671efcfb2..5efaeb8dd44 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -1127,6 +1127,14 @@ impl ScriptThread { can_gc, ); }, + InputEvent::MouseLeave(_) => { + self.topmost_mouse_over_target.take(); + document.handle_mouse_leave_event( + event.hit_test_result, + event.pressed_mouse_buttons, + can_gc, + ); + }, InputEvent::Touch(touch_event) => { let touch_result = document.handle_touch_event(touch_event, event.hit_test_result, can_gc); diff --git a/components/shared/embedder/input_events.rs b/components/shared/embedder/input_events.rs index 6a77b3b7109..372fac2277f 100644 --- a/components/shared/embedder/input_events.rs +++ b/components/shared/embedder/input_events.rs @@ -20,6 +20,7 @@ pub enum InputEvent { Keyboard(KeyboardEvent), MouseButton(MouseButtonEvent), MouseMove(MouseMoveEvent), + MouseLeave(MouseLeaveEvent), Touch(TouchEvent), Wheel(WheelEvent), Scroll(ScrollEvent), @@ -42,6 +43,7 @@ impl InputEvent { InputEvent::Keyboard(..) => None, InputEvent::MouseButton(event) => Some(event.point), InputEvent::MouseMove(event) => Some(event.point), + InputEvent::MouseLeave(event) => Some(event.point), InputEvent::Touch(event) => Some(event.point), InputEvent::Wheel(event) => Some(event.point), InputEvent::Scroll(..) => None, @@ -56,6 +58,7 @@ impl InputEvent { InputEvent::Keyboard(event) => event.webdriver_id, InputEvent::MouseButton(event) => event.webdriver_id, InputEvent::MouseMove(event) => event.webdriver_id, + InputEvent::MouseLeave(..) => None, InputEvent::Touch(..) => None, InputEvent::Wheel(event) => event.webdriver_id, InputEvent::Scroll(..) => None, @@ -76,6 +79,7 @@ impl InputEvent { InputEvent::MouseMove(ref mut event) => { event.webdriver_id = webdriver_id; }, + InputEvent::MouseLeave(..) => {}, InputEvent::Touch(..) => {}, InputEvent::Wheel(ref mut event) => { event.webdriver_id = webdriver_id; @@ -214,6 +218,17 @@ impl MouseMoveEvent { } } +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct MouseLeaveEvent { + pub point: DevicePoint, +} + +impl MouseLeaveEvent { + pub fn new(point: DevicePoint) -> Self { + Self { point } + } +} + /// The type of input represented by a multi-touch event. #[derive(Clone, Copy, Debug, Deserialize, Serialize)] pub enum TouchEventType { diff --git a/ports/servoshell/desktop/headed_window.rs b/ports/servoshell/desktop/headed_window.rs index e06942bef18..d0c4e24a376 100644 --- a/ports/servoshell/desktop/headed_window.rs +++ b/ports/servoshell/desktop/headed_window.rs @@ -20,9 +20,10 @@ use servo::webrender_api::ScrollLocation; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; use servo::{ Cursor, ImeEvent, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, - MouseButtonAction, MouseButtonEvent, MouseMoveEvent, OffscreenRenderingContext, - RenderingContext, ScreenGeometry, Theme, TouchEvent, TouchEventType, TouchId, - WebRenderDebugOption, WebView, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext, + MouseButtonAction, MouseButtonEvent, MouseLeaveEvent, MouseMoveEvent, + OffscreenRenderingContext, RenderingContext, ScreenGeometry, Theme, TouchEvent, TouchEventType, + TouchId, WebRenderDebugOption, WebView, WheelDelta, WheelEvent, WheelMode, + WindowRenderingContext, }; use surfman::{Context, Device}; use url::Url; @@ -570,8 +571,22 @@ impl WindowPortsMethods for Window { let mut point = winit_position_to_euclid_point(position).to_f32(); point.y -= (self.toolbar_height() * self.hidpi_scale_factor()).0; + let previous_point = self.webview_relative_mouse_point.get(); + if webview.rect().contains(point) { + webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point))); + } else if webview.rect().contains(previous_point) { + webview.notify_input_event(InputEvent::MouseLeave(MouseLeaveEvent::new( + previous_point, + ))); + } + self.webview_relative_mouse_point.set(point); - webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point))); + }, + WindowEvent::CursorLeft { .. } => { + let point = self.webview_relative_mouse_point.get(); + if webview.rect().contains(point) { + webview.notify_input_event(InputEvent::MouseLeave(MouseLeaveEvent::new(point))); + } }, WindowEvent::MouseWheel { delta, phase, .. } => { let (mut dx, mut dy, mode) = match delta { @@ -592,8 +607,7 @@ impl WindowPortsMethods for Window { z: 0.0, mode, }; - let pos = self.webview_relative_mouse_point.get(); - let point = Point2D::new(pos.x, pos.y); + let point = self.webview_relative_mouse_point.get(); // Scroll events snap to the major axis of movement, with vertical // preferred over horizontal. @@ -608,11 +622,7 @@ impl WindowPortsMethods for Window { // Send events webview.notify_input_event(InputEvent::Wheel(WheelEvent::new(delta, point))); - webview.notify_scroll_event( - scroll_location, - self.webview_relative_mouse_point.get().to_i32(), - phase, - ); + webview.notify_scroll_event(scroll_location, point.to_i32(), phase); }, WindowEvent::Touch(touch) => { webview.notify_input_event(InputEvent::Touch(TouchEvent::new(