script: Move the majority of the input event handling code to DocumentEventHandler (#38584)

This moves the majority of the input event handler code to the
`DocumentEventHandler` helper structure. It better encapsulates event
handling, hiding most of the details from both `ScriptThread` and
`Document`. The benefit here is that the majority of the functions can
become private and `Document` is over 1000 lines shorter.

Testing: This should not change any behavior so is covered by existing
WPT tests.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-08-13 11:38:58 +02:00 committed by GitHub
parent bd9bb77295
commit 069ad40872
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1519 additions and 1484 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3164,7 +3164,9 @@ impl VirtualMethods for HTMLInputElement {
.borrow_mut()
.handle_clipboard_event(clipboard_event);
if reaction.contains(ClipboardEventReaction::FireClipboardChangedEvent) {
self.owner_document().fire_clipboardchange_event(can_gc);
self.owner_document()
.event_handler()
.fire_clipboardchange_event(can_gc);
}
if reaction.contains(ClipboardEventReaction::QueueInputEvent) {
self.owner_global()

View file

@ -703,7 +703,9 @@ impl VirtualMethods for HTMLTextAreaElement {
.borrow_mut()
.handle_clipboard_event(clipboard_event);
if reaction.contains(ClipboardEventReaction::FireClipboardChangedEvent) {
self.owner_document().fire_clipboardchange_event(can_gc);
self.owner_document()
.event_handler()
.fire_clipboardchange_event(can_gc);
}
if reaction.contains(ClipboardEventReaction::QueueInputEvent) {
self.owner_global()

View file

@ -10,6 +10,7 @@ use euclid::Point2D;
use js::rust::HandleObject;
use keyboard_types::Modifiers;
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
use script_traits::ConstellationInputEvent;
use servo_config::pref;
use style_traits::CSSPixel;
@ -22,6 +23,7 @@ use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto};
use crate::dom::bindings::root::{DomRoot, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::document::FireMouseEventType;
use crate::dom::event::{Event, EventBubbles, EventCancelable};
use crate::dom::eventtarget::EventTarget;
use crate::dom::inputevent::HitTestResult;
@ -181,6 +183,36 @@ impl MouseEvent {
ev
}
pub(crate) fn new_simple(
window: &Window,
event_name: FireMouseEventType,
can_bubble: EventBubbles,
cancelable: EventCancelable,
hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) -> DomRoot<Self> {
Self::new(
window,
DOMString::from(event_name.as_str()),
can_bubble,
cancelable,
Some(window),
0i32,
hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result
.point_relative_to_initial_containing_block
.to_i32(),
input_event.active_keyboard_modifiers,
0i16,
input_event.pressed_mouse_buttons,
None,
None,
can_gc,
)
}
/// <https://w3c.github.io/uievents/#initialize-a-mouseevent>
#[allow(clippy::too_many_arguments)]
pub(crate) fn initialize_mouse_event(

View file

@ -33,9 +33,9 @@ use devtools_traits::{ScriptToDevtoolsControlMsg, TimelineMarker, TimelineMarker
use dom_struct::dom_struct;
use embedder_traits::user_content_manager::{UserContentManager, UserScript};
use embedder_traits::{
AlertResponse, ConfirmResponse, EmbedderMsg, GamepadEvent, GamepadSupportedHapticEffects,
GamepadUpdateType, PromptResponse, SimpleDialog, Theme, UntrustedNodeAddress, ViewportDetails,
WebDriverJSError, WebDriverJSResult, WebDriverLoadStatus,
AlertResponse, ConfirmResponse, EmbedderMsg, PromptResponse, SimpleDialog, Theme,
UntrustedNodeAddress, ViewportDetails, WebDriverJSError, WebDriverJSResult,
WebDriverLoadStatus,
};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D};
use euclid::{Point2D, Scale, Size2D, Vector2D};
@ -69,8 +69,6 @@ use num_traits::ToPrimitive;
use profile_traits::ipc as ProfiledIpc;
use profile_traits::mem::ProfilerChan as MemProfilerChan;
use profile_traits::time::ProfilerChan as TimeProfilerChan;
use script_bindings::codegen::GenericBindings::NavigatorBinding::NavigatorMethods;
use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
use script_bindings::conversions::SafeToJSValConvertible;
use script_bindings::interfaces::WindowHelpers;
use script_bindings::root::Root;
@ -133,8 +131,6 @@ use crate::dom::document::{AnimationFrameCallback, Document};
use crate::dom::element::Element;
use crate::dom::event::{Event, EventBubbles, EventCancelable};
use crate::dom::eventtarget::EventTarget;
use crate::dom::gamepad::{Gamepad, contains_user_gesture};
use crate::dom::gamepadevent::GamepadEventType;
use crate::dom::globalscope::GlobalScope;
use crate::dom::hashchangeevent::HashChangeEvent;
use crate::dom::history::History;
@ -713,126 +709,6 @@ impl Window {
pub(crate) fn font_context(&self) -> &Arc<FontContext> {
&self.font_context
}
pub(crate) fn handle_gamepad_event(&self, gamepad_event: GamepadEvent) {
match gamepad_event {
GamepadEvent::Connected(index, name, bounds, supported_haptic_effects) => {
self.handle_gamepad_connect(
index.0,
name,
bounds.axis_bounds,
bounds.button_bounds,
supported_haptic_effects,
);
},
GamepadEvent::Disconnected(index) => {
self.handle_gamepad_disconnect(index.0);
},
GamepadEvent::Updated(index, update_type) => {
self.receive_new_gamepad_button_or_axis(index.0, update_type);
},
};
}
/// <https://www.w3.org/TR/gamepad/#dfn-gamepadconnected>
fn handle_gamepad_connect(
&self,
// As the spec actually defines how to set the gamepad index, the GilRs index
// is currently unused, though in practice it will almost always be the same.
// More infra is currently needed to track gamepads across windows.
_index: usize,
name: String,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
supported_haptic_effects: GamepadSupportedHapticEffects,
) {
// TODO: 2. If document is not null and is not allowed to use the "gamepad" permission,
// then abort these steps.
let this = Trusted::new(self);
self.upcast::<GlobalScope>()
.task_manager()
.gamepad_task_source()
.queue(task!(gamepad_connected: move || {
let window = this.root();
let navigator = window.Navigator();
let selected_index = navigator.select_gamepad_index();
let gamepad = Gamepad::new(
&window,
selected_index,
name,
"standard".into(),
axis_bounds,
button_bounds,
supported_haptic_effects,
false,
CanGc::note(),
);
navigator.set_gamepad(selected_index as usize, &gamepad, CanGc::note());
}));
}
/// <https://www.w3.org/TR/gamepad/#dfn-gamepaddisconnected>
fn handle_gamepad_disconnect(&self, index: usize) {
let this = Trusted::new(self);
self.upcast::<GlobalScope>()
.task_manager()
.gamepad_task_source()
.queue(task!(gamepad_disconnected: move || {
let window = this.root();
let navigator = window.Navigator();
if let Some(gamepad) = navigator.get_gamepad(index) {
if window.Document().is_fully_active() {
gamepad.update_connected(false, gamepad.exposed(), CanGc::note());
navigator.remove_gamepad(index);
}
}
}));
}
/// <https://www.w3.org/TR/gamepad/#receiving-inputs>
fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) {
let this = Trusted::new(self);
// <https://w3c.github.io/gamepad/#dfn-update-gamepad-state>
self.upcast::<GlobalScope>().task_manager().gamepad_task_source().queue(
task!(update_gamepad_state: move || {
let window = this.root();
let navigator = window.Navigator();
if let Some(gamepad) = navigator.get_gamepad(index) {
let current_time = window.Performance().Now();
gamepad.update_timestamp(*current_time);
match update_type {
GamepadUpdateType::Axis(index, value) => {
gamepad.map_and_normalize_axes(index, value);
},
GamepadUpdateType::Button(index, value) => {
gamepad.map_and_normalize_buttons(index, value);
}
};
if !navigator.has_gamepad_gesture() && contains_user_gesture(update_type) {
navigator.set_has_gamepad_gesture(true);
navigator.GetGamepads()
.iter()
.filter_map(|g| g.as_ref())
.for_each(|gamepad| {
gamepad.set_exposed(true);
gamepad.update_timestamp(*current_time);
let new_gamepad = Trusted::new(&**gamepad);
if window.Document().is_fully_active() {
window.upcast::<GlobalScope>().task_manager().gamepad_task_source().queue(
task!(update_gamepad_connect: move || {
let gamepad = new_gamepad.root();
gamepad.notify_event(GamepadEventType::Connected, CanGc::note());
})
);
}
});
}
}
})
);
}
}
// https://html.spec.whatwg.org/multipage/#atob
@ -3128,7 +3004,9 @@ impl Window {
return;
};
self.Document().set_cursor(hit_test_result.cursor);
self.Document()
.event_handler()
.set_cursor(hit_test_result.cursor);
}
}

View file

@ -358,7 +358,10 @@ pub(crate) fn follow_hyperlink(
let document = subject.owner_document();
let target_attribute_value =
if subject.is::<HTMLAreaElement>() || subject.is::<HTMLAnchorElement>() {
if document.alternate_action_keyboard_modifier_active() {
if document
.event_handler()
.alternate_action_keyboard_modifier_active()
{
Some("_blank".into())
} else {
get_element_target(subject)

View file

@ -123,7 +123,7 @@ use crate::dom::customelementregistry::{
CallbackReaction, CustomElementDefinition, CustomElementReactionStack,
};
use crate::dom::document::{
Document, DocumentSource, FocusInitiator, HasBrowsingContext, IsHTMLDocument, TouchEventResult,
Document, DocumentSource, FocusInitiator, HasBrowsingContext, IsHTMLDocument,
};
use crate::dom::element::Element;
use crate::dom::globalscope::GlobalScope;
@ -1039,16 +1039,6 @@ impl ScriptThread {
debug!("Stopped script thread.");
}
/// Process a compositor mouse move event.
fn process_mouse_move_event(
&self,
document: &Document,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
unsafe { document.handle_mouse_move_event(input_event, can_gc) }
}
/// Process compositor events as part of a "update the rendering task".
fn process_pending_input_events(&self, pipeline_id: PipelineId, can_gc: CanGc) {
let Some(document) = self.documents.borrow().find_document(pipeline_id) else {
@ -1062,97 +1052,10 @@ impl ScriptThread {
}
ScriptThread::set_user_interacting(true);
let window = document.window();
let _realm = enter_realm(document.window());
for event in document.take_pending_input_events().into_iter() {
document.update_active_keyboard_modifiers(event.active_keyboard_modifiers);
match event.event.clone() {
InputEvent::MouseButton(mouse_button_event) => {
document.handle_mouse_button_event(mouse_button_event, &event, can_gc);
},
InputEvent::MouseMove(_) => {
self.process_mouse_move_event(&document, &event, can_gc);
},
InputEvent::MouseLeave(mouse_leave_event) => {
document.handle_mouse_leave_event(&event, &mouse_leave_event, can_gc);
},
InputEvent::Touch(touch_event) => {
let touch_result = document.handle_touch_event(touch_event, &event, can_gc);
if let (TouchEventResult::Processed(handled), true) =
(touch_result, touch_event.is_cancelable())
{
let sequence_id = touch_event.expect_sequence_id();
let result = if handled {
embedder_traits::TouchEventResult::DefaultAllowed(
sequence_id,
touch_event.event_type,
)
} else {
embedder_traits::TouchEventResult::DefaultPrevented(
sequence_id,
touch_event.event_type,
)
};
let message = ScriptToConstellationMessage::TouchEventProcessed(result);
self.senders
.pipeline_to_constellation_sender
.send((pipeline_id, message))
.unwrap();
}
},
InputEvent::Wheel(wheel_event) => {
document.handle_wheel_event(wheel_event, &event, can_gc);
},
InputEvent::Keyboard(keyboard_event) => {
document.dispatch_key_event(keyboard_event, can_gc);
},
InputEvent::Ime(ime_event) => {
document.dispatch_ime_event(ime_event, can_gc);
},
InputEvent::Gamepad(gamepad_event) => {
window.handle_gamepad_event(gamepad_event);
},
InputEvent::EditingAction(editing_action_event) => {
document.handle_editing_action(editing_action_event, can_gc);
},
InputEvent::Scroll(scroll_event) => {
document.handle_embedder_scroll_event(scroll_event);
},
}
self.notify_webdriver_input_event_completed(pipeline_id, event.event, &document);
}
document.event_handler().handle_pending_input_events(can_gc);
ScriptThread::set_user_interacting(false);
}
fn notify_webdriver_input_event_completed(
&self,
pipeline_id: PipelineId,
event: InputEvent,
document: &Document,
) {
let Some(id) = event.webdriver_message_id() else {
return;
};
let sender = self.senders.pipeline_to_constellation_sender.clone();
// Webdriver should be notified once all current dom events have been processed.
document
.owner_global()
.task_manager()
.dom_manipulation_task_source()
.queue(task!(notify_webdriver_input_event_completed: move || {
if let Err(error) = sender.send((
pipeline_id,
ScriptToConstellationMessage::WebDriverInputComplete(id),
)) {
warn!("ScriptThread failed to send WebDriverInputComplete {id:?}: {error:?}",);
}
}));
}
fn cancel_scheduled_update_the_rendering(&self) {
if let Some(timer_id) = self.scheduled_update_the_rendering.borrow_mut().take() {
self.timer_scheduler.borrow_mut().cancel_timer(timer_id);
@ -3629,23 +3532,27 @@ impl ScriptThread {
(pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y).sqrt();
if pixel_dist < 10.0 * document.window().device_pixel_ratio().get() {
// Pass webdriver_id to the newly generated click event
document.note_pending_input_event(ConstellationInputEvent {
hit_test_result: event.hit_test_result.clone(),
pressed_mouse_buttons: event.pressed_mouse_buttons,
active_keyboard_modifiers: event.active_keyboard_modifiers,
event: event.event.clone().with_webdriver_message_id(None),
});
document.note_pending_input_event(ConstellationInputEvent {
hit_test_result: event.hit_test_result,
pressed_mouse_buttons: event.pressed_mouse_buttons,
active_keyboard_modifiers: event.active_keyboard_modifiers,
event: InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Click,
mouse_button_event.button,
mouse_button_event.point,
))
.with_webdriver_message_id(event.event.webdriver_message_id()),
});
document.event_handler().note_pending_input_event(
ConstellationInputEvent {
hit_test_result: event.hit_test_result.clone(),
pressed_mouse_buttons: event.pressed_mouse_buttons,
active_keyboard_modifiers: event.active_keyboard_modifiers,
event: event.event.clone().with_webdriver_message_id(None),
},
);
document.event_handler().note_pending_input_event(
ConstellationInputEvent {
hit_test_result: event.hit_test_result,
pressed_mouse_buttons: event.pressed_mouse_buttons,
active_keyboard_modifiers: event.active_keyboard_modifiers,
event: InputEvent::MouseButton(MouseButtonEvent::new(
MouseButtonAction::Click,
mouse_button_event.button,
mouse_button_event.point,
))
.with_webdriver_message_id(event.event.webdriver_message_id()),
},
);
return;
}
},
@ -3657,7 +3564,7 @@ impl ScriptThread {
}
}
document.note_pending_input_event(event);
document.event_handler().note_pending_input_event(event);
}
/// Handle a "navigate an iframe" message from the constellation.