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); .send_transaction(self.webrender_document, transaction);
} }
pub(crate) fn update_cursor(&mut self, pos: DevicePoint, result: &CompositorHitTestResult) { pub(crate) fn update_cursor_from_hittest(
self.cursor_pos = pos; &mut self,
pos: DevicePoint,
let cursor = match result.cursor { result: &CompositorHitTestResult,
Some(cursor) if cursor != self.cursor => cursor, ) {
_ => return, if let Some(webview_id) = self
};
let Some(webview_id) = self
.pipeline_to_webview_map .pipeline_to_webview_map
.get(&result.pipeline_id) .get(&result.pipeline_id)
.cloned() .copied()
else { {
self.update_cursor(pos, webview_id, result.cursor);
} else {
warn!("Couldn't update cursor for non-WebView-associated pipeline"); 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; self.cursor = cursor;
@ -631,7 +643,9 @@ impl IOCompositor {
.borrow() .borrow()
.hit_test_at_point(point, details_for_pipeline); .hit_test_at_point(point, details_for_pipeline);
if let Ok(result) = result { if let 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) => { 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::Wheel(_) => { InputEvent::MouseButton(_) |
self.global.borrow_mut().update_cursor(point, &result); InputEvent::MouseLeave(_) |
InputEvent::MouseMove(_) |
InputEvent::Wheel(_) => {
self.global
.borrow_mut()
.update_cursor_from_hittest(point, &result);
}, },
_ => unreachable!("Unexpected input event type: {event:?}"), _ => unreachable!("Unexpected input event type: {event:?}"),
} }
@ -428,7 +433,9 @@ impl WebViewRenderer {
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::Wheel(_) => { 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:?}"), _ => unreachable!("Unexpected input event type: {event:?}"),
} }

View file

@ -97,6 +97,7 @@ mod from_compositor {
InputEvent::Keyboard(..) => target_variant!("Keyboard"), InputEvent::Keyboard(..) => target_variant!("Keyboard"),
InputEvent::MouseButton(..) => target_variant!("MouseButton"), InputEvent::MouseButton(..) => target_variant!("MouseButton"),
InputEvent::MouseMove(..) => target_variant!("MouseMove"), InputEvent::MouseMove(..) => target_variant!("MouseMove"),
InputEvent::MouseLeave(..) => target_variant!("MouseLeave"),
InputEvent::Touch(..) => target_variant!("Touch"), InputEvent::Touch(..) => target_variant!("Touch"),
InputEvent::Wheel(..) => target_variant!("Wheel"), InputEvent::Wheel(..) => target_variant!("Wheel"),
InputEvent::Scroll(..) => target_variant!("Scroll"), 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( fn handle_mouse_enter_leave_event(
&self, &self,
client_point: Point2D<f32>, client_point: Point2D<f32>,

View file

@ -1127,6 +1127,14 @@ impl ScriptThread {
can_gc, 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) => { InputEvent::Touch(touch_event) => {
let touch_result = let touch_result =
document.handle_touch_event(touch_event, event.hit_test_result, can_gc); document.handle_touch_event(touch_event, event.hit_test_result, can_gc);

View file

@ -20,6 +20,7 @@ pub enum InputEvent {
Keyboard(KeyboardEvent), Keyboard(KeyboardEvent),
MouseButton(MouseButtonEvent), MouseButton(MouseButtonEvent),
MouseMove(MouseMoveEvent), MouseMove(MouseMoveEvent),
MouseLeave(MouseLeaveEvent),
Touch(TouchEvent), Touch(TouchEvent),
Wheel(WheelEvent), Wheel(WheelEvent),
Scroll(ScrollEvent), Scroll(ScrollEvent),
@ -42,6 +43,7 @@ impl InputEvent {
InputEvent::Keyboard(..) => None, InputEvent::Keyboard(..) => None,
InputEvent::MouseButton(event) => Some(event.point), InputEvent::MouseButton(event) => Some(event.point),
InputEvent::MouseMove(event) => Some(event.point), InputEvent::MouseMove(event) => Some(event.point),
InputEvent::MouseLeave(event) => Some(event.point),
InputEvent::Touch(event) => Some(event.point), InputEvent::Touch(event) => Some(event.point),
InputEvent::Wheel(event) => Some(event.point), InputEvent::Wheel(event) => Some(event.point),
InputEvent::Scroll(..) => None, InputEvent::Scroll(..) => None,
@ -56,6 +58,7 @@ impl InputEvent {
InputEvent::Keyboard(event) => event.webdriver_id, InputEvent::Keyboard(event) => event.webdriver_id,
InputEvent::MouseButton(event) => event.webdriver_id, InputEvent::MouseButton(event) => event.webdriver_id,
InputEvent::MouseMove(event) => event.webdriver_id, InputEvent::MouseMove(event) => event.webdriver_id,
InputEvent::MouseLeave(..) => None,
InputEvent::Touch(..) => None, InputEvent::Touch(..) => None,
InputEvent::Wheel(event) => event.webdriver_id, InputEvent::Wheel(event) => event.webdriver_id,
InputEvent::Scroll(..) => None, InputEvent::Scroll(..) => None,
@ -76,6 +79,7 @@ impl InputEvent {
InputEvent::MouseMove(ref mut event) => { InputEvent::MouseMove(ref mut event) => {
event.webdriver_id = webdriver_id; event.webdriver_id = webdriver_id;
}, },
InputEvent::MouseLeave(..) => {},
InputEvent::Touch(..) => {}, InputEvent::Touch(..) => {},
InputEvent::Wheel(ref mut event) => { InputEvent::Wheel(ref mut event) => {
event.webdriver_id = webdriver_id; 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. /// The type of input represented by a multi-touch event.
#[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum TouchEventType { pub enum TouchEventType {

View file

@ -20,9 +20,10 @@ use servo::webrender_api::ScrollLocation;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
use servo::{ use servo::{
Cursor, ImeEvent, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, Cursor, ImeEvent, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, OffscreenRenderingContext, MouseButtonAction, MouseButtonEvent, MouseLeaveEvent, MouseMoveEvent,
RenderingContext, ScreenGeometry, Theme, TouchEvent, TouchEventType, TouchId, OffscreenRenderingContext, RenderingContext, ScreenGeometry, Theme, TouchEvent, TouchEventType,
WebRenderDebugOption, WebView, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext, TouchId, WebRenderDebugOption, WebView, WheelDelta, WheelEvent, WheelMode,
WindowRenderingContext,
}; };
use surfman::{Context, Device}; use surfman::{Context, Device};
use url::Url; use url::Url;
@ -570,8 +571,22 @@ impl WindowPortsMethods for Window {
let mut point = winit_position_to_euclid_point(position).to_f32(); let mut point = winit_position_to_euclid_point(position).to_f32();
point.y -= (self.toolbar_height() * self.hidpi_scale_factor()).0; 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); 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, .. } => { WindowEvent::MouseWheel { delta, phase, .. } => {
let (mut dx, mut dy, mode) = match delta { let (mut dx, mut dy, mode) = match delta {
@ -592,8 +607,7 @@ impl WindowPortsMethods for Window {
z: 0.0, z: 0.0,
mode, mode,
}; };
let pos = self.webview_relative_mouse_point.get(); let point = self.webview_relative_mouse_point.get();
let point = Point2D::new(pos.x, pos.y);
// Scroll events snap to the major axis of movement, with vertical // Scroll events snap to the major axis of movement, with vertical
// preferred over horizontal. // preferred over horizontal.
@ -608,11 +622,7 @@ impl WindowPortsMethods for Window {
// Send events // Send events
webview.notify_input_event(InputEvent::Wheel(WheelEvent::new(delta, point))); webview.notify_input_event(InputEvent::Wheel(WheelEvent::new(delta, point)));
webview.notify_scroll_event( webview.notify_scroll_event(scroll_location, point.to_i32(), phase);
scroll_location,
self.webview_relative_mouse_point.get().to_i32(),
phase,
);
}, },
WindowEvent::Touch(touch) => { WindowEvent::Touch(touch) => {
webview.notify_input_event(InputEvent::Touch(TouchEvent::new( webview.notify_input_event(InputEvent::Touch(TouchEvent::new(