script: Ensure that leaving the WebView sets the cursor back to the default cursor (#38759)

This changes makes a variety of changes to ensure that the cursor is set
back to the default cursor when it leaves the `WebView`:

1. Display list updates can come after a mouse leaves the `WebView`, so
   when refreshing the cursor after the update, base the updated cursor
   on the last hovered location in the `DocumentEventHandler`, rather
   than the compositor. This allows us to catch when the last hovered
   position is `None` (ie the cursor has left the `WebView`).
2. When handling `MouseLeftViewport` events for the cursor leaving the
   entire WebView, properly set the
   MouseLeftViewport::focus_moving_to_another_iframe` on the input event
   passed to the script thread.
3. When moving out of the `WebView` entirely, explicitly ask the
   embedder to set the cursor back to the default.

Testing: This change adds a unit test verifying this behavior.
Fixes: #38710.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-08-22 00:49:56 -07:00 committed by GitHub
parent 66adf2bf9f
commit 4784ff0375
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 164 additions and 90 deletions

View file

@ -79,7 +79,7 @@ pub(crate) struct DocumentEventHandler {
current_hover_target: MutNullableDom<Element>,
/// The most recent mouse movement point, used for processing `mouseleave` events.
#[no_trace]
most_recent_mousemove_point: Point2D<f32, CSSPixel>,
most_recent_mousemove_point: Cell<Option<Point2D<f32, CSSPixel>>>,
/// The currently set [`Cursor`] or `None` if the `Document` isn't being hovered
/// by the cursor.
#[no_trace]
@ -212,13 +212,15 @@ impl DocumentEventHandler {
}));
}
pub(crate) fn set_cursor(&self, cursor: Cursor) {
if Some(cursor) == self.current_cursor.get() {
pub(crate) fn set_cursor(&self, cursor: Option<Cursor>) {
if cursor == self.current_cursor.get() {
return;
}
self.current_cursor.set(Some(cursor));
self.window
.send_to_embedder(EmbedderMsg::SetCursor(self.window.webview_id(), cursor));
self.current_cursor.set(cursor);
self.window.send_to_embedder(EmbedderMsg::SetCursor(
self.window.webview_id(),
cursor.unwrap_or_default(),
));
}
fn handle_mouse_left_viewport_event(
@ -238,8 +240,9 @@ impl DocumentEventHandler {
}
if let Some(hit_test_result) = self
.window
.hit_test_from_point_in_viewport(self.most_recent_mousemove_point)
.most_recent_mousemove_point
.get()
.and_then(|point| self.window.hit_test_from_point_in_viewport(point))
{
MouseEvent::new_simple(
&self.window,
@ -263,15 +266,23 @@ impl DocumentEventHandler {
}
}
self.current_cursor.set(None);
self.current_hover_target.set(None);
// If focus is moving to another frame, it will decide what the new status text is, but if
// this mouse leave event is leaving the WebView entirely, then clear it.
// We do not want to always inform the embedder that cursor has been set to the
// default cursor, in order to avoid a timing issue when moving between `<iframe>`
// elements. There is currently no way to control which `SetCursor` message will
// reach the embedder first. This is safer when leaving the `WebView` entirely.
if !mouse_leave_event.focus_moving_to_another_iframe {
// If focus is moving to another frame, it will decide what the new status
// text is, but if this mouse leave event is leaving the WebView entirely,
// then clear it.
self.window
.send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None));
self.set_cursor(None);
} else {
self.current_cursor.set(None);
}
self.current_hover_target.set(None);
self.most_recent_mousemove_point.set(None);
}
fn handle_mouse_enter_leave_event(
@ -336,7 +347,7 @@ impl DocumentEventHandler {
};
// Update the cursor when the mouse moves, if it has changed.
self.set_cursor(hit_test_result.cursor);
self.set_cursor(Some(hit_test_result.cursor));
let Some(new_target) = hit_test_result
.node
@ -451,6 +462,8 @@ impl DocumentEventHandler {
.fire(new_target.upcast(), can_gc);
self.update_current_hover_target_and_status(Some(new_target));
self.most_recent_mousemove_point
.set(Some(hit_test_result.point_in_frame));
}
fn update_current_hover_target_and_status(&self, new_hover_target: Option<DomRoot<Element>>) {
@ -502,6 +515,21 @@ impl DocumentEventHandler {
}
}
pub(crate) fn handle_refresh_cursor(&self) {
let Some(most_recent_mousemove_point) = self.most_recent_mousemove_point.get() else {
return;
};
let Some(hit_test_result) = self
.window
.hit_test_from_point_in_viewport(most_recent_mousemove_point)
else {
return;
};
self.set_cursor(Some(hit_test_result.cursor));
}
/// <https://w3c.github.io/uievents/#mouseevent-algorithms>
/// Handles native mouse down, mouse up, mouse click.
fn handle_native_mouse_button_event(

View file

@ -3067,22 +3067,6 @@ impl Window {
node.dirty(NodeDamage::Other);
}
}
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()
.event_handler()
.set_cursor(hit_test_result.cursor);
}
}
impl Window {

View file

@ -91,7 +91,6 @@ use script_traits::{
use servo_config::{opts, prefs};
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;
@ -1884,8 +1883,8 @@ impl ScriptThread {
);
}
},
ScriptThreadMessage::RefreshCursor(pipeline_id, cursor_position) => {
self.handle_refresh_cursor(pipeline_id, cursor_position);
ScriptThreadMessage::RefreshCursor(pipeline_id) => {
self.handle_refresh_cursor(pipeline_id);
},
ScriptThreadMessage::PreferencesUpdated(updates) => {
let mut current_preferences = prefs::get().clone();
@ -3996,15 +3995,11 @@ impl ScriptThread {
));
}
fn handle_refresh_cursor(
&self,
pipeline_id: PipelineId,
cursor_position: Point2D<f32, CSSPixel>,
) {
let Some(window) = self.documents.borrow().find_window(pipeline_id) else {
fn handle_refresh_cursor(&self, pipeline_id: PipelineId) {
let Some(document) = self.documents.borrow().find_document(pipeline_id) else {
return;
};
window.handle_refresh_cursor(cursor_position);
document.event_handler().handle_refresh_cursor();
}
}