diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index a6e49a93639..f7568844ac1 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -7,13 +7,10 @@ use std::cmp::Ordering; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::{HashMap, HashSet, VecDeque}; use std::default::Default; -use std::f64::consts::PI; -use std::mem; use std::rc::Rc; -use std::slice::from_ref; use std::str::FromStr; use std::sync::{LazyLock, Mutex}; -use std::time::{Duration, Instant}; +use std::time::Duration; use base::cross_process_instant::CrossProcessInstant; use base::id::WebViewId; @@ -27,12 +24,7 @@ use cssparser::match_ignore_ascii_case; use data_url::mime::Mime; use devtools_traits::ScriptToDevtoolsControlMsg; use dom_struct::dom_struct; -use embedder_traits::{ - AllowOrDeny, AnimationState, ContextMenuResult, Cursor, EditingActionEvent, EmbedderMsg, - FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction, - MouseButtonEvent, MouseLeaveEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId, - UntrustedNodeAddress, WheelEvent, -}; +use embedder_traits::{AllowOrDeny, AnimationState, EmbedderMsg, FocusSequenceNumber, LoadStatus}; use encoding_rs::{Encoding, UTF_8}; use euclid::Point2D; use euclid::default::{Rect, Size2D}; @@ -41,11 +33,7 @@ use html5ever::{LocalName, Namespace, QualName, local_name, ns}; use hyper_serde::Serde; use ipc_channel::ipc; use js::rust::{HandleObject, HandleValue, MutableHandleValue}; -use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey}; -use layout_api::{ - PendingRestyle, ReflowGoal, ReflowPhasesRun, RestyleReason, TrustedNodeAddress, - node_id_from_scroll_id, -}; +use layout_api::{PendingRestyle, ReflowGoal, ReflowPhasesRun, RestyleReason, TrustedNodeAddress}; use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics}; use net_traits::CookieSource::NonHTTP; use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl}; @@ -61,7 +49,7 @@ use regex::bytes::Regex; use script_bindings::codegen::GenericBindings::ElementBinding::ElementMethods; use script_bindings::interfaces::DocumentHelpers; use script_bindings::script_runtime::JSContext; -use script_traits::{ConstellationInputEvent, DocumentActivity, ProgressiveWebMetricType}; +use script_traits::{DocumentActivity, ProgressiveWebMetricType}; use servo_arc::Arc; use servo_config::pref; use servo_media::{ClientContextId, ServoMedia}; @@ -74,7 +62,6 @@ use style::shared_lock::SharedRwLock as StyleSharedRwLock; use style::str::{split_html_space_chars, str_join}; use style::stylesheet_set::DocumentStylesheetSet; use style::stylesheets::{Origin, OriginSet, Stylesheet}; -use style_traits::CSSPixel; use stylo_atoms::Atom; use url::Host; use uuid::Uuid; @@ -107,7 +94,6 @@ use crate::dom::bindings::codegen::Bindings::NodeFilterBinding::NodeFilter; use crate::dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceMethods; use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionName; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMethods; -use crate::dom::bindings::codegen::Bindings::TouchBinding::TouchMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::{ FrameRequestCallback, ScrollBehavior, ScrollOptions, WindowMethods, }; @@ -126,7 +112,7 @@ use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementType use crate::dom::bindings::num::Finite; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto}; -use crate::dom::bindings::root::{Dom, DomRoot, DomSlice, LayoutDom, MutNullableDom, ToLayout}; +use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom, ToLayout}; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::trace::{HashMapTracedValues, NoTrace}; #[cfg(feature = "webgpu")] @@ -134,13 +120,11 @@ use crate::dom::bindings::weakref::WeakRef; use crate::dom::bindings::xmlname::matches_name_production; use crate::dom::canvasrenderingcontext2d::CanvasRenderingContext2D; use crate::dom::cdatasection::CDATASection; -use crate::dom::clipboardevent::{ClipboardEvent, ClipboardEventType}; use crate::dom::comment::Comment; use crate::dom::compositionevent::CompositionEvent; use crate::dom::cssstylesheet::CSSStyleSheet; use crate::dom::customelementregistry::CustomElementDefinition; use crate::dom::customevent::CustomEvent; -use crate::dom::datatransfer::DataTransfer; use crate::dom::document_event_handler::DocumentEventHandler; use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentorshadowroot::{ @@ -152,7 +136,7 @@ use crate::dom::element::{ CustomElementCreationMode, Element, ElementCreator, ElementPerformFullscreenEnter, ElementPerformFullscreenExit, }; -use crate::dom::event::{Event, EventBubbles, EventCancelable, EventDefault}; +use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; use crate::dom::focusevent::FocusEvent; use crate::dom::fontfaceset::FontFaceSet; @@ -173,21 +157,19 @@ use crate::dom::htmlinputelement::HTMLInputElement; use crate::dom::htmlscriptelement::{HTMLScriptElement, ScriptResult}; use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::htmltitleelement::HTMLTitleElement; -use crate::dom::inputevent::HitTestResult; use crate::dom::intersectionobserver::IntersectionObserver; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::location::{Location, NavigationType}; use crate::dom::messageevent::MessageEvent; use crate::dom::mouseevent::MouseEvent; use crate::dom::node::{ - self, CloneChildrenFlag, Node, NodeDamage, NodeFlags, NodeTraits, ShadowIncluding, + CloneChildrenFlag, Node, NodeDamage, NodeFlags, NodeTraits, ShadowIncluding, }; use crate::dom::nodeiterator::NodeIterator; use crate::dom::nodelist::NodeList; use crate::dom::pagetransitionevent::PageTransitionEvent; use crate::dom::performanceentry::PerformanceEntry; use crate::dom::performancepainttiming::PerformancePaintTiming; -use crate::dom::pointerevent::{PointerEvent, PointerId}; use crate::dom::processinginstruction::ProcessingInstruction; use crate::dom::promise::Promise; use crate::dom::range::Range; @@ -198,7 +180,6 @@ use crate::dom::shadowroot::ShadowRoot; use crate::dom::storageevent::StorageEvent; use crate::dom::stylesheetlist::{StyleSheetList, StyleSheetListOwner}; use crate::dom::text::Text; -use crate::dom::touch::Touch; use crate::dom::touchevent::TouchEvent as DomTouchEvent; use crate::dom::touchlist::TouchList; use crate::dom::treewalker::TreeWalker; @@ -209,11 +190,9 @@ use crate::dom::virtualmethods::vtable_for; use crate::dom::webglrenderingcontext::WebGLRenderingContext; #[cfg(feature = "webgpu")] use crate::dom::webgpu::gpucanvascontext::GPUCanvasContext; -use crate::dom::wheelevent::WheelEvent as DomWheelEvent; use crate::dom::window::Window; use crate::dom::windowproxy::WindowProxy; use crate::dom::xpathevaluator::XPathEvaluator; -use crate::drag_data_store::{DragDataStore, Kind, Mode}; use crate::fetch::FetchCanceller; use crate::iframe_collection::IFrameCollection; use crate::image_animation::ImageAnimationManager; @@ -405,8 +384,6 @@ pub(crate) struct Document { /// be restyled. #[no_trace] needs_restyle: Cell, - /// - active_touch_points: DomRefCell>>, /// Navigation Timing properties: /// #[no_trace] @@ -440,10 +417,6 @@ pub(crate) struct Document { /// #[no_trace] policy_container: DomRefCell, - /// - #[ignore_malloc_size_of = "Defined in std"] - #[no_trace] - last_click_info: DomRefCell)>>, /// ignore_destructive_writes_counter: Cell, /// @@ -526,12 +499,6 @@ pub(crate) struct Document { dirty_root: MutNullableDom, /// declarative_refresh: DomRefCell>, - /// Pending input events, to be handled at the next rendering opportunity. - #[no_trace] - #[ignore_malloc_size_of = "CompositorEvent contains data from outside crates"] - pending_input_events: DomRefCell>, - /// The index of the last mouse move event in the pending compositor events queue. - mouse_move_event_index: DomRefCell>, /// /// /// Note: we are storing, but never removing, resize observers. @@ -571,9 +538,6 @@ pub(crate) struct Document { /// The lifetime of an intersection observer is specified at /// . intersection_observers: DomRefCell>>, - /// The active keyboard modifiers for the WebView. This is updated when receiving any input event. - #[no_trace] - active_keyboard_modifiers: Cell, /// The node that is currently highlighted by the devtools highlighted_dom_node: MutNullableDom, /// The constructed stylesheet that is adopted by this [Document]. @@ -1165,9 +1129,13 @@ impl Document { self.focus_sequence.get() } + pub(crate) fn has_focus_transaction(&self) -> bool { + self.focus_transaction.borrow().is_some() + } + /// Initiate a new round of checking for elements requesting focus. The last element to call /// `request_focus` before `commit_focus_transaction` is called will receive focus. - fn begin_focus_transaction(&self) { + pub(crate) fn begin_focus_transaction(&self) { // Initialize it with the current state *self.focus_transaction.borrow_mut() = Some(FocusTransaction { element: self.focused.get().as_deref().map(Dom::from_ref), @@ -1267,7 +1235,7 @@ impl Document { /// Reassign the focus context to the element that last requested focus during this /// transaction, or the document if no elements requested it. - fn commit_focus_transaction(&self, focus_initiator: FocusInitiator, can_gc: CanGc) { + pub(crate) fn commit_focus_transaction(&self, focus_initiator: FocusInitiator, can_gc: CanGc) { let (mut new_focused, new_focus_state) = { let focus_transaction = self.focus_transaction.borrow(); let focus_transaction = focus_transaction @@ -1546,953 +1514,6 @@ impl Document { } } - /// - pub(crate) fn handle_mouse_button_event( - &self, - event: MouseButtonEvent, - input_event: &ConstellationInputEvent, - can_gc: CanGc, - ) { - // Ignore all incoming events without a hit test. - let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { - return; - }; - - debug!( - "{:?}: at {:?}", - event.action, hit_test_result.point_in_frame - ); - - let Some(el) = hit_test_result - .node - .inclusive_ancestors(ShadowIncluding::Yes) - .filter_map(DomRoot::downcast::) - .next() - else { - return; - }; - - let node = el.upcast::(); - debug!("{:?} on {:?}", event.action, node.debug_str()); - // Prevent click event if form control element is disabled. - if let MouseButtonAction::Click = event.action { - // The click event is filtered by the disabled state. - if el.is_actually_disabled() { - return; - } - } - - let dom_event = DomRoot::upcast::(MouseEvent::for_platform_mouse_event( - event, - input_event.pressed_mouse_buttons, - &self.window, - &hit_test_result, - input_event.active_keyboard_modifiers, - can_gc, - )); - - let activatable = el.as_maybe_activatable(); - match event.action { - // https://w3c.github.io/uievents/#handle-native-mouse-click - MouseButtonAction::Click => { - el.set_click_in_progress(true); - dom_event.dispatch(node.upcast(), false, can_gc); - el.set_click_in_progress(false); - - self.maybe_fire_dblclick(node, &hit_test_result, input_event, can_gc); - }, - // https://w3c.github.io/uievents/#handle-native-mouse-down - MouseButtonAction::Down => { - // (TODO) Step 6. Maybe send pointerdown event with `dom_event`. - if let Some(a) = activatable { - a.enter_formal_activation_state(); - } - - // For a node within a text input UA shadow DOM, - // delegate the focus target into its shadow host. - // TODO: This focus delegation should be done - // with shadow DOM delegateFocus attribute. - let target_el = el.find_focusable_shadow_host_if_necessary(); - self.begin_focus_transaction(); - // Try to focus `el`. If it's not focusable, focus the document instead. - self.request_focus(None, FocusInitiator::Local, can_gc); - self.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc); - - // Step 7. Let result = dispatch event at target - let result = dom_event.dispatch(node.upcast(), false, can_gc); - - // Step 8. If result is true and target is a focusable area - // that is click focusable, then Run the focusing steps at target. - if result && self.focus_transaction.borrow().is_some() { - self.commit_focus_transaction(FocusInitiator::Local, can_gc); - } - - // Step 9. If mbutton is the secondary mouse button, then - // Maybe show context menu with native, target. - if let (MouseButtonAction::Down, MouseButton::Right) = (event.action, event.button) - { - self.maybe_show_context_menu( - node.upcast(), - &hit_test_result, - input_event, - can_gc, - ); - } - }, - // https://w3c.github.io/uievents/#handle-native-mouse-up - MouseButtonAction::Up => { - if let Some(a) = activatable { - a.exit_formal_activation_state(); - } - // (TODO) Step 6. Maybe send pointerup event with `dom_event``. - - // Step 7. dispatch event at target. - dom_event.dispatch(node.upcast(), false, can_gc); - }, - } - } - - /// - fn maybe_show_context_menu( - &self, - target: &EventTarget, - hit_test_result: &HitTestResult, - input_event: &ConstellationInputEvent, - can_gc: CanGc, - ) { - // - let menu_event = PointerEvent::new( - &self.window, // window - DOMString::from("contextmenu"), // type - EventBubbles::Bubbles, // can_bubble - EventCancelable::Cancelable, // cancelable - Some(&self.window), // view - 0, // detail - 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, - 2i16, // button, right mouse button - input_event.pressed_mouse_buttons, - None, // related_target - None, // point_in_target - PointerId::Mouse as i32, // pointer_id - 1, // width - 1, // height - 0.5, // pressure - 0.0, // tangential_pressure - 0, // tilt_x - 0, // tilt_y - 0, // twist - PI / 2.0, // altitude_angle - 0.0, // azimuth_angle - DOMString::from("mouse"), // pointer_type - true, // is_primary - vec![], // coalesced_events - vec![], // predicted_events - can_gc, - ); - - // Step 3. Let result = dispatch menuevent at target. - let result = menu_event.upcast::().fire(target, can_gc); - - // Step 4. If result is true, then show the UA context menu - if result { - let (sender, receiver) = - ipc::channel::().expect("Failed to create IPC channel."); - self.send_to_embedder(EmbedderMsg::ShowContextMenu( - self.webview_id(), - sender, - None, - vec![], - )); - let _ = receiver.recv().unwrap(); - }; - } - - fn maybe_fire_dblclick( - &self, - target: &Node, - hit_test_result: &HitTestResult, - input_event: &ConstellationInputEvent, - can_gc: CanGc, - ) { - // https://w3c.github.io/uievents/#event-type-dblclick - let now = Instant::now(); - let point_in_frame = hit_test_result.point_in_frame; - let opt = self.last_click_info.borrow_mut().take(); - - if let Some((last_time, last_pos)) = opt { - let DBL_CLICK_TIMEOUT = - Duration::from_millis(pref!(dom_document_dblclick_timeout) as u64); - let DBL_CLICK_DIST_THRESHOLD = pref!(dom_document_dblclick_dist) as u64; - - // Calculate distance between this click and the previous click. - let line = point_in_frame - last_pos; - let dist = (line.dot(line) as f64).sqrt(); - - if now.duration_since(last_time) < DBL_CLICK_TIMEOUT && - dist < DBL_CLICK_DIST_THRESHOLD as f64 - { - // A double click has occurred if this click is within a certain time and dist. of previous click. - let click_count = 2; - - let event = MouseEvent::new( - &self.window, - DOMString::from("dblclick"), - EventBubbles::Bubbles, - EventCancelable::Cancelable, - Some(&self.window), - click_count, - point_in_frame.to_i32(), - 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, - ); - event.upcast::().fire(target.upcast(), can_gc); - - // When a double click occurs, self.last_click_info is left as None so that a - // third sequential click will not cause another double click. - return; - } - } - - // Update last_click_info with the time and position of the click. - *self.last_click_info.borrow_mut() = Some((now, point_in_frame)); - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn fire_mouse_event( - &self, - target: &EventTarget, - event_name: FireMouseEventType, - can_bubble: EventBubbles, - cancelable: EventCancelable, - hit_test_result: &HitTestResult, - input_event: &ConstellationInputEvent, - can_gc: CanGc, - ) { - MouseEvent::new( - &self.window, - DOMString::from(event_name.as_str()), - can_bubble, - cancelable, - Some(&self.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, - ) - .upcast::() - .fire(target, can_gc); - } - - pub(crate) fn handle_editing_action(&self, action: EditingActionEvent, can_gc: CanGc) -> bool { - let clipboard_event = match action { - EditingActionEvent::Copy => ClipboardEventType::Copy, - EditingActionEvent::Cut => ClipboardEventType::Cut, - EditingActionEvent::Paste => ClipboardEventType::Paste, - }; - self.handle_clipboard_action(clipboard_event, can_gc) - } - - /// - fn handle_clipboard_action(&self, action: ClipboardEventType, can_gc: CanGc) -> bool { - // The script_triggered flag is set if the action runs because of a script, e.g. document.execCommand() - let script_triggered = false; - - // The script_may_access_clipboard flag is set - // if action is paste and the script thread is allowed to read from clipboard or - // if action is copy or cut and the script thread is allowed to modify the clipboard - let script_may_access_clipboard = false; - - // Step 1 If the script-triggered flag is set and the script-may-access-clipboard flag is unset - if script_triggered && !script_may_access_clipboard { - return false; - } - - // Step 2 Fire a clipboard event - let event = ClipboardEvent::new( - &self.window, - None, - DOMString::from(action.as_str()), - EventBubbles::Bubbles, - EventCancelable::Cancelable, - None, - can_gc, - ); - self.fire_clipboard_event(&event, action, can_gc); - - // Step 3 If a script doesn't call preventDefault() - // the event will be handled inside target's VirtualMethods::handle_event - - let e = event.upcast::(); - - if !e.IsTrusted() { - return false; - } - - // Step 4 If the event was canceled, then - if e.DefaultPrevented() { - match e.Type().str() { - "copy" => { - // Step 4.1 Call the write content to the clipboard algorithm, - // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. - if let Some(clipboard_data) = event.get_clipboard_data() { - let drag_data_store = - clipboard_data.data_store().expect("This shouldn't fail"); - self.write_content_to_the_clipboard(&drag_data_store); - } - }, - "cut" => { - // Step 4.1 Call the write content to the clipboard algorithm, - // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. - if let Some(clipboard_data) = event.get_clipboard_data() { - let drag_data_store = - clipboard_data.data_store().expect("This shouldn't fail"); - self.write_content_to_the_clipboard(&drag_data_store); - } - - // Step 4.2 Fire a clipboard event named clipboardchange - self.fire_clipboardchange_event(can_gc); - }, - "paste" => return false, - _ => (), - } - } - //Step 5 - true - } - - /// - fn fire_clipboard_event( - &self, - event: &ClipboardEvent, - action: ClipboardEventType, - can_gc: CanGc, - ) { - // Step 1 Let clear_was_called be false - // Step 2 Let types_to_clear an empty list - let mut drag_data_store = DragDataStore::new(); - - // Step 4 let clipboard-entry be the sequence number of clipboard content, null if the OS doesn't support it. - - // Step 5 let trusted be true if the event is generated by the user agent, false otherwise - let trusted = true; - - // Step 6 if the context is editable: - let focused = self.get_focused_element(); - let body = self.GetBody(); - - let target = match (&focused, &body) { - (Some(focused), _) => focused.upcast(), - (&None, Some(body)) => body.upcast(), - (&None, &None) => self.window.upcast(), - }; - // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70 - - // Step 7 - match action { - ClipboardEventType::Copy | ClipboardEventType::Cut => { - // Step 7.2.1 - drag_data_store.set_mode(Mode::ReadWrite); - }, - ClipboardEventType::Paste => { - let (sender, receiver) = ipc::channel().unwrap(); - self.window - .send_to_constellation(ScriptToConstellationMessage::ForwardToEmbedder( - EmbedderMsg::GetClipboardText(self.window.webview_id(), sender), - )); - let text_contents = receiver - .recv() - .map(Result::unwrap_or_default) - .unwrap_or_default(); - - // Step 7.1.1 - drag_data_store.set_mode(Mode::ReadOnly); - // Step 7.1.2 If trusted or the implementation gives script-generated events access to the clipboard - if trusted { - // Step 7.1.2.1 For each clipboard-part on the OS clipboard: - - // Step 7.1.2.1.1 If clipboard-part contains plain text, then - let data = DOMString::from(text_contents.to_string()); - let type_ = DOMString::from("text/plain"); - let _ = drag_data_store.add(Kind::Text { data, type_ }); - - // Step 7.1.2.1.2 TODO If clipboard-part represents file references, then for each file reference - // Step 7.1.2.1.3 TODO If clipboard-part contains HTML- or XHTML-formatted text then - - // Step 7.1.3 Update clipboard-event-data’s files to match clipboard-event-data’s items - // Step 7.1.4 Update clipboard-event-data’s types to match clipboard-event-data’s items - } - }, - ClipboardEventType::Change => (), - } - - // Step 3 - let clipboard_event_data = DataTransfer::new( - &self.window, - Rc::new(RefCell::new(Some(drag_data_store))), - can_gc, - ); - - // Step 8 - event.set_clipboard_data(Some(&clipboard_event_data)); - let event = event.upcast::(); - // Step 9 - event.set_trusted(trusted); - // Step 10 Set event’s composed to true. - event.set_composed(true); - // Step 11 - event.dispatch(target, false, can_gc); - } - - pub(crate) fn fire_clipboardchange_event(&self, can_gc: CanGc) { - let clipboardchange_event = ClipboardEvent::new( - &self.window, - None, - DOMString::from("clipboardchange"), - EventBubbles::Bubbles, - EventCancelable::Cancelable, - None, - can_gc, - ); - self.fire_clipboard_event(&clipboardchange_event, ClipboardEventType::Change, can_gc); - } - - /// - fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) { - // Step 1 - if drag_data_store.list_len() > 0 { - // Step 1.1 Clear the clipboard. - self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id())); - // Step 1.2 - for item in drag_data_store.iter_item_list() { - match item { - Kind::Text { data, .. } => { - // Step 1.2.1.1 Ensure encoding is correct per OS and locale conventions - // Step 1.2.1.2 Normalize line endings according to platform conventions - // Step 1.2.1.3 - self.send_to_embedder(EmbedderMsg::SetClipboardText( - self.webview_id(), - data.to_string(), - )); - }, - Kind::File { .. } => { - // Step 1.2.2 If data is of a type listed in the mandatory data types list, then - // Step 1.2.2.1 Place part on clipboard with the appropriate OS clipboard format description - // Step 1.2.3 Else this is left to the implementation - }, - } - } - } else { - // Step 2.1 - if drag_data_store.clear_was_called { - // Step 2.1.1 If types-to-clear list is empty, clear the clipboard - self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id())); - // Step 2.1.2 Else remove the types in the list from the clipboard - // As of now this can't be done with Arboard, and it's possible that will be removed from the spec - } - } - } - - #[allow(unsafe_code)] - pub(crate) unsafe fn handle_mouse_move_event( - &self, - input_event: &ConstellationInputEvent, - can_gc: CanGc, - ) { - // Ignore all incoming events without a hit test. - let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { - return; - }; - - // Update the cursor when the mouse moves, if it has changed. - self.set_cursor(hit_test_result.cursor); - - let Some(new_target) = hit_test_result - .node - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - .next() - else { - return; - }; - - let target_has_changed = self - .event_handler - .current_hover_target - .get() - .as_ref() - .is_none_or(|old_target| old_target != &new_target); - - // Here we know the target has changed, so we must update the state, - // dispatch mouseout to the previous one, mouseover to the new one. - if target_has_changed { - // Dispatch mouseout and mouseleave to previous target. - if let Some(old_target) = self.event_handler.current_hover_target.get() { - let old_target_is_ancestor_of_new_target = old_target - .upcast::() - .is_ancestor_of(new_target.upcast::()); - - // If the old target is an ancestor of the new target, this can be skipped - // completely, since the node's hover state will be reset below. - if !old_target_is_ancestor_of_new_target { - for element in old_target - .upcast::() - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - { - element.set_hover_state(false); - element.set_active_state(false); - } - } - - self.fire_mouse_event( - old_target.upcast(), - FireMouseEventType::Out, - EventBubbles::Bubbles, - EventCancelable::Cancelable, - &hit_test_result, - input_event, - can_gc, - ); - - if !old_target_is_ancestor_of_new_target { - let event_target = DomRoot::from_ref(old_target.upcast::()); - let moving_into = Some(DomRoot::from_ref(new_target.upcast::())); - self.handle_mouse_enter_leave_event( - event_target, - moving_into, - FireMouseEventType::Leave, - &hit_test_result, - input_event, - can_gc, - ); - } - } - - // Dispatch mouseover and mouseenter to new target. - for element in new_target - .upcast::() - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - { - element.set_hover_state(true); - } - - self.fire_mouse_event( - new_target.upcast(), - FireMouseEventType::Over, - EventBubbles::Bubbles, - EventCancelable::Cancelable, - &hit_test_result, - input_event, - can_gc, - ); - - let moving_from = self - .event_handler - .current_hover_target - .get() - .map(|old_target| DomRoot::from_ref(old_target.upcast::())); - let event_target = DomRoot::from_ref(new_target.upcast::()); - self.handle_mouse_enter_leave_event( - event_target, - moving_from, - FireMouseEventType::Enter, - &hit_test_result, - input_event, - can_gc, - ); - } - - // Send mousemove event to topmost target, unless it's an iframe, in which case the - // compositor should have also sent an event to the inner document. - self.fire_mouse_event( - new_target.upcast(), - FireMouseEventType::Move, - EventBubbles::Bubbles, - EventCancelable::Cancelable, - &hit_test_result, - input_event, - can_gc, - ); - - self.update_current_hover_target_and_status(Some(new_target)); - } - - fn update_current_hover_target_and_status(&self, new_hover_target: Option>) { - let previous_hover_target = self.event_handler.current_hover_target.get(); - if previous_hover_target == new_hover_target { - return; - } - - self.event_handler - .current_hover_target - .set(new_hover_target.as_deref()); - - // If the new hover target is an anchor with a status value, inform the embedder - // of the new value. - let window = self.window(); - if let Some(anchor) = new_hover_target.and_then(|new_hover_target| { - new_hover_target - .upcast::() - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - .next() - }) { - let status = anchor - .upcast::() - .get_attribute(&ns!(), &local_name!("href")) - .and_then(|href| { - let value = href.value(); - let url = self.url(); - url.join(&value).map(|url| url.to_string()).ok() - }); - window.send_to_embedder(EmbedderMsg::Status(self.webview_id(), status)); - return; - } - - // No state was set above, which means that the new value of the status in the embedder - // should be `None`. Set that now. If `previous_hover_target` is `None` that means this - // is the first mouse move event we are seeing after getting the cursor. In that case, - // we also clear the status. - if previous_hover_target.is_none_or(|previous_hover_target| { - previous_hover_target - .upcast::() - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - .next() - .is_some() - }) { - window.send_to_embedder(EmbedderMsg::Status(window.webview_id(), None)); - } - } - - #[allow(unsafe_code)] - pub(crate) fn handle_mouse_leave_event( - &self, - input_event: &ConstellationInputEvent, - mouse_leave_event: &MouseLeaveEvent, - can_gc: CanGc, - ) { - if let Some(current_hover_target) = self.event_handler.current_hover_target.get() { - let current_hover_target = current_hover_target.upcast::(); - for element in current_hover_target - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - { - element.set_hover_state(false); - element.set_active_state(false); - } - - if let Some(hit_test_result) = self - .window() - .hit_test_from_point_in_viewport(self.event_handler.most_recent_mousemove_point) - { - self.fire_mouse_event( - current_hover_target.upcast(), - FireMouseEventType::Out, - EventBubbles::Bubbles, - EventCancelable::Cancelable, - &hit_test_result, - input_event, - can_gc, - ); - self.handle_mouse_enter_leave_event( - DomRoot::from_ref(current_hover_target), - None, - FireMouseEventType::Leave, - &hit_test_result, - input_event, - can_gc, - ); - } - } - - self.event_handler.current_cursor.set(None); - self.event_handler.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. - if !mouse_leave_event.focus_moving_to_another_iframe { - self.window() - .send_to_embedder(EmbedderMsg::Status(self.webview_id(), None)); - } - } - - fn handle_mouse_enter_leave_event( - &self, - event_target: DomRoot, - related_target: Option>, - event_type: FireMouseEventType, - hit_test_result: &HitTestResult, - input_event: &ConstellationInputEvent, - can_gc: CanGc, - ) { - assert!(matches!( - event_type, - FireMouseEventType::Enter | FireMouseEventType::Leave - )); - - let common_ancestor = match related_target.as_ref() { - Some(related_target) => event_target - .common_ancestor(related_target, ShadowIncluding::No) - .unwrap_or_else(|| DomRoot::from_ref(&*event_target)), - None => DomRoot::from_ref(&*event_target), - }; - - // We need to create a target chain in case the event target shares - // its boundaries with its ancestors. - let mut targets = vec![]; - let mut current = Some(event_target); - while let Some(node) = current { - if node == common_ancestor { - break; - } - current = node.GetParentNode(); - targets.push(node); - } - - // The order for dispatching mouseenter events starts from the topmost - // common ancestor of the event target and the related target. - if event_type == FireMouseEventType::Enter { - targets = targets.into_iter().rev().collect(); - } - - for target in targets { - self.fire_mouse_event( - target.upcast(), - event_type, - EventBubbles::DoesNotBubble, - EventCancelable::NotCancelable, - hit_test_result, - input_event, - can_gc, - ); - } - } - - #[allow(unsafe_code)] - pub(crate) fn handle_wheel_event( - &self, - event: WheelEvent, - input_event: &ConstellationInputEvent, - can_gc: CanGc, - ) { - // Ignore all incoming events without a hit test. - let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { - return; - }; - - let Some(el) = hit_test_result - .node - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - .next() - else { - return; - }; - - let node = el.upcast::(); - let wheel_event_type_string = "wheel".to_owned(); - debug!( - "{}: on {:?} at {:?}", - wheel_event_type_string, - node.debug_str(), - hit_test_result.point_in_frame - ); - - // https://w3c.github.io/uievents/#event-wheelevents - let dom_event = DomWheelEvent::new( - &self.window, - DOMString::from(wheel_event_type_string), - EventBubbles::Bubbles, - EventCancelable::Cancelable, - Some(&self.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, - // winit defines positive wheel delta values as revealing more content left/up. - // https://docs.rs/winit-gtk/latest/winit/event/enum.MouseScrollDelta.html - // This is the opposite of wheel delta in uievents - // https://w3c.github.io/uievents/#dom-wheeleventinit-deltaz - Finite::wrap(-event.delta.x), - Finite::wrap(-event.delta.y), - Finite::wrap(-event.delta.z), - event.delta.mode as u32, - can_gc, - ); - - let dom_event = dom_event.upcast::(); - dom_event.set_trusted(true); - - let target = node.upcast(); - dom_event.fire(target, can_gc); - } - - #[allow(unsafe_code)] - pub(crate) fn handle_touch_event( - &self, - event: TouchEvent, - input_event: &ConstellationInputEvent, - can_gc: CanGc, - ) -> TouchEventResult { - // Ignore all incoming events without a hit test. - let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { - self.update_active_touch_points_when_early_return(event); - return TouchEventResult::Forwarded; - }; - - let TouchId(identifier) = event.id; - let event_name = match event.event_type { - TouchEventType::Down => "touchstart", - TouchEventType::Move => "touchmove", - TouchEventType::Up => "touchend", - TouchEventType::Cancel => "touchcancel", - }; - - let Some(el) = hit_test_result - .node - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - .next() - else { - self.update_active_touch_points_when_early_return(event); - return TouchEventResult::Forwarded; - }; - - let target = DomRoot::upcast::(el); - let window = &*self.window; - - let client_x = Finite::wrap(hit_test_result.point_in_frame.x as f64); - let client_y = Finite::wrap(hit_test_result.point_in_frame.y as f64); - let page_x = - Finite::wrap(hit_test_result.point_in_frame.x as f64 + window.PageXOffset() as f64); - let page_y = - Finite::wrap(hit_test_result.point_in_frame.y as f64 + window.PageYOffset() as f64); - - let touch = Touch::new( - window, identifier, &target, client_x, - client_y, // TODO: Get real screen coordinates? - client_x, client_y, page_x, page_y, can_gc, - ); - - match event.event_type { - TouchEventType::Down => { - // Add a new touch point - self.active_touch_points - .borrow_mut() - .push(Dom::from_ref(&*touch)); - }, - TouchEventType::Move => { - // Replace an existing touch point - let mut active_touch_points = self.active_touch_points.borrow_mut(); - match active_touch_points - .iter_mut() - .find(|t| t.Identifier() == identifier) - { - Some(t) => *t = Dom::from_ref(&*touch), - None => warn!("Got a touchmove event for a non-active touch point"), - } - }, - TouchEventType::Up | TouchEventType::Cancel => { - // Remove an existing touch point - let mut active_touch_points = self.active_touch_points.borrow_mut(); - match active_touch_points - .iter() - .position(|t| t.Identifier() == identifier) - { - Some(i) => { - active_touch_points.swap_remove(i); - }, - None => warn!("Got a touchend event for a non-active touch point"), - } - }, - } - - rooted_vec!(let mut target_touches); - let touches = { - let touches = self.active_touch_points.borrow(); - target_touches.extend(touches.iter().filter(|t| t.Target() == target).cloned()); - TouchList::new(window, touches.r(), can_gc) - }; - - let event = DomTouchEvent::new( - window, - DOMString::from(event_name), - EventBubbles::Bubbles, - EventCancelable::from(event.is_cancelable()), - Some(window), - 0i32, - &touches, - &TouchList::new(window, from_ref(&&*touch), can_gc), - &TouchList::new(window, target_touches.r(), can_gc), - // FIXME: modifier keys - false, - false, - false, - false, - can_gc, - ); - - TouchEventResult::Processed(event.upcast::().fire(&target, can_gc)) - } - - // If hittest fails, we still need to update the active point information. - fn update_active_touch_points_when_early_return(&self, event: TouchEvent) { - match event.event_type { - TouchEventType::Down => { - // If the touchdown fails, we don't need to do anything. - // When a touchmove or touchdown occurs at that touch point, - // a warning is triggered: Got a touchmove/touchend event for a non-active touch point - }, - TouchEventType::Move => { - // The failure of touchmove does not affect the number of active points. - // Since there is no position information when it fails, we do not need to update. - }, - TouchEventType::Up | TouchEventType::Cancel => { - // Remove an existing touch point - let mut active_touch_points = self.active_touch_points.borrow_mut(); - match active_touch_points - .iter() - .position(|t| t.Identifier() == event.id.0) - { - Some(i) => { - active_touch_points.swap_remove(i); - }, - None => warn!("Got a touchend event for a non-active touch point"), - } - }, - } - } - /// pub(crate) fn run_the_scroll_steps(&self, can_gc: CanGc) { // Step 1. @@ -2583,172 +1604,6 @@ impl Document { .push(Dom::from_ref(target)); } - /// Handle scroll event triggered by user interactions from embedder side. - /// - #[allow(unsafe_code)] - pub(crate) fn handle_embedder_scroll_event(&self, event: ScrollEvent) { - // If it is a viewport scroll. - if event.external_id.is_root() { - let Some(document) = self - .node - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - .next() - else { - return; - }; - - document.handle_viewport_scroll_event(); - } else { - // Otherwise, check whether it is for a relevant element within the document. - let Some(node_id) = node_id_from_scroll_id(event.external_id.0 as usize) else { - return; - }; - let node = unsafe { - node::from_untrusted_node_address(UntrustedNodeAddress::from_id(node_id)) - }; - let Some(element) = node - .inclusive_ancestors(ShadowIncluding::No) - .filter_map(DomRoot::downcast::) - .next() - else { - return; - }; - - self.handle_element_scroll_event(&element); - } - } - - /// The entry point for all key processing for web content - pub(crate) fn dispatch_key_event( - &self, - keyboard_event: ::embedder_traits::KeyboardEvent, - can_gc: CanGc, - ) { - let focused = self.get_focused_element(); - let body = self.GetBody(); - - let target = match (&focused, &body) { - (Some(focused), _) => focused.upcast(), - (&None, Some(body)) => body.upcast(), - (&None, &None) => self.window.upcast(), - }; - - let keyevent = KeyboardEvent::new( - &self.window, - DOMString::from(keyboard_event.event.state.event_type()), - true, - true, - Some(&self.window), - 0, - keyboard_event.event.key.clone(), - DOMString::from(keyboard_event.event.code.to_string()), - keyboard_event.event.location as u32, - keyboard_event.event.repeat, - keyboard_event.event.is_composing, - keyboard_event.event.modifiers, - 0, - keyboard_event.event.key.legacy_keycode(), - can_gc, - ); - let event = keyevent.upcast::(); - event.fire(target, can_gc); - let mut cancel_state = event.get_cancel_state(); - - // https://w3c.github.io/uievents/#keys-cancelable-keys - // it MUST prevent the respective beforeinput and input - // (and keypress if supported) events from being generated - // TODO: keypress should be deprecated and superceded by beforeinput - if keyboard_event.event.state == KeyState::Down && - is_character_value_key(&(keyboard_event.event.key)) && - !keyboard_event.event.is_composing && - cancel_state != EventDefault::Prevented - { - // https://w3c.github.io/uievents/#keypress-event-order - let event = KeyboardEvent::new( - &self.window, - DOMString::from("keypress"), - true, - true, - Some(&self.window), - 0, - keyboard_event.event.key.clone(), - DOMString::from(keyboard_event.event.code.to_string()), - keyboard_event.event.location as u32, - keyboard_event.event.repeat, - keyboard_event.event.is_composing, - keyboard_event.event.modifiers, - keyboard_event.event.key.legacy_charcode(), - 0, - can_gc, - ); - let ev = event.upcast::(); - ev.fire(target, can_gc); - cancel_state = ev.get_cancel_state(); - } - - if cancel_state == EventDefault::Allowed { - let msg = EmbedderMsg::Keyboard(self.webview_id(), keyboard_event.clone()); - self.send_to_embedder(msg); - - // This behavior is unspecced - // We are supposed to dispatch synthetic click activation for Space and/or Return, - // however *when* we do it is up to us. - // Here, we're dispatching it after the key event so the script has a chance to cancel it - // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27337 - if (keyboard_event.event.key == Key::Named(NamedKey::Enter) || - keyboard_event.event.code == Code::Space) && - keyboard_event.event.state == KeyState::Up - { - if let Some(elem) = target.downcast::() { - elem.upcast::() - .fire_synthetic_pointer_event_not_trusted(DOMString::from("click"), can_gc); - } - } - } - } - - pub(crate) fn dispatch_ime_event(&self, event: ImeEvent, can_gc: CanGc) { - let composition_event = match event { - ImeEvent::Dismissed => { - self.request_focus( - self.GetBody().as_ref().map(|e| e.upcast()), - FocusInitiator::Local, - can_gc, - ); - return; - }, - ImeEvent::Composition(composition_event) => composition_event, - }; - - // spec: https://w3c.github.io/uievents/#compositionstart - // spec: https://w3c.github.io/uievents/#compositionupdate - // spec: https://w3c.github.io/uievents/#compositionend - // > Event.target : focused element processing the composition - let focused = self.get_focused_element(); - let target = if let Some(elem) = &focused { - elem.upcast() - } else { - // Event is only dispatched if there is a focused element. - return; - }; - - let cancelable = composition_event.state == keyboard_types::CompositionState::Start; - - let compositionevent = CompositionEvent::new( - &self.window, - DOMString::from(composition_event.state.event_type()), - true, - cancelable, - Some(&self.window), - 0, - DOMString::from(composition_event.data), - can_gc, - ); - let event = compositionevent.upcast::(); - event.fire(target, can_gc); - } - // https://dom.spec.whatwg.org/#converting-nodes-into-a-node pub(crate) fn node_from_nodes_and_strings( &self, @@ -3761,7 +2616,7 @@ impl Document { if self.resize_observer_started_observing_target.get() { return true; } - if self.has_pending_input_events() { + if self.event_handler.has_pending_input_events() { return true; } if self.has_pending_scroll_events() { @@ -4189,18 +3044,6 @@ impl Document { Ok(()) } - - pub(crate) fn set_cursor(&self, cursor: Cursor) { - if Some(cursor) == self.event_handler.current_cursor.get() { - return; - } - self.event_handler.current_cursor.set(Some(cursor)); - self.send_to_embedder(EmbedderMsg::SetCursor(self.webview_id(), cursor)); - } -} - -fn is_character_value_key(key: &Key) -> bool { - matches!(key, Key::Character(_) | Key::Named(NamedKey::Enter)) } #[derive(MallocSizeOf, PartialEq)] @@ -4395,7 +3238,7 @@ impl Document { url: DomRefCell::new(url), // https://dom.spec.whatwg.org/#concept-document-quirks quirks_mode: Cell::new(QuirksMode::NoQuirks), - event_handler: DocumentEventHandler::default(), + event_handler: DocumentEventHandler::new(window), id_map: DomRefCell::new(HashMapTracedValues::new()), name_map: DomRefCell::new(HashMapTracedValues::new()), // https://dom.spec.whatwg.org/#concept-document-encoding @@ -4450,7 +3293,6 @@ impl Document { appropriate_template_contents_owner_document: Default::default(), pending_restyles: DomRefCell::new(FnvHashMap::default()), needs_restyle: Cell::new(RestyleReason::DOMChanged), - active_touch_points: DomRefCell::new(Vec::new()), dom_interactive: Cell::new(Default::default()), dom_content_loaded_event_start: Cell::new(Default::default()), dom_content_loaded_event_end: Cell::new(Default::default()), @@ -4465,7 +3307,6 @@ impl Document { referrer, target_element: MutNullableDom::new(None), policy_container: DomRefCell::new(PolicyContainer::default()), - last_click_info: DomRefCell::new(None), ignore_destructive_writes_counter: Default::default(), ignore_opens_during_unload_counter: Default::default(), spurious_animation_frames: Cell::new(0), @@ -4502,8 +3343,6 @@ impl Document { image_animation_manager: DomRefCell::new(ImageAnimationManager::default()), dirty_root: Default::default(), declarative_refresh: Default::default(), - pending_input_events: Default::default(), - mouse_move_event_index: Default::default(), resize_observers: Default::default(), fonts: Default::default(), visibility_state: Cell::new(DocumentVisibilityState::Hidden), @@ -4514,7 +3353,6 @@ impl Document { has_trustworthy_ancestor_origin: Cell::new(has_trustworthy_ancestor_origin), intersection_observer_task_queued: Cell::new(false), intersection_observers: Default::default(), - active_keyboard_modifiers: Cell::new(Modifiers::empty()), highlighted_dom_node: Default::default(), adopted_stylesheets: Default::default(), adopted_stylesheets_frozen_types: CachedFrozenArray::new(), @@ -4540,57 +3378,9 @@ impl Document { .unwrap_or(InsecureRequestsPolicy::DoNotUpgrade) } - /// Update the active keyboard modifiers for this [`Document`] while handling events. - pub(crate) fn update_active_keyboard_modifiers(&self, modifiers: Modifiers) { - self.active_keyboard_modifiers.set(modifiers); - } - - 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) - } - } - - /// Note a pending compositor event, to be processed at the next `update_the_rendering` task. - pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) { - let mut pending_compositor_events = self.pending_input_events.borrow_mut(); - if matches!(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_compositor_events.get_mut(index)) - { - *mouse_move_event = event; - return; - } - - *self.mouse_move_event_index.borrow_mut() = Some(pending_compositor_events.len()); - } - - pending_compositor_events.push(event); - } - - /// Get pending compositor events, for processing within an `update_the_rendering` task. - pub(crate) fn take_pending_input_events(&self) -> Vec { - // Reset the mouse event index. - *self.mouse_move_event_index.borrow_mut() = None; - mem::take(&mut *self.pending_input_events.borrow_mut()) - } - - /// 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() + /// Get the [`Document`]'s [`DocumentEventHandler`]. + pub(crate) fn event_handler(&self) -> &DocumentEventHandler { + &self.event_handler } /// Whether or not this [`Document`] has any pending scroll events to be processed during diff --git a/components/script/dom/document_event_handler.rs b/components/script/dom/document_event_handler.rs index d79d12b3dd6..db743309ef3 100644 --- a/components/script/dom/document_event_handler.rs +++ b/components/script/dom/document_event_handler.rs @@ -2,28 +2,1449 @@ * 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::cell::Cell; +use std::array::from_ref; +use std::cell::{Cell, RefCell}; +use std::f64::consts::PI; +use std::mem; +use std::rc::Rc; +use std::time::{Duration, Instant}; -use embedder_traits::Cursor; +use constellation_traits::ScriptToConstellationMessage; +use embedder_traits::{ + Cursor, EditingActionEvent, EmbedderMsg, GamepadEvent as EmbedderGamepadEvent, + GamepadSupportedHapticEffects, GamepadUpdateType, ImeEvent, InputEvent, + KeyboardEvent as EmbedderKeyboardEvent, MouseButton, MouseButtonAction, MouseButtonEvent, + MouseLeaveEvent, ScrollEvent, TouchEvent as EmbedderTouchEvent, TouchEventType, TouchId, + UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent, +}; use euclid::Point2D; +use ipc_channel::ipc; +use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey}; +use layout_api::node_id_from_scroll_id; +use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; +use script_bindings::codegen::GenericBindings::EventBinding::EventMethods; +use script_bindings::codegen::GenericBindings::NavigatorBinding::NavigatorMethods; +use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods; +use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods; +use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods; +use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods; +use script_bindings::inheritance::Castable; +use script_bindings::num::Finite; +use script_bindings::root::{Dom, DomRoot, DomSlice}; +use script_bindings::script_runtime::CanGc; +use script_bindings::str::DOMString; +use script_traits::ConstellationInputEvent; +use servo_config::pref; use style_traits::CSSPixel; +use xml5ever::{local_name, ns}; +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::root::MutNullableDom; -use crate::dom::types::Element; +use crate::dom::clipboardevent::ClipboardEventType; +use crate::dom::document::{FireMouseEventType, FocusInitiator, TouchEventResult}; +use crate::dom::event::{EventBubbles, EventCancelable, EventDefault}; +use crate::dom::gamepad::contains_user_gesture; +use crate::dom::gamepadevent::GamepadEventType; +use crate::dom::inputevent::HitTestResult; +use crate::dom::node::{self, Node, ShadowIncluding}; +use crate::dom::pointerevent::PointerId; +use crate::dom::types::{ + ClipboardEvent, CompositionEvent, DataTransfer, Element, Event, EventTarget, Gamepad, + GlobalScope, HTMLAnchorElement, KeyboardEvent, MouseEvent, PointerEvent, Touch, TouchEvent, + TouchList, WheelEvent, Window, +}; +use crate::drag_data_store::{DragDataStore, Kind, Mode}; +use crate::realms::enter_realm; -/// The [`DocumentEventHandler`] is a structure responsible for handling events for +/// 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(Default, JSTraceable, MallocSizeOf)] +#[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 = "CompositorEvent contains data from outside crates"] + pending_input_events: DomRefCell>, + /// The index of the last mouse move event in the pending compositor events queue. + mouse_move_event_index: DomRefCell>, + /// + #[ignore_malloc_size_of = "Defined in std"] + #[no_trace] + last_click_info: DomRefCell)>>, /// The element that is currently hovered by the cursor. - pub(crate) current_hover_target: MutNullableDom, + current_hover_target: MutNullableDom, /// The most recent mouse movement point, used for processing `mouseleave` events. #[no_trace] - pub(crate) most_recent_mousemove_point: Point2D, + most_recent_mousemove_point: Point2D, /// The currently set [`Cursor`] or `None` if the `Document` isn't being hovered /// by the cursor. #[no_trace] - pub(crate) current_cursor: Cell>, + 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, +} + +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(), + last_click_info: Default::default(), + current_hover_target: Default::default(), + most_recent_mousemove_point: Default::default(), + current_cursor: Default::default(), + active_touch_points: Default::default(), + active_keyboard_modifiers: Default::default(), + } + } + + /// Note a pending compositor event, to be processed at the next `update_the_rendering` task. + pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) { + let mut pending_compositor_events = self.pending_input_events.borrow_mut(); + if matches!(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_compositor_events.get_mut(index)) + { + *mouse_move_event = event; + return; + } + + *self.mouse_move_event_index.borrow_mut() = Some(pending_compositor_events.len()); + } + + pending_compositor_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) { + let _realm = enter_realm(&*self.window); + + // Reset the mouse event index. + *self.mouse_move_event_index.borrow_mut() = None; + let pending_input_events = mem::take(&mut *self.pending_input_events.borrow_mut()); + + for event in pending_input_events { + self.active_keyboard_modifiers + .set(event.active_keyboard_modifiers); + + match event.event.clone() { + InputEvent::MouseButton(mouse_button_event) => { + self.handle_mouse_button_event(mouse_button_event, &event, can_gc); + }, + InputEvent::MouseMove(_) => { + self.handle_mouse_move_event(&event, can_gc); + }, + InputEvent::MouseLeave(mouse_leave_event) => { + self.handle_mouse_leave_event(&event, &mouse_leave_event, can_gc); + }, + InputEvent::Touch(touch_event) => { + self.handle_touch_event(touch_event, &event, can_gc); + }, + InputEvent::Wheel(wheel_event) => { + self.handle_wheel_event(wheel_event, &event, can_gc); + }, + InputEvent::Keyboard(keyboard_event) => { + self.handle_keyboard_event(keyboard_event, can_gc); + }, + InputEvent::Ime(ime_event) => { + self.handle_ime_event(ime_event, can_gc); + }, + InputEvent::Gamepad(gamepad_event) => { + self.handle_gamepad_event(gamepad_event); + }, + InputEvent::EditingAction(editing_action_event) => { + self.handle_editing_action(editing_action_event, can_gc); + }, + InputEvent::Scroll(scroll_event) => { + self.handle_embedder_scroll_event(scroll_event); + }, + } + + self.notify_webdriver_input_event_completed(event.event); + } + } + + fn notify_webdriver_input_event_completed(&self, event: InputEvent) { + let Some(id) = event.webdriver_message_id() else { + return; + }; + + // Webdriver should be notified once all current dom events have been processed. + 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_constellation(ScriptToConstellationMessage::WebDriverInputComplete(id)); + })); + } + + pub(crate) fn set_cursor(&self, cursor: Cursor) { + if Some(cursor) == self.current_cursor.get() { + return; + } + self.current_cursor.set(Some(cursor)); + self.window + .send_to_embedder(EmbedderMsg::SetCursor(self.window.webview_id(), cursor)); + } + + fn handle_mouse_leave_event( + &self, + input_event: &ConstellationInputEvent, + mouse_leave_event: &MouseLeaveEvent, + 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::No) + .filter_map(DomRoot::downcast::) + { + element.set_hover_state(false); + element.set_active_state(false); + } + + if let Some(hit_test_result) = self + .window + .hit_test_from_point_in_viewport(self.most_recent_mousemove_point) + { + MouseEvent::new_simple( + &self.window, + FireMouseEventType::Out, + EventBubbles::Bubbles, + EventCancelable::Cancelable, + &hit_test_result, + input_event, + can_gc, + ) + .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, + ); + } + } + + 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. + if !mouse_leave_event.focus_moving_to_another_iframe { + self.window + .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None)); + } + } + + fn handle_mouse_enter_leave_event( + &self, + event_target: DomRoot, + related_target: Option>, + event_type: FireMouseEventType, + hit_test_result: &HitTestResult, + input_event: &ConstellationInputEvent, + can_gc: CanGc, + ) { + assert!(matches!( + event_type, + FireMouseEventType::Enter | FireMouseEventType::Leave + )); + + let common_ancestor = match related_target.as_ref() { + Some(related_target) => event_target + .common_ancestor(related_target, ShadowIncluding::No) + .unwrap_or_else(|| DomRoot::from_ref(&*event_target)), + None => DomRoot::from_ref(&*event_target), + }; + + // We need to create a target chain in case the event target shares + // its boundaries with its ancestors. + let mut targets = vec![]; + let mut current = Some(event_target); + while let Some(node) = current { + if node == common_ancestor { + break; + } + current = node.GetParentNode(); + targets.push(node); + } + + // The order for dispatching mouseenter events starts from the topmost + // common ancestor of the event target and the related target. + if event_type == FireMouseEventType::Enter { + targets = targets.into_iter().rev().collect(); + } + + for target in targets { + MouseEvent::new_simple( + &self.window, + event_type, + EventBubbles::DoesNotBubble, + EventCancelable::NotCancelable, + hit_test_result, + input_event, + can_gc, + ) + .upcast::() + .fire(target.upcast(), can_gc); + } + } + + fn handle_mouse_move_event(&self, input_event: &ConstellationInputEvent, can_gc: CanGc) { + // Ignore all incoming events without a hit test. + let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { + return; + }; + + // Update the cursor when the mouse moves, if it has changed. + self.set_cursor(hit_test_result.cursor); + + let Some(new_target) = hit_test_result + .node + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + .next() + else { + return; + }; + + let target_has_changed = self + .current_hover_target + .get() + .as_ref() + .is_none_or(|old_target| old_target != &new_target); + + // Here we know the target has changed, so we must update the state, + // dispatch mouseout to the previous one, mouseover to the new one. + if target_has_changed { + // Dispatch mouseout and mouseleave to previous target. + if let Some(old_target) = self.current_hover_target.get() { + let old_target_is_ancestor_of_new_target = old_target + .upcast::() + .is_ancestor_of(new_target.upcast::()); + + // If the old target is an ancestor of the new target, this can be skipped + // completely, since the node's hover state will be reset below. + if !old_target_is_ancestor_of_new_target { + for element in old_target + .upcast::() + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + { + element.set_hover_state(false); + element.set_active_state(false); + } + } + + MouseEvent::new_simple( + &self.window, + FireMouseEventType::Out, + EventBubbles::Bubbles, + EventCancelable::Cancelable, + &hit_test_result, + input_event, + can_gc, + ) + .upcast::() + .fire(old_target.upcast(), can_gc); + + if !old_target_is_ancestor_of_new_target { + let event_target = DomRoot::from_ref(old_target.upcast::()); + let moving_into = Some(DomRoot::from_ref(new_target.upcast::())); + self.handle_mouse_enter_leave_event( + event_target, + moving_into, + FireMouseEventType::Leave, + &hit_test_result, + input_event, + can_gc, + ); + } + } + + // Dispatch mouseover and mouseenter to new target. + for element in new_target + .upcast::() + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + { + element.set_hover_state(true); + } + + MouseEvent::new_simple( + &self.window, + FireMouseEventType::Over, + EventBubbles::Bubbles, + EventCancelable::Cancelable, + &hit_test_result, + input_event, + can_gc, + ) + .upcast::() + .fire(new_target.upcast(), can_gc); + + let moving_from = self + .current_hover_target + .get() + .map(|old_target| DomRoot::from_ref(old_target.upcast::())); + let event_target = DomRoot::from_ref(new_target.upcast::()); + self.handle_mouse_enter_leave_event( + event_target, + moving_from, + FireMouseEventType::Enter, + &hit_test_result, + input_event, + can_gc, + ); + } + + // Send mousemove event to topmost target, unless it's an iframe, in which case the + // compositor should have also sent an event to the inner document. + MouseEvent::new_simple( + &self.window, + FireMouseEventType::Move, + EventBubbles::Bubbles, + EventCancelable::Cancelable, + &hit_test_result, + input_event, + can_gc, + ) + .upcast::() + .fire(new_target.upcast(), can_gc); + + self.update_current_hover_target_and_status(Some(new_target)); + } + + fn update_current_hover_target_and_status(&self, new_hover_target: Option>) { + let current_hover_target = self.current_hover_target.get(); + if current_hover_target == new_hover_target { + return; + } + + let previous_hover_target = self.current_hover_target.get(); + self.current_hover_target.set(new_hover_target.as_deref()); + + // If the new hover target is an anchor with a status value, inform the embedder + // of the new value. + if let Some(target) = self.current_hover_target.get() { + if let Some(anchor) = target + .upcast::() + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + .next() + { + let status = anchor + .upcast::() + .get_attribute(&ns!(), &local_name!("href")) + .and_then(|href| { + let value = href.value(); + let url = self.window.get_url(); + url.join(&value).map(|url| url.to_string()).ok() + }); + self.window + .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), status)); + return; + } + } + + // No state was set above, which means that the new value of the status in the embedder + // should be `None`. Set that now. If `previous_hover_target` is `None` that means this + // is the first mouse move event we are seeing after getting the cursor. In that case, + // we also clear the status. + if previous_hover_target.is_none_or(|previous_hover_target| { + previous_hover_target + .upcast::() + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + .next() + .is_some() + }) { + self.window + .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None)); + } + } + + /// + fn handle_mouse_button_event( + &self, + event: MouseButtonEvent, + input_event: &ConstellationInputEvent, + can_gc: CanGc, + ) { + // Ignore all incoming events without a hit test. + let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { + return; + }; + + debug!( + "{:?}: at {:?}", + event.action, hit_test_result.point_in_frame + ); + + let Some(el) = hit_test_result + .node + .inclusive_ancestors(ShadowIncluding::Yes) + .filter_map(DomRoot::downcast::) + .next() + else { + return; + }; + + let node = el.upcast::(); + debug!("{:?} on {:?}", event.action, node.debug_str()); + // Prevent click event if form control element is disabled. + if let MouseButtonAction::Click = event.action { + // The click event is filtered by the disabled state. + if el.is_actually_disabled() { + return; + } + } + + let dom_event = DomRoot::upcast::(MouseEvent::for_platform_mouse_event( + event, + input_event.pressed_mouse_buttons, + &self.window, + &hit_test_result, + input_event.active_keyboard_modifiers, + can_gc, + )); + + let activatable = el.as_maybe_activatable(); + match event.action { + // https://w3c.github.io/uievents/#handle-native-mouse-click + MouseButtonAction::Click => { + el.set_click_in_progress(true); + dom_event.dispatch(node.upcast(), false, can_gc); + el.set_click_in_progress(false); + + self.maybe_fire_dblclick(node, &hit_test_result, input_event, can_gc); + }, + // https://w3c.github.io/uievents/#handle-native-mouse-down + MouseButtonAction::Down => { + if let Some(a) = activatable { + a.enter_formal_activation_state(); + } + + // (TODO) Step 6. Maybe send pointerdown event with `dom_event`. + + // For a node within a text input UA shadow DOM, + // delegate the focus target into its shadow host. + // TODO: This focus delegation should be done + // with shadow DOM delegateFocus attribute. + let target_el = el.find_focusable_shadow_host_if_necessary(); + + let document = self.window.Document(); + document.begin_focus_transaction(); + + // Try to focus `el`. If it's not focusable, focus the document instead. + document.request_focus(None, FocusInitiator::Local, can_gc); + document.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc); + + // Step 7. Let result = dispatch event at target + let result = dom_event.dispatch(node.upcast(), false, can_gc); + + // Step 8. If result is true and target is a focusable area + // that is click focusable, then Run the focusing steps at target. + if result && document.has_focus_transaction() { + document.commit_focus_transaction(FocusInitiator::Local, can_gc); + } + + // Step 9. If mbutton is the secondary mouse button, then + // Maybe show context menu with native, target. + if let (MouseButtonAction::Down, MouseButton::Right) = (event.action, event.button) + { + self.maybe_show_context_menu( + node.upcast(), + &hit_test_result, + input_event, + can_gc, + ); + } + }, + // https://w3c.github.io/uievents/#handle-native-mouse-up + MouseButtonAction::Up => { + if let Some(a) = activatable { + a.exit_formal_activation_state(); + } + + // (TODO) Step 6. Maybe send pointerup event with `dom_event``. + + // Step 7. dispatch event at target. + dom_event.dispatch(node.upcast(), false, can_gc); + }, + } + + // When the contextmenu event is triggered by right mouse button + // the contextmenu event MUST be dispatched after the mousedown event. + if let (MouseButtonAction::Down, MouseButton::Right) = (event.action, event.button) { + self.maybe_show_context_menu(node.upcast(), &hit_test_result, input_event, can_gc); + } + } + + /// + fn maybe_show_context_menu( + &self, + target: &EventTarget, + hit_test_result: &HitTestResult, + input_event: &ConstellationInputEvent, + can_gc: CanGc, + ) { + // + let menu_event = PointerEvent::new( + &self.window, // window + DOMString::from("contextmenu"), // type + EventBubbles::Bubbles, // can_bubble + EventCancelable::Cancelable, // cancelable + Some(&self.window), // view + 0, // detail + 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, + 2i16, // button, right mouse button + input_event.pressed_mouse_buttons, + None, // related_target + None, // point_in_target + PointerId::Mouse as i32, // pointer_id + 1, // width + 1, // height + 0.5, // pressure + 0.0, // tangential_pressure + 0, // tilt_x + 0, // tilt_y + 0, // twist + PI / 2.0, // altitude_angle + 0.0, // azimuth_angle + DOMString::from("mouse"), // pointer_type + true, // is_primary + vec![], // coalesced_events + vec![], // predicted_events + can_gc, + ); + + // Step 3. Let result = dispatch menuevent at target. + let result = menu_event.upcast::().fire(target, can_gc); + + // Step 4. If result is true, then show the UA context menu + if result { + let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel."); + self.window.send_to_embedder(EmbedderMsg::ShowContextMenu( + self.window.webview_id(), + sender, + None, + vec![], + )); + let _ = receiver.recv().unwrap(); + }; + } + + fn maybe_fire_dblclick( + &self, + target: &Node, + hit_test_result: &HitTestResult, + input_event: &ConstellationInputEvent, + can_gc: CanGc, + ) { + // https://w3c.github.io/uievents/#event-type-dblclick + let now = Instant::now(); + let point_in_frame = hit_test_result.point_in_frame; + let opt = self.last_click_info.borrow_mut().take(); + + if let Some((last_time, last_pos)) = opt { + 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 - last_pos; + let dist = (line.dot(line) as f64).sqrt(); + + if now.duration_since(last_time) < double_click_timeout && + dist < double_click_distance_threshold as f64 + { + // A double click has occurred if this click is within a certain time and dist. of previous click. + let click_count = 2; + + let event = MouseEvent::new( + &self.window, + DOMString::from("dblclick"), + EventBubbles::Bubbles, + EventCancelable::Cancelable, + Some(&self.window), + click_count, + point_in_frame.to_i32(), + 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, + ); + event.upcast::().fire(target.upcast(), can_gc); + + // When a double click occurs, self.last_click_info is left as None so that a + // third sequential click will not cause another double click. + return; + } + } + + // Update last_click_info with the time and position of the click. + *self.last_click_info.borrow_mut() = Some((now, point_in_frame)); + } + + fn handle_touch_event( + &self, + event: EmbedderTouchEvent, + input_event: &ConstellationInputEvent, + can_gc: CanGc, + ) { + let result = self.handle_touch_event_inner(event, input_event, can_gc); + if let (TouchEventResult::Processed(handled), true) = (result, event.is_cancelable()) { + let sequence_id = event.expect_sequence_id(); + let result = if handled { + embedder_traits::TouchEventResult::DefaultAllowed(sequence_id, event.event_type) + } else { + embedder_traits::TouchEventResult::DefaultPrevented(sequence_id, event.event_type) + }; + self.window + .send_to_constellation(ScriptToConstellationMessage::TouchEventProcessed(result)); + } + } + + fn handle_touch_event_inner( + &self, + event: EmbedderTouchEvent, + input_event: &ConstellationInputEvent, + can_gc: CanGc, + ) -> TouchEventResult { + // Ignore all incoming events without a hit test. + let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { + self.update_active_touch_points_when_early_return(event); + return TouchEventResult::Forwarded; + }; + + let TouchId(identifier) = event.id; + let event_name = match event.event_type { + TouchEventType::Down => "touchstart", + TouchEventType::Move => "touchmove", + TouchEventType::Up => "touchend", + TouchEventType::Cancel => "touchcancel", + }; + + let Some(el) = hit_test_result + .node + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + .next() + else { + self.update_active_touch_points_when_early_return(event); + return TouchEventResult::Forwarded; + }; + + let target = DomRoot::upcast::(el); + let window = &*self.window; + + let client_x = Finite::wrap(hit_test_result.point_in_frame.x as f64); + let client_y = Finite::wrap(hit_test_result.point_in_frame.y as f64); + let page_x = + Finite::wrap(hit_test_result.point_in_frame.x as f64 + window.PageXOffset() as f64); + let page_y = + Finite::wrap(hit_test_result.point_in_frame.y as f64 + window.PageYOffset() as f64); + + let touch = Touch::new( + window, identifier, &target, client_x, + client_y, // TODO: Get real screen coordinates? + client_x, client_y, page_x, page_y, can_gc, + ); + + match event.event_type { + TouchEventType::Down => { + // Add a new touch point + self.active_touch_points + .borrow_mut() + .push(Dom::from_ref(&*touch)); + }, + TouchEventType::Move => { + // Replace an existing touch point + let mut active_touch_points = self.active_touch_points.borrow_mut(); + match active_touch_points + .iter_mut() + .find(|t| t.Identifier() == identifier) + { + Some(t) => *t = Dom::from_ref(&*touch), + None => warn!("Got a touchmove event for a non-active touch point"), + } + }, + TouchEventType::Up | TouchEventType::Cancel => { + // Remove an existing touch point + let mut active_touch_points = self.active_touch_points.borrow_mut(); + match active_touch_points + .iter() + .position(|t| t.Identifier() == identifier) + { + Some(i) => { + active_touch_points.swap_remove(i); + }, + None => warn!("Got a touchend event for a non-active touch point"), + } + }, + } + + rooted_vec!(let mut target_touches); + let touches = { + let touches = self.active_touch_points.borrow(); + target_touches.extend(touches.iter().filter(|t| t.Target() == target).cloned()); + TouchList::new(window, touches.r(), can_gc) + }; + + let event = TouchEvent::new( + window, + DOMString::from(event_name), + EventBubbles::Bubbles, + EventCancelable::from(event.is_cancelable()), + Some(window), + 0i32, + &touches, + &TouchList::new(window, from_ref(&&*touch), can_gc), + &TouchList::new(window, target_touches.r(), can_gc), + // FIXME: modifier keys + false, + false, + false, + false, + can_gc, + ); + + TouchEventResult::Processed(event.upcast::().fire(&target, can_gc)) + } + + // If hittest fails, we still need to update the active point information. + fn update_active_touch_points_when_early_return(&self, event: EmbedderTouchEvent) { + match event.event_type { + TouchEventType::Down => { + // If the touchdown fails, we don't need to do anything. + // When a touchmove or touchdown occurs at that touch point, + // a warning is triggered: Got a touchmove/touchend event for a non-active touch point + }, + TouchEventType::Move => { + // The failure of touchmove does not affect the number of active points. + // Since there is no position information when it fails, we do not need to update. + }, + TouchEventType::Up | TouchEventType::Cancel => { + // Remove an existing touch point + let mut active_touch_points = self.active_touch_points.borrow_mut(); + match active_touch_points + .iter() + .position(|t| t.Identifier() == event.id.0) + { + Some(i) => { + active_touch_points.swap_remove(i); + }, + None => warn!("Got a touchend event for a non-active touch point"), + } + }, + } + } + + /// The entry point for all key processing for web content + fn handle_keyboard_event(&self, keyboard_event: EmbedderKeyboardEvent, can_gc: CanGc) { + let document = self.window.Document(); + let focused = document.get_focused_element(); + let body = document.GetBody(); + + let target = match (&focused, &body) { + (Some(focused), _) => focused.upcast(), + (&None, Some(body)) => body.upcast(), + (&None, &None) => self.window.upcast(), + }; + + let keyevent = KeyboardEvent::new( + &self.window, + DOMString::from(keyboard_event.event.state.event_type()), + true, + true, + Some(&self.window), + 0, + keyboard_event.event.key.clone(), + DOMString::from(keyboard_event.event.code.to_string()), + keyboard_event.event.location as u32, + keyboard_event.event.repeat, + keyboard_event.event.is_composing, + keyboard_event.event.modifiers, + 0, + keyboard_event.event.key.legacy_keycode(), + can_gc, + ); + let event = keyevent.upcast::(); + event.fire(target, can_gc); + let mut cancel_state = event.get_cancel_state(); + + // https://w3c.github.io/uievents/#keys-cancelable-keys + // it MUST prevent the respective beforeinput and input + // (and keypress if supported) events from being generated + // TODO: keypress should be deprecated and superceded by beforeinput + + let is_character_value_key = matches!( + keyboard_event.event.key, + Key::Character(_) | Key::Named(NamedKey::Enter) + ); + if keyboard_event.event.state == KeyState::Down && + is_character_value_key && + !keyboard_event.event.is_composing && + cancel_state != EventDefault::Prevented + { + // https://w3c.github.io/uievents/#keypress-event-order + let event = KeyboardEvent::new( + &self.window, + DOMString::from("keypress"), + true, + true, + Some(&self.window), + 0, + keyboard_event.event.key.clone(), + DOMString::from(keyboard_event.event.code.to_string()), + keyboard_event.event.location as u32, + keyboard_event.event.repeat, + keyboard_event.event.is_composing, + keyboard_event.event.modifiers, + keyboard_event.event.key.legacy_charcode(), + 0, + can_gc, + ); + let ev = event.upcast::(); + ev.fire(target, can_gc); + cancel_state = ev.get_cancel_state(); + } + + if cancel_state == EventDefault::Allowed { + self.window.send_to_embedder(EmbedderMsg::Keyboard( + self.window.webview_id(), + keyboard_event.clone(), + )); + + // This behavior is unspecced + // We are supposed to dispatch synthetic click activation for Space and/or Return, + // however *when* we do it is up to us. + // Here, we're dispatching it after the key event so the script has a chance to cancel it + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27337 + if (keyboard_event.event.key == Key::Named(NamedKey::Enter) || + keyboard_event.event.code == Code::Space) && + keyboard_event.event.state == KeyState::Up + { + if let Some(elem) = target.downcast::() { + elem.upcast::() + .fire_synthetic_pointer_event_not_trusted(DOMString::from("click"), can_gc); + } + } + } + } + + fn handle_ime_event(&self, event: ImeEvent, can_gc: CanGc) { + let document = self.window.Document(); + let composition_event = match event { + ImeEvent::Dismissed => { + document.request_focus( + document.GetBody().as_ref().map(|e| e.upcast()), + FocusInitiator::Local, + can_gc, + ); + return; + }, + ImeEvent::Composition(composition_event) => composition_event, + }; + + // spec: https://w3c.github.io/uievents/#compositionstart + // spec: https://w3c.github.io/uievents/#compositionupdate + // spec: https://w3c.github.io/uievents/#compositionend + // > Event.target : focused element processing the composition + let focused = document.get_focused_element(); + let target = if let Some(elem) = &focused { + elem.upcast() + } else { + // Event is only dispatched if there is a focused element. + return; + }; + + let cancelable = composition_event.state == keyboard_types::CompositionState::Start; + CompositionEvent::new( + &self.window, + DOMString::from(composition_event.state.event_type()), + true, + cancelable, + Some(&self.window), + 0, + DOMString::from(composition_event.data), + can_gc, + ) + .upcast::() + .fire(target, can_gc); + } + + fn handle_wheel_event( + &self, + event: EmbedderWheelEvent, + input_event: &ConstellationInputEvent, + can_gc: CanGc, + ) { + // Ignore all incoming events without a hit test. + let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { + return; + }; + + let Some(el) = hit_test_result + .node + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + .next() + else { + return; + }; + + let node = el.upcast::(); + let wheel_event_type_string = "wheel".to_owned(); + debug!( + "{}: on {:?} at {:?}", + wheel_event_type_string, + node.debug_str(), + hit_test_result.point_in_frame + ); + + // https://w3c.github.io/uievents/#event-wheelevents + let dom_event = WheelEvent::new( + &self.window, + DOMString::from(wheel_event_type_string), + EventBubbles::Bubbles, + EventCancelable::Cancelable, + Some(&self.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, + // winit defines positive wheel delta values as revealing more content left/up. + // https://docs.rs/winit-gtk/latest/winit/event/enum.MouseScrollDelta.html + // This is the opposite of wheel delta in uievents + // https://w3c.github.io/uievents/#dom-wheeleventinit-deltaz + Finite::wrap(-event.delta.x), + Finite::wrap(-event.delta.y), + Finite::wrap(-event.delta.z), + event.delta.mode as u32, + can_gc, + ); + + let dom_event = dom_event.upcast::(); + dom_event.set_trusted(true); + + let target = node.upcast(); + dom_event.fire(target, can_gc); + } + + fn handle_gamepad_event(&self, gamepad_event: EmbedderGamepadEvent) { + match gamepad_event { + EmbedderGamepadEvent::Connected(index, name, bounds, supported_haptic_effects) => { + self.handle_gamepad_connect( + index.0, + name, + bounds.axis_bounds, + bounds.button_bounds, + supported_haptic_effects, + ); + }, + EmbedderGamepadEvent::Disconnected(index) => { + self.handle_gamepad_disconnect(index.0); + }, + EmbedderGamepadEvent::Updated(index, update_type) => { + self.receive_new_gamepad_button_or_axis(index.0, update_type); + }, + }; + } + + /// + 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 trusted_window = Trusted::new(&*self.window); + self.window + .upcast::() + .task_manager() + .gamepad_task_source() + .queue(task!(gamepad_connected: move || { + let window = trusted_window.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()); + })); + } + + /// + fn handle_gamepad_disconnect(&self, index: usize) { + let trusted_window = Trusted::new(&*self.window); + self.window + .upcast::() + .task_manager() + .gamepad_task_source() + .queue(task!(gamepad_disconnected: move || { + let window = trusted_window.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); + } + } + })); + } + + /// + fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) { + let trusted_window = Trusted::new(&*self.window); + + // + self.window.upcast::().task_manager().gamepad_task_source().queue( + task!(update_gamepad_state: move || { + let window = trusted_window.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::().task_manager().gamepad_task_source().queue( + task!(update_gamepad_connect: move || { + let gamepad = new_gamepad.root(); + gamepad.notify_event(GamepadEventType::Connected, CanGc::note()); + }) + ); + } + }); + } + } + }) + ); + } + + /// + fn handle_editing_action(&self, action: EditingActionEvent, can_gc: CanGc) -> bool { + let clipboard_event_type = match action { + EditingActionEvent::Copy => ClipboardEventType::Copy, + EditingActionEvent::Cut => ClipboardEventType::Cut, + EditingActionEvent::Paste => ClipboardEventType::Paste, + }; + + // The script_triggered flag is set if the action runs because of a script, e.g. document.execCommand() + let script_triggered = false; + + // The script_may_access_clipboard flag is set + // if action is paste and the script thread is allowed to read from clipboard or + // if action is copy or cut and the script thread is allowed to modify the clipboard + let script_may_access_clipboard = false; + + // Step 1 If the script-triggered flag is set and the script-may-access-clipboard flag is unset + if script_triggered && !script_may_access_clipboard { + return false; + } + + // Step 2 Fire a clipboard event + let event = ClipboardEvent::new( + &self.window, + None, + DOMString::from(clipboard_event_type.as_str()), + EventBubbles::Bubbles, + EventCancelable::Cancelable, + None, + can_gc, + ); + self.fire_clipboard_event(&event, clipboard_event_type, can_gc); + + // Step 3 If a script doesn't call preventDefault() + // the event will be handled inside target's VirtualMethods::handle_event + + let e = event.upcast::(); + + if !e.IsTrusted() { + return false; + } + + // Step 4 If the event was canceled, then + if e.DefaultPrevented() { + match e.Type().str() { + "copy" => { + // Step 4.1 Call the write content to the clipboard algorithm, + // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. + if let Some(clipboard_data) = event.get_clipboard_data() { + let drag_data_store = + clipboard_data.data_store().expect("This shouldn't fail"); + self.write_content_to_the_clipboard(&drag_data_store); + } + }, + "cut" => { + // Step 4.1 Call the write content to the clipboard algorithm, + // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. + if let Some(clipboard_data) = event.get_clipboard_data() { + let drag_data_store = + clipboard_data.data_store().expect("This shouldn't fail"); + self.write_content_to_the_clipboard(&drag_data_store); + } + + // Step 4.2 Fire a clipboard event named clipboardchange + self.fire_clipboardchange_event(can_gc); + }, + "paste" => return false, + _ => (), + } + } + //Step 5 + true + } + + /// + fn fire_clipboard_event( + &self, + event: &ClipboardEvent, + action: ClipboardEventType, + can_gc: CanGc, + ) { + // Step 1 Let clear_was_called be false + // Step 2 Let types_to_clear an empty list + let mut drag_data_store = DragDataStore::new(); + + // Step 4 let clipboard-entry be the sequence number of clipboard content, null if the OS doesn't support it. + + // Step 5 let trusted be true if the event is generated by the user agent, false otherwise + let trusted = true; + + // Step 6 if the context is editable: + let document = self.window.Document(); + let focused = document.get_focused_element(); + let body = document.GetBody(); + + let target = match (&focused, &body) { + (Some(focused), _) => focused.upcast(), + (&None, Some(body)) => body.upcast(), + (&None, &None) => self.window.upcast(), + }; + // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70 + + // Step 7 + match action { + ClipboardEventType::Copy | ClipboardEventType::Cut => { + // Step 7.2.1 + drag_data_store.set_mode(Mode::ReadWrite); + }, + ClipboardEventType::Paste => { + let (sender, receiver) = ipc::channel().unwrap(); + self.window + .send_to_constellation(ScriptToConstellationMessage::ForwardToEmbedder( + EmbedderMsg::GetClipboardText(self.window.webview_id(), sender), + )); + let text_contents = receiver + .recv() + .map(Result::unwrap_or_default) + .unwrap_or_default(); + + // Step 7.1.1 + drag_data_store.set_mode(Mode::ReadOnly); + // Step 7.1.2 If trusted or the implementation gives script-generated events access to the clipboard + if trusted { + // Step 7.1.2.1 For each clipboard-part on the OS clipboard: + + // Step 7.1.2.1.1 If clipboard-part contains plain text, then + let data = DOMString::from(text_contents.to_string()); + let type_ = DOMString::from("text/plain"); + let _ = drag_data_store.add(Kind::Text { data, type_ }); + + // Step 7.1.2.1.2 TODO If clipboard-part represents file references, then for each file reference + // Step 7.1.2.1.3 TODO If clipboard-part contains HTML- or XHTML-formatted text then + + // Step 7.1.3 Update clipboard-event-data’s files to match clipboard-event-data’s items + // Step 7.1.4 Update clipboard-event-data’s types to match clipboard-event-data’s items + } + }, + ClipboardEventType::Change => (), + } + + // Step 3 + let clipboard_event_data = DataTransfer::new( + &self.window, + Rc::new(RefCell::new(Some(drag_data_store))), + can_gc, + ); + + // Step 8 + event.set_clipboard_data(Some(&clipboard_event_data)); + let event = event.upcast::(); + // Step 9 + event.set_trusted(trusted); + // Step 10 Set event’s composed to true. + event.set_composed(true); + // Step 11 + event.dispatch(target, false, can_gc); + } + + pub(crate) fn fire_clipboardchange_event(&self, can_gc: CanGc) { + let clipboardchange_event = ClipboardEvent::new( + &self.window, + None, + DOMString::from("clipboardchange"), + EventBubbles::Bubbles, + EventCancelable::Cancelable, + None, + can_gc, + ); + self.fire_clipboard_event(&clipboardchange_event, ClipboardEventType::Change, can_gc); + } + + /// + fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) { + // Step 1 + if drag_data_store.list_len() > 0 { + // Step 1.1 Clear the clipboard. + self.window + .send_to_embedder(EmbedderMsg::ClearClipboard(self.window.webview_id())); + // Step 1.2 + for item in drag_data_store.iter_item_list() { + match item { + Kind::Text { data, .. } => { + // Step 1.2.1.1 Ensure encoding is correct per OS and locale conventions + // Step 1.2.1.2 Normalize line endings according to platform conventions + // Step 1.2.1.3 + self.window.send_to_embedder(EmbedderMsg::SetClipboardText( + self.window.webview_id(), + data.to_string(), + )); + }, + Kind::File { .. } => { + // Step 1.2.2 If data is of a type listed in the mandatory data types list, then + // Step 1.2.2.1 Place part on clipboard with the appropriate OS clipboard format description + // Step 1.2.3 Else this is left to the implementation + }, + } + } + } else { + // Step 2.1 + if drag_data_store.clear_was_called { + // Step 2.1.1 If types-to-clear list is empty, clear the clipboard + self.window + .send_to_embedder(EmbedderMsg::ClearClipboard(self.window.webview_id())); + // Step 2.1.2 Else remove the types in the list from the clipboard + // As of now this can't be done with Arboard, and it's possible that will be removed from the spec + } + } + } + + /// Handle scroll event triggered by user interactions from embedder side. + /// + #[allow(unsafe_code)] + fn handle_embedder_scroll_event(&self, event: ScrollEvent) { + // If it is a viewport scroll. + let document = self.window.Document(); + if event.external_id.is_root() { + document.handle_viewport_scroll_event(); + } else { + // Otherwise, check whether it is for a relevant element within the document. + let Some(node_id) = node_id_from_scroll_id(event.external_id.0 as usize) else { + return; + }; + let node = unsafe { + node::from_untrusted_node_address(UntrustedNodeAddress::from_id(node_id)) + }; + let Some(element) = node + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + .next() + else { + return; + }; + + document.handle_element_scroll_event(&element); + } + } } diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 716c3eaf89b..e66739aa319 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -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() diff --git a/components/script/dom/htmltextareaelement.rs b/components/script/dom/htmltextareaelement.rs index df0817dcf63..ac92867c4eb 100644 --- a/components/script/dom/htmltextareaelement.rs +++ b/components/script/dom/htmltextareaelement.rs @@ -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() diff --git a/components/script/dom/mouseevent.rs b/components/script/dom/mouseevent.rs index fe88f0b3356..cdcae214bb4 100644 --- a/components/script/dom/mouseevent.rs +++ b/components/script/dom/mouseevent.rs @@ -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::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, + ) + } + /// #[allow(clippy::too_many_arguments)] pub(crate) fn initialize_mouse_event( diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index cd7d38cc46b..ba7cedfa4b8 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -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 { &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); - }, - }; - } - - /// - 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::() - .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()); - })); - } - - /// - fn handle_gamepad_disconnect(&self, index: usize) { - let this = Trusted::new(self); - self.upcast::() - .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); - } - } - })); - } - - /// - fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) { - let this = Trusted::new(self); - - // - self.upcast::().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::().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); } } diff --git a/components/script/links.rs b/components/script/links.rs index f38ba6f7767..528ca717816 100644 --- a/components/script/links.rs +++ b/components/script/links.rs @@ -358,7 +358,10 @@ pub(crate) fn follow_hyperlink( let document = subject.owner_document(); let target_attribute_value = if subject.is::() || subject.is::() { - if document.alternate_action_keyboard_modifier_active() { + if document + .event_handler() + .alternate_action_keyboard_modifier_active() + { Some("_blank".into()) } else { get_element_target(subject) diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 8c842debd0d..3ed0574d626 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -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.