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 <legendmastertony@gmail.com>
This commit is contained in:
Tony 2025-06-18 19:59:11 +08:00 committed by GitHub
parent 0896341285
commit b9fcc95992
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 125 additions and 27 deletions

View file

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

View file

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

View file

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

View file

@ -2113,6 +2113,49 @@ impl Document {
}
}
#[allow(unsafe_code)]
pub(crate) fn handle_mouse_leave_event(
&self,
hit_test_result: Option<CompositorHitTestResult>,
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>)
{
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<f32>,

View file

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

View file

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

View file

@ -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;
self.webview_relative_mouse_point.set(point);
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);
},
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(