/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::array::from_ref; use std::cell::{Cell, RefCell}; use std::cmp::Ordering; use std::f64::consts::PI; use std::mem; use std::rc::Rc; use std::str::FromStr; use std::time::{Duration, Instant}; use embedder_traits::{ Cursor, EditingActionEvent, EmbedderMsg, ImeEvent, InputEvent, InputEventId, InputEventOutcome, InputEventResult, KeyboardEvent as EmbedderKeyboardEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseLeftViewportEvent, TouchEvent as EmbedderTouchEvent, TouchEventType, TouchId, UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent, }; #[cfg(feature = "gamepad")] use embedder_traits::{ GamepadEvent as EmbedderGamepadEvent, GamepadSupportedHapticEffects, GamepadUpdateType, }; use euclid::{Point2D, Vector2D}; use js::jsapi::JSAutoRealm; use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey}; use layout_api::{ScrollContainerQueryFlags, node_id_from_scroll_id}; use rustc_hash::FxHashMap; use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; use script_bindings::codegen::GenericBindings::ElementBinding::ScrollLogicalPosition; use script_bindings::codegen::GenericBindings::EventBinding::EventMethods; use script_bindings::codegen::GenericBindings::HTMLElementBinding::HTMLElementMethods; use script_bindings::codegen::GenericBindings::HTMLLabelElementBinding::HTMLLabelElementMethods; use script_bindings::codegen::GenericBindings::KeyboardEventBinding::KeyboardEventMethods; use script_bindings::codegen::GenericBindings::NavigatorBinding::NavigatorMethods; use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods; use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods; use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods; use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods}; use script_bindings::inheritance::Castable; use script_bindings::match_domstring_ascii; use script_bindings::num::Finite; use script_bindings::reflector::DomObject; use script_bindings::root::{Dom, DomRoot, DomSlice}; use script_bindings::script_runtime::CanGc; use script_bindings::str::DOMString; use script_traits::ConstellationInputEvent; use servo_base::generic_channel::GenericCallback; use servo_config::pref; use servo_constellation_traits::{KeyboardScroll, ScriptToConstellationMessage}; use style::Atom; use style_traits::CSSPixel; use webrender_api::ExternalScrollId; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::root::MutNullableDom; use crate::dom::bindings::trace::NoTrace; use crate::dom::clipboardevent::ClipboardEventType; use crate::dom::document::FireMouseEventType; use crate::dom::document::focus::{FocusInitiator, FocusOperation, FocusableArea}; use crate::dom::event::{EventBubbles, EventCancelable, EventComposed, EventFlags}; #[cfg(feature = "gamepad")] use crate::dom::gamepad::gamepad::{Gamepad, contains_user_gesture}; #[cfg(feature = "gamepad")] use crate::dom::gamepad::gamepadevent::GamepadEventType; use crate::dom::inputevent::HitTestResult; use crate::dom::interactive_element_command::InteractiveElementCommand; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::node::{self, Node, NodeTraits, ShadowIncluding}; use crate::dom::pointerevent::{PointerEvent, PointerId}; use crate::dom::scrolling_box::{ScrollAxisState, ScrollRequirement, ScrollingBoxAxis}; use crate::dom::types::{ ClipboardEvent, CompositionEvent, DataTransfer, Element, Event, EventTarget, GlobalScope, HTMLAnchorElement, HTMLElement, HTMLLabelElement, MouseEvent, Touch, TouchEvent, TouchList, WheelEvent, Window, }; use crate::drag_data_store::{DragDataStore, Kind, Mode}; use crate::realms::enter_realm; /// A data structure used for tracking the current click count. This can be /// reset to 0 if a mouse button event happens at a sufficient distance or time /// from the previous one. /// /// From : /// > Implementations MUST maintain the current click count when generating mouse /// > events. This MUST be a non-negative integer indicating the number of consecutive /// > clicks of a pointing device button within a specific time. The delay after which /// > the count resets is specific to the environment configuration. #[derive(Default, JSTraceable, MallocSizeOf)] struct ClickCountingInfo { time: Option, #[no_trace] point: Option>, #[no_trace] button: Option, count: usize, } impl ClickCountingInfo { fn reset_click_count_if_necessary( &mut self, button: MouseButton, point_in_frame: Point2D, ) { let (Some(previous_button), Some(previous_point), Some(previous_time)) = (self.button, self.point, self.time) else { assert_eq!(self.count, 0); return; }; let double_click_timeout = Duration::from_millis(pref!(dom_document_dblclick_timeout) as u64); let double_click_distance_threshold = pref!(dom_document_dblclick_dist) as u64; // Calculate distance between this click and the previous click. let line = point_in_frame - previous_point; let distance = (line.dot(line) as f64).sqrt(); if previous_button != button || Instant::now().duration_since(previous_time) > double_click_timeout || distance > double_click_distance_threshold as f64 { self.count = 0; self.time = None; self.point = None; } } fn increment_click_count( &mut self, button: MouseButton, point: Point2D, ) -> usize { self.time = Some(Instant::now()); self.point = Some(point); self.button = Some(button); self.count += 1; self.count } } /// The [`DocumentEventHandler`] is a structure responsible for handling input events for /// the [`crate::Document`] and storing data related to event handling. It exists to /// decrease the size of the [`crate::Document`] structure. #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] pub(crate) struct DocumentEventHandler { /// The [`Window`] element for this [`DocumentEventHandler`]. window: Dom, /// Pending input events, to be handled at the next rendering opportunity. #[no_trace] #[ignore_malloc_size_of = "InputEvent contains data from outside crates"] pending_input_events: DomRefCell>, /// The index of the last mouse move event in the pending input events queue. mouse_move_event_index: DomRefCell>, /// The [`InputEventId`]s of mousemove events that have been coalesced. #[no_trace] #[ignore_malloc_size_of = "InputEventId contains data from outside crates"] coalesced_move_event_ids: DomRefCell>, /// The index of the last wheel event in the pending input events queue. /// This is non-standard behaviour. /// According to , /// we should only coalesce `pointermove` events. wheel_event_index: DomRefCell>, /// The [`InputEventId`]s of wheel events that have been coalesced. #[no_trace] #[ignore_malloc_size_of = "InputEventId contains data from outside crates"] coalesced_wheel_event_ids: DomRefCell>, /// click_counting_info: DomRefCell, #[no_trace] last_mouse_button_down_point: Cell>>, /// The number of currently down buttons, used to decide which kind /// of pointer event to dispatch on MouseDown/MouseUp. down_button_count: Cell, /// The element that is currently hovered by the cursor. current_hover_target: MutNullableDom, /// The element that was most recently clicked. most_recently_clicked_element: MutNullableDom, /// The most recent mouse movement point, used for processing `mouseleave` events. #[no_trace] most_recent_mousemove_point: Cell>>, /// The currently set [`Cursor`] or `None` if the `Document` isn't being hovered /// by the cursor. #[no_trace] current_cursor: Cell>, /// active_touch_points: DomRefCell>>, /// The active keyboard modifiers for the WebView. This is updated when receiving any input event. #[no_trace] active_keyboard_modifiers: Cell, /// Map from touch identifier to pointer ID for active touch points active_pointer_ids: DomRefCell>, /// Counter for generating unique pointer IDs for touch inputs next_touch_pointer_id: Cell, /// A map holding information about currently registered access key handlers. access_key_handlers: DomRefCell, Dom>>, /// sequential_focus_navigation_starting_point: MutNullableDom, } impl DocumentEventHandler { pub(crate) fn new(window: &Window) -> Self { Self { window: Dom::from_ref(window), pending_input_events: Default::default(), mouse_move_event_index: Default::default(), coalesced_move_event_ids: Default::default(), wheel_event_index: Default::default(), coalesced_wheel_event_ids: Default::default(), click_counting_info: Default::default(), last_mouse_button_down_point: Default::default(), down_button_count: Cell::new(0), current_hover_target: Default::default(), most_recently_clicked_element: Default::default(), most_recent_mousemove_point: Default::default(), current_cursor: Default::default(), active_touch_points: Default::default(), active_keyboard_modifiers: Default::default(), active_pointer_ids: Default::default(), next_touch_pointer_id: Cell::new(1), access_key_handlers: Default::default(), sequential_focus_navigation_starting_point: Default::default(), } } /// Note a pending input event, to be processed at the next `update_the_rendering` task. pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) { let mut pending_input_events = self.pending_input_events.borrow_mut(); if matches!(event.event.event, InputEvent::MouseMove(..)) { // First try to replace any existing mouse move event. if let Some(mouse_move_event) = self .mouse_move_event_index .borrow() .and_then(|index| pending_input_events.get_mut(index)) { self.coalesced_move_event_ids .borrow_mut() .push(mouse_move_event.event.id); *mouse_move_event = event; return; } *self.mouse_move_event_index.borrow_mut() = Some(pending_input_events.len()); } if let InputEvent::Wheel(ref new_wheel_event) = event.event.event { // Coalesce with any existing pending wheel event by summing deltas. if let Some(existing_constellation_wheel_event) = self .wheel_event_index .borrow() .and_then(|index| pending_input_events.get_mut(index)) { if let InputEvent::Wheel(ref mut existing_wheel_event) = existing_constellation_wheel_event.event.event { if existing_wheel_event.delta.mode == new_wheel_event.delta.mode { self.coalesced_wheel_event_ids .borrow_mut() .push(existing_constellation_wheel_event.event.id); existing_wheel_event.delta.x += new_wheel_event.delta.x; existing_wheel_event.delta.y += new_wheel_event.delta.y; existing_wheel_event.delta.z += new_wheel_event.delta.z; existing_wheel_event.point = new_wheel_event.point; existing_constellation_wheel_event.event.id = event.event.id; return; } } } *self.wheel_event_index.borrow_mut() = Some(pending_input_events.len()); } pending_input_events.push(event); } /// Whether or not this [`Document`] has any pending input events to be processed during /// "update the rendering." pub(crate) fn has_pending_input_events(&self) -> bool { !self.pending_input_events.borrow().is_empty() } pub(crate) fn alternate_action_keyboard_modifier_active(&self) -> bool { #[cfg(target_os = "macos")] { self.active_keyboard_modifiers .get() .contains(Modifiers::META) } #[cfg(not(target_os = "macos"))] { self.active_keyboard_modifiers .get() .contains(Modifiers::CONTROL) } } pub(crate) fn handle_pending_input_events(&self, can_gc: CanGc) { debug_assert!( !self.pending_input_events.borrow().is_empty(), "handle_pending_input_events called with no events" ); let _realm = enter_realm(&*self.window); // Reset the mouse and wheel event indices. *self.mouse_move_event_index.borrow_mut() = None; *self.wheel_event_index.borrow_mut() = None; let pending_input_events = mem::take(&mut *self.pending_input_events.borrow_mut()); let mut coalesced_move_event_ids = mem::take(&mut *self.coalesced_move_event_ids.borrow_mut()); let mut coalesced_wheel_event_ids = mem::take(&mut *self.coalesced_wheel_event_ids.borrow_mut()); let mut input_event_outcomes = Vec::with_capacity( pending_input_events.len() + coalesced_move_event_ids.len() + coalesced_wheel_event_ids.len(), ); // TODO: For some of these we still aren't properly calculating whether or not // the event was handled or if `preventDefault()` was called on it. Each of // these cases needs to be examined and some of them either fire more than one // event or fire events later. We have to make a good decision about what to // return to the embedder when that happens. for event in pending_input_events { self.active_keyboard_modifiers .set(event.active_keyboard_modifiers); let result = match event.event.event { InputEvent::MouseButton(mouse_button_event) => { self.handle_native_mouse_button_event(mouse_button_event, &event, can_gc); InputEventResult::default() }, InputEvent::MouseMove(_) => { self.handle_native_mouse_move_event(&event, can_gc); input_event_outcomes.extend( mem::take(&mut coalesced_move_event_ids) .into_iter() .map(|id| InputEventOutcome { id, result: InputEventResult::default(), }), ); InputEventResult::default() }, InputEvent::MouseLeftViewport(mouse_leave_event) => { self.handle_mouse_left_viewport_event(&event, &mouse_leave_event, can_gc); InputEventResult::default() }, InputEvent::Touch(touch_event) => { self.handle_touch_event(touch_event, &event, can_gc) }, InputEvent::Wheel(wheel_event) => { let result = self.handle_wheel_event(wheel_event, &event, can_gc); input_event_outcomes.extend( mem::take(&mut coalesced_wheel_event_ids) .into_iter() .map(|id| InputEventOutcome { id, result }), ); result }, InputEvent::Keyboard(keyboard_event) => { self.handle_keyboard_event(keyboard_event, can_gc) }, InputEvent::Ime(ime_event) => self.handle_ime_event(ime_event, can_gc), #[cfg(feature = "gamepad")] InputEvent::Gamepad(gamepad_event) => { self.handle_gamepad_event(gamepad_event); InputEventResult::default() }, InputEvent::EditingAction(editing_action_event) => { self.handle_editing_action(None, editing_action_event, can_gc) }, }; input_event_outcomes.push(InputEventOutcome { id: event.event.id, result, }); } self.notify_embedder_that_events_were_handled(input_event_outcomes); } fn notify_embedder_that_events_were_handled( &self, input_event_outcomes: Vec, ) { // Wait to to notify the embedder that the event was handled until all pending DOM // event processing is finished. let trusted_window = Trusted::new(&*self.window); self.window .as_global_scope() .task_manager() .dom_manipulation_task_source() .queue(task!(notify_webdriver_input_event_completed: move || { let window = trusted_window.root(); window.send_to_embedder( EmbedderMsg::InputEventsHandled(window.webview_id(), input_event_outcomes)); })); } /// When an event should be fired on the element that has focus, this returns the target. If /// there is no associated element with the focused area (such as when the viewport is focused), /// then the body is returned. If no body is returned then the `Window` is returned. fn target_for_events_following_focus(&self) -> DomRoot { let document = self.window.Document(); match &*document.focus_handler().focused_area() { FocusableArea::Node { node, .. } => DomRoot::from_ref(node.upcast()), FocusableArea::Viewport => document .GetBody() .map(DomRoot::upcast) .unwrap_or_else(|| DomRoot::from_ref(self.window.upcast())), } } pub(crate) fn set_cursor(&self, cursor: Option) { if cursor == self.current_cursor.get() { return; } 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( &self, input_event: &ConstellationInputEvent, mouse_leave_event: &MouseLeftViewportEvent, can_gc: CanGc, ) { if let Some(current_hover_target) = self.current_hover_target.get() { let current_hover_target = current_hover_target.upcast::(); for element in current_hover_target .inclusive_ancestors(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::) { element.set_hover_state(false); self.element_for_activation(element).set_active_state(false); } if let Some(hit_test_result) = self .most_recent_mousemove_point .get() .and_then(|point| self.window.hit_test_from_point_in_viewport(point)) { let mouse_out_event = MouseEvent::new_for_platform_motion_event( &self.window, FireMouseEventType::Out, &hit_test_result, input_event, can_gc, ); // Fire pointerout before mouseout mouse_out_event .to_pointer_hover_event("pointerout", can_gc) .upcast::() .fire(current_hover_target.upcast(), can_gc); mouse_out_event .upcast::() .fire(current_hover_target.upcast(), can_gc); self.handle_mouse_enter_leave_event( DomRoot::from_ref(current_hover_target), None, FireMouseEventType::Leave, &hit_test_result, input_event, can_gc, ); } } // 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 `