/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/document/document_event_handler.rs
3 052 строки
123 KB
Tim van der Lippe
script: Move individual interfaces into subfolders (#47060)
07 авг 2026, 12:10
Не верифицирован
07 авг 2026, 12:10
33036ff
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::array::from_ref; use std::cell::{Cell, RefCell}; use std::f64::consts::PI; use std::mem; use std::rc::Rc; use std::str::FromStr; use std::time::{Duration, Instant}; use embedder_traits::{ Cursor, EditingActionEvent, EmbedderMsg, ImeEvent, InputEvent, InputEventId, InputEventOutcome, InputEventResult, KeyboardEvent as EmbedderKeyboardEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseLeftViewportEvent, TouchEvent as EmbedderTouchEvent, TouchEventType, TouchId, TouchPointerType, UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent, }; #[cfg(feature = "gamepad")] use embedder_traits::{ GamepadEvent as EmbedderGamepadEvent, GamepadSupportedHapticEffects, GamepadUpdateType, }; use euclid::{Point2D, Vector2D}; use js::context::JSContext; use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey}; use layout_api::{HitTestFlags, ScrollContainerQueryFlags, node_id_from_scroll_id}; use rustc_hash::FxHashMap; use script_bindings::cell::DomRefCell; use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; use script_bindings::codegen::GenericBindings::ElementBinding::ScrollLogicalPosition; use script_bindings::codegen::GenericBindings::EventBinding::EventMethods; use script_bindings::codegen::GenericBindings::HTMLElementBinding::HTMLElementMethods; use script_bindings::codegen::GenericBindings::HTMLLabelElementBinding::HTMLLabelElementMethods; use script_bindings::codegen::GenericBindings::KeyboardEventBinding::KeyboardEventMethods; use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods; use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods; use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods}; use script_bindings::inheritance::Castable; use script_bindings::match_domstring_ascii; use script_bindings::num::Finite; use script_bindings::root::{Dom, DomRoot, DomSlice}; use script_bindings::str::DOMString; use script_traits::ConstellationInputEvent; use servo_base::generic_channel::GenericCallback; use servo_config::pref; use servo_constellation_traits::{KeyboardScroll, ScriptToConstellationMessage}; use style::Atom; use style_traits::CSSPixel; use webrender_api::ExternalScrollId; #[cfg(feature = "gamepad")] use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionName; use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::root::MutNullableDom; use crate::dom::bindings::trace::NoTrace; use crate::dom::clipboardevent::ClipboardEventType; use crate::dom::document::FireMouseEventType; use crate::dom::document::focus::FocusableArea; use crate::dom::event::{EventBubbles, EventCancelable, EventComposed, EventFlags}; #[cfg(feature = "gamepad")] use crate::dom::gamepad::gamepad::{Gamepad, contains_user_gesture}; #[cfg(feature = "gamepad")] use crate::dom::gamepad::gamepadevent::GamepadEventType; use crate::dom::inputevent::HitTestResult; use crate::dom::interactive_element_command::InteractiveElementCommand; use crate::dom::iterators::ShadowIncluding; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::node::{self, Node, NodeTraits}; use crate::dom::pointerevent::{PointerEvent, PointerId}; use crate::dom::types::{ ClipboardEvent, CompositionEvent, DataTransfer, Element, Event, EventTarget, GlobalScope, HTMLAnchorElement, HTMLElement, HTMLLabelElement, MouseEvent, Touch, TouchEvent, TouchList, WheelEvent, Window, }; use crate::dom::window::scrolling_box::{ScrollAxisState, ScrollRequirement, ScrollingBoxAxis}; use crate::drag_data_store::{DragDataStore, Kind, Mode}; use crate::realms::enter_auto_realm; /// A data structure used for tracking the current click count. This can be /// reset to 0 if a mouse button event happens at a sufficient distance or time /// from the previous one. /// /// From <https://w3c.github.io/uievents/#current-click-count>: /// > Implementations MUST maintain the current click count when generating mouse /// > events. This MUST be a non-negative integer indicating the number of consecutive /// > clicks of a pointing device button within a specific time. The delay after which /// > the count resets is specific to the environment configuration. #[derive(Default, JSTraceable, MallocSizeOf)] struct ClickCountingInfo { time: Option<Instant>, #[no_trace] point: Option<Point2D<f32, CSSPixel>>, #[no_trace] button: Option<MouseButton>, count: usize, } impl ClickCountingInfo { fn reset_click_count_if_necessary( &mut self, button: MouseButton, point_in_frame: Point2D<f32, CSSPixel>, ) { let (Some(previous_button), Some(previous_point), Some(previous_time)) = (self.button, self.point, self.time) else { assert_eq!(self.count, 0); return; }; let double_click_timeout = Duration::from_millis(pref!(dom_document_dblclick_timeout) as u64); let double_click_distance_threshold = pref!(dom_document_dblclick_dist) as u64; // Calculate distance between this click and the previous click. let line = point_in_frame - previous_point; let distance = (line.dot(line) as f64).sqrt(); if previous_button != button || Instant::now().duration_since(previous_time) > double_click_timeout || distance > double_click_distance_threshold as f64 { self.count = 0; self.time = None; self.point = None; } } fn increment_click_count( &mut self, button: MouseButton, point: Point2D<f32, CSSPixel>, ) -> usize { self.time = Some(Instant::now()); self.point = Some(point); self.button = Some(button); self.count += 1; self.count } } /// The [`DocumentEventHandler`] is a structure responsible for handling input events for /// the [`crate::Document`] and storing data related to event handling. It exists to /// decrease the size of the [`crate::Document`] structure. #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] pub(crate) struct DocumentEventHandler { /// The [`Window`] element for this [`DocumentEventHandler`]. window: Dom<Window>, /// Pending input events, to be handled at the next rendering opportunity. #[no_trace] #[ignore_malloc_size_of = "InputEvent contains data from outside crates"] pending_input_events: DomRefCell<Vec<ConstellationInputEvent>>, /// The index of the last mouse move event in the pending input events queue. mouse_move_event_index: DomRefCell<Option<usize>>, /// The [`InputEventId`]s of mousemove events that have been coalesced. #[no_trace] #[ignore_malloc_size_of = "InputEventId contains data from outside crates"] coalesced_mouse_move_event_ids: DomRefCell<Vec<InputEventId>>, /// The index of the last wheel event in the pending input events queue. /// This is non-standard behaviour. /// According to <https://www.w3.org/TR/pointerevents/#dfn-coalesced-events>, /// we should only coalesce `pointermove` events. wheel_event_index: DomRefCell<Option<usize>>, /// The [`InputEventId`]s of wheel events that have been coalesced. #[no_trace] #[ignore_malloc_size_of = "InputEventId contains data from outside crates"] coalesced_wheel_event_ids: DomRefCell<Vec<InputEventId>>, /// <https://w3c.github.io/uievents/#event-type-dblclick> click_counting_info: DomRefCell<ClickCountingInfo>, #[no_trace] last_mouse_button_down_point: Cell<Option<Point2D<f32, CSSPixel>>>, /// The number of mouse buttons currently being pressed. This is used to ensure /// that `pointerup` and `pointerdown` events are only sent when transitioning from /// having no mouse buttons pressed to having any and vice-versa. mouse_buttons_down: Cell<u32>, /// The element that is currently hovered by the cursor. current_hover_target: MutNullableDom<Element>, /// The element that was most recently activated during a mouse button press or touch /// event. current_active_element: MutNullableDom<Element>, /// The element that was most recently clicked. most_recently_clicked_element: MutNullableDom<Element>, /// The most recent mouse movement point, used for processing `mouseleave` events. #[no_trace] most_recent_mousemove_point: Cell<Option<Point2D<f32, CSSPixel>>>, /// The currently set [`Cursor`] or `None` if the `Document` isn't being hovered /// by the cursor. #[no_trace] current_cursor: Cell<Option<Cursor>>, /// <http://w3c.github.io/touch-events/#dfn-active-touch-point> active_touch_points: DomRefCell<Vec<Dom<Touch>>>, /// The active keyboard modifiers for the WebView. This is updated when receiving any input event. #[no_trace] active_keyboard_modifiers: Cell<Modifiers>, /// Map from touch identifier to pointer ID for active touch points active_pointer_ids: DomRefCell<FxHashMap<i32, i32>>, /// Counter for generating unique pointer IDs for touch inputs next_touch_pointer_id: Cell<i32>, /// A map holding information about currently registered access key handlers. access_key_handlers: DomRefCell<FxHashMap<NoTrace<Code>, Dom<HTMLElement>>>, /// Map from pointer ID to pending pointer capture target override element. /// This is set by setPointerCapture and cleared by releasePointerCapture. /// <https://w3c.github.io/pointerevents/#pointer-capture> pending_pointer_capture: DomRefCell<FxHashMap<i32, Dom<Element>>>, /// Map from pointer ID to the actual/current pointer capture target. /// Updated during process_pending_pointer_capture when events are dispatched. pointer_capture_target: DomRefCell<FxHashMap<i32, Dom<Element>>>, } impl DocumentEventHandler { pub(crate) fn new(window: &Window) -> Self { Self { window: Dom::from_ref(window), pending_input_events: Default::default(), mouse_move_event_index: Default::default(), coalesced_mouse_move_event_ids: Default::default(), wheel_event_index: Default::default(), coalesced_wheel_event_ids: Default::default(), click_counting_info: Default::default(), last_mouse_button_down_point: Default::default(), mouse_buttons_down: Cell::new(0), current_hover_target: Default::default(), current_active_element: Default::default(), most_recently_clicked_element: Default::default(), most_recent_mousemove_point: Default::default(), current_cursor: Default::default(), active_touch_points: Default::default(), active_keyboard_modifiers: Default::default(), active_pointer_ids: Default::default(), next_touch_pointer_id: Cell::new(1), access_key_handlers: Default::default(), pending_pointer_capture: Default::default(), pointer_capture_target: Default::default(), } } /// Note a pending input event, to be processed at the next `update_the_rendering` task. pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) { let mut pending_input_events = self.pending_input_events.borrow_mut(); if matches!(event.event.event, InputEvent::MouseMove(..)) { // First try to replace any existing mouse move event. if let Some(mouse_move_event) = self .mouse_move_event_index .borrow() .and_then(|index| pending_input_events.get_mut(index)) { self.coalesced_mouse_move_event_ids .borrow_mut() .push(mouse_move_event.event.id); *mouse_move_event = event; return; } *self.mouse_move_event_index.borrow_mut() = Some(pending_input_events.len()); } if let InputEvent::Wheel(ref new_wheel_event) = event.event.event { // Coalesce with any existing pending wheel event by summing deltas. if let Some(existing_constellation_wheel_event) = self .wheel_event_index .borrow() .and_then(|index| pending_input_events.get_mut(index)) && let InputEvent::Wheel(ref mut existing_wheel_event) = existing_constellation_wheel_event.event.event && existing_wheel_event.delta.mode == new_wheel_event.delta.mode { self.coalesced_wheel_event_ids .borrow_mut() .push(existing_constellation_wheel_event.event.id); existing_wheel_event.delta.x += new_wheel_event.delta.x; existing_wheel_event.delta.y += new_wheel_event.delta.y; existing_wheel_event.delta.z += new_wheel_event.delta.z; existing_wheel_event.point = new_wheel_event.point; existing_constellation_wheel_event.event.id = event.event.id; return; } *self.wheel_event_index.borrow_mut() = Some(pending_input_events.len()); } pending_input_events.push(event); } /// Whether or not this [`Document`] has any pending input events to be processed during /// "update the rendering." pub(crate) fn has_pending_input_events(&self) -> bool { !self.pending_input_events.borrow().is_empty() } pub(crate) fn alternate_action_keyboard_modifier_active(&self) -> bool { #[cfg(target_os = "macos")] { self.active_keyboard_modifiers .get() .contains(Modifiers::META) } #[cfg(not(target_os = "macos"))] { self.active_keyboard_modifiers .get() .contains(Modifiers::CONTROL) } } pub(crate) fn handle_pending_input_events(&self, cx: &mut JSContext) { debug_assert!( !self.pending_input_events.borrow().is_empty(), "handle_pending_input_events called with no events" ); let mut realm = enter_auto_realm(cx, &*self.window); let cx = &mut realm.current_realm(); // Reset the mouse and wheel event indices. *self.mouse_move_event_index.safe_borrow_mut(cx.no_gc()) = None; *self.wheel_event_index.safe_borrow_mut(cx.no_gc()) = None; let pending_input_events = mem::take(&mut *self.pending_input_events.safe_borrow_mut(cx.no_gc())); let mut coalesced_mouse_move_event_ids = mem::take( &mut *self .coalesced_mouse_move_event_ids .safe_borrow_mut(cx.no_gc()), ); let mut coalesced_wheel_event_ids = mem::take(&mut *self.coalesced_wheel_event_ids.safe_borrow_mut(cx.no_gc())); let mut input_event_outcomes = Vec::with_capacity( pending_input_events.len() + coalesced_mouse_move_event_ids.len() + coalesced_wheel_event_ids.len(), ); // TODO: For some of these we still aren't properly calculating whether or not // the event was handled or if `preventDefault()` was called on it. Each of // these cases needs to be examined and some of them either fire more than one // event or fire events later. We have to make a good decision about what to // return to the embedder when that happens. for event in pending_input_events { self.active_keyboard_modifiers .set(event.active_keyboard_modifiers); let result = match event.event.event { InputEvent::MouseButton(mouse_button_event) => { self.handle_native_mouse_button_event(cx, mouse_button_event, &event); InputEventResult::default() }, InputEvent::MouseMove(_) => { self.handle_native_mouse_move_event(cx, &event); input_event_outcomes.extend( mem::take(&mut coalesced_mouse_move_event_ids) .into_iter() .map(|id| InputEventOutcome { id, result: InputEventResult::default(), }), ); InputEventResult::default() }, InputEvent::MouseLeftViewport(mouse_leave_event) => { self.handle_mouse_left_viewport_event(cx, &event, &mouse_leave_event); InputEventResult::default() }, InputEvent::Touch(touch_event) => self.handle_touch_event(cx, touch_event, &event), InputEvent::Wheel(wheel_event) => { let result = self.handle_wheel_event(cx, wheel_event, &event); input_event_outcomes.extend( mem::take(&mut coalesced_wheel_event_ids) .into_iter() .map(|id| InputEventOutcome { id, result }), ); result }, InputEvent::Keyboard(keyboard_event) => { self.handle_keyboard_event(cx, keyboard_event) }, InputEvent::Ime(ime_event) => self.handle_ime_event(cx, ime_event), #[cfg(feature = "gamepad")] InputEvent::Gamepad(gamepad_event) => { self.handle_gamepad_event(gamepad_event); InputEventResult::default() }, InputEvent::EditingAction(editing_action_event) => { self.handle_editing_action(cx, None, editing_action_event) }, }; input_event_outcomes.push(InputEventOutcome { id: event.event.id, result, }); } self.notify_embedder_that_events_were_handled(input_event_outcomes); } fn notify_embedder_that_events_were_handled( &self, input_event_outcomes: Vec<InputEventOutcome>, ) { // Wait to to notify the embedder that the event was handled until all pending DOM // event processing is finished. let trusted_window = Trusted::new(&*self.window); self.window .as_global_scope() .task_manager() .dom_manipulation_task_source() .queue(task!(notify_webdriver_input_event_completed: move || { let window = trusted_window.root(); window.send_to_embedder( EmbedderMsg::InputEventsHandled(window.webview_id(), input_event_outcomes)); })); } /// When an event should be fired on the element that has focus, this returns the target. If /// there is no associated element with the focused area (such as when the viewport is focused), /// then the body is returned. If no body is returned then the `Window` is returned. fn target_for_events_following_focus(&self) -> DomRoot<EventTarget> { let document = self.window.Document(); match &*document.focus_handler().focused_area() { FocusableArea::Node { node, .. } => DomRoot::from_ref(node.upcast()), FocusableArea::IFrameViewport { iframe_element, .. } => { DomRoot::from_ref(iframe_element.upcast()) }, FocusableArea::Viewport => document .GetBody() .map(DomRoot::upcast) .unwrap_or_else(|| DomRoot::from_ref(self.window.upcast())), } } pub(crate) fn set_cursor(&self, cursor: Option<Cursor>) { if cursor == self.current_cursor.get() { return; } self.current_cursor.set(cursor); self.window.send_to_embedder(EmbedderMsg::SetCursor( self.window.webview_id(), cursor.unwrap_or_default(), )); } fn handle_mouse_left_viewport_event( &self, cx: &mut JSContext, input_event: &ConstellationInputEvent, mouse_leave_event: &MouseLeftViewportEvent, ) { if let Some(current_hover_target) = self.current_hover_target.get() { let current_hover_target = current_hover_target.upcast::<Node>(); for element in current_hover_target .inclusive_ancestors(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::<Element>) { element.set_hover_state(false); } let flags = HitTestFlags::empty(); if let Some(hit_test_result) = self .most_recent_mousemove_point .get() .and_then(|point| self.window.hit_test_from_point_in_viewport(flags, point)) { let mouse_out_event = MouseEvent::new_for_platform_motion_event( cx, &self.window, FireMouseEventType::Out, &hit_test_result, input_event, ); // Fire pointerout before mouseout mouse_out_event .to_pointer_hover_event(cx, "pointerout") .upcast::<Event>() .fire(cx, current_hover_target.upcast()); mouse_out_event .upcast::<Event>() .fire(cx, current_hover_target.upcast()); self.handle_mouse_enter_leave_event( cx, current_hover_target, None, FireMouseEventType::Leave, &hit_test_result, input_event, ); } } // We do not want to always inform the embedder that cursor has been set to the // default cursor, in order to avoid a timing issue when moving between `<iframe>` // elements. There is currently no way to control which `SetCursor` message will // reach the embedder first. This is safer when leaving the `WebView` entirely. if !mouse_leave_event.focus_moving_to_another_iframe { // If focus is moving to another frame, it will decide what the new status // text is, but if this mouse leave event is leaving the WebView entirely, // then clear it. self.window .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None)); self.set_cursor(None); } else { self.current_cursor.set(None); } self.current_hover_target.set(None); self.most_recent_mousemove_point.set(None); } fn handle_mouse_enter_leave_event( &self, cx: &mut JSContext, event_target: &Node, related_target: Option<&Node>, event_type: FireMouseEventType, hit_test_result: &HitTestResult, input_event: &ConstellationInputEvent, ) { assert!(matches!( event_type, FireMouseEventType::Enter | FireMouseEventType::Leave )); let common_ancestor = match related_target.as_ref() { Some(related_target) => event_target .common_ancestor_in_flat_tree(related_target) .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<_> = event_target .inclusive_ancestors_in_flat_tree() .take_while(|node| *node != common_ancestor) .collect(); // The order for dispatching mouseenter/pointerenter events starts from the topmost // common ancestor of the event target and the related target. if event_type == FireMouseEventType::Enter { targets.reverse(); } let pointer_event_name = match event_type { FireMouseEventType::Enter => "pointerenter", FireMouseEventType::Leave => "pointerleave", _ => unreachable!(), }; for target in targets { let mouse_event = MouseEvent::new_for_platform_motion_event( cx, &self.window, event_type, hit_test_result, input_event, ); mouse_event .upcast::<Event>() .set_related_target(related_target.as_ref().map(|target| target.upcast())); // Fire pointer event before mouse event mouse_event .to_pointer_hover_event(cx, pointer_event_name) .upcast::<Event>() .fire(cx, target.upcast()); // Fire mouse event mouse_event.upcast::<Event>().fire(cx, target.upcast()); } } /// <https://w3c.github.io/uievents/#handle-native-mouse-move> fn handle_native_mouse_move_event( &self, cx: &mut JSContext, input_event: &ConstellationInputEvent, ) { // First check if the capture target is disconnected and release it if so. // This must happen before any pointer event fires. let pointer_id = PointerId::Mouse as i32; let released_disconnected = self.release_disconnected_pointer_capture(cx, pointer_id, "mouse", true); let flags = if input_event.primary_button_is_pressed() { HitTestFlags::IncludeDomPosition } else { HitTestFlags::empty() }; // Always do full hit test so we can keep `current_hover_target` in sync // with the actual element under the pointer. Boundary events for hover // transitions are suppressed while pointer capture is active: per spec, // pointer events are retargeted to the capture element. let Some(hit_test_result) = self.window.hit_test_from_input_event(flags, input_event) else { return; }; let old_mouse_move_point = self .most_recent_mousemove_point .replace(Some(hit_test_result.point_in_frame)); if old_mouse_move_point == Some(hit_test_result.point_in_frame) { return; } // Update the cursor when the mouse moves, if it has changed. self.set_cursor(Some(hit_test_result.cursor)); let Some(new_target) = hit_test_result .node .inclusive_ancestors(ShadowIncluding::Yes) .find_map(DomRoot::downcast::<Element>) else { return; }; let capture_is_active = self.get_pointer_capture_target(pointer_id).is_some(); let old_hover_target = self.current_hover_target.get(); let target_has_changed = old_hover_target .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 pointerout/mouseout and pointerleave/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::<Node>() .is_ancestor_of(new_target.upcast::<Node>()); // 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::<Node>() .inclusive_ancestors(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::<Element>) { element.set_hover_state(false); } } if !capture_is_active { let mouse_out_event = MouseEvent::new_for_platform_motion_event( cx, &self.window, FireMouseEventType::Out, &hit_test_result, input_event, ); mouse_out_event .upcast::<Event>() .set_related_target(Some(new_target.upcast())); // Fire pointerout before mouseout mouse_out_event .to_pointer_hover_event(cx, "pointerout") .upcast::<Event>() .fire(cx, old_target.upcast()); mouse_out_event .upcast::<Event>() .fire(cx, old_target.upcast()); if !old_target_is_ancestor_of_new_target { let event_target = old_target.upcast::<Node>(); let moving_into = Some(new_target.upcast::<Node>()); self.handle_mouse_enter_leave_event( cx, event_target, moving_into, FireMouseEventType::Leave, &hit_test_result, input_event, ); } } } // Dispatch pointerover/mouseover and pointerenter/mouseenter to new target. for element in new_target .upcast::<Node>() .inclusive_ancestors(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::<Element>) { element.set_hover_state(true); } if !capture_is_active { let mouse_over_event = MouseEvent::new_for_platform_motion_event( cx, &self.window, FireMouseEventType::Over, &hit_test_result, input_event, ); mouse_over_event .upcast::<Event>() .set_related_target(old_hover_target.as_ref().map(|target| target.upcast())); // Fire pointerover before mouseover mouse_over_event .to_pointer_hover_event(cx, "pointerover") .upcast::<Event>() .dispatch(cx, new_target.upcast(), false); mouse_over_event .upcast::<Event>() .dispatch(cx, new_target.upcast(), false); let moving_from = old_hover_target .as_ref() .map(|old_target| old_target.upcast::<Node>()); let event_target = new_target.upcast::<Node>(); self.handle_mouse_enter_leave_event( cx, event_target, moving_from, FireMouseEventType::Enter, &hit_test_result, input_event, ); } } // Send mousemove event to topmost target, unless it's an iframe, in which case // `Paint` should have also sent an event to the inner document. let mouse_event = MouseEvent::new_for_platform_motion_event( cx, &self.window, FireMouseEventType::Move, &hit_test_result, input_event, ); // Send pointermove event before mousemove. // If pointer capture is active, retarget the pointer/mouse events to // the capture element. Boundary events (pointerover/out/enter/leave) // already fired above use the actual hit-test target. let pointer_target = self .get_pointer_capture_target(pointer_id) .map(DomRoot::upcast::<EventTarget>) .unwrap_or_else(|| DomRoot::from_ref(new_target.upcast::<EventTarget>())); let pointer_event = mouse_event.to_pointer_event(cx, Atom::from("pointermove")); pointer_event.upcast::<Event>().set_composed(true); pointer_event.upcast::<Event>().fire(cx, &pointer_target); // Process pending pointer capture after firing event, but skip if we just // released a disconnected capture to avoid immediately re-capturing. // https://w3c.github.io/pointerevents/#process-pending-pointer-capture if !released_disconnected { self.process_pending_pointer_capture(cx, pointer_id, "mouse", true); } // Send mousemove event. Routed to the capture target when capture is active. mouse_event.upcast::<Event>().fire(cx, &pointer_target); self.update_current_hover_target_and_status(Some(new_target)); } fn update_current_hover_target_and_status(&self, new_hover_target: Option<DomRoot<Element>>) { 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() && let Some(anchor) = target .upcast::<Node>() .inclusive_ancestors(ShadowIncluding::Yes) .find_map(DomRoot::downcast::<HTMLAnchorElement>) { let status = anchor .full_href_url_for_user_interface() .map(|url| url.to_string()); 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::<Node>() .inclusive_ancestors(ShadowIncluding::Yes) .any(|node| node.is::<HTMLAnchorElement>()) }) { self.window .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None)); } } pub(crate) fn handle_refresh_cursor(&self) { let Some(most_recent_mousemove_point) = self.most_recent_mousemove_point.get() else { return; }; let flags = HitTestFlags::empty(); let Some(hit_test_result) = self .window .hit_test_from_point_in_viewport(flags, most_recent_mousemove_point) else { return; }; self.set_cursor(Some(hit_test_result.cursor)); } fn set_active_element(&self, original_target: &Element) { let find_element_for_activation = |element: &Element| { let node: &Node = element.upcast(); if node.is_in_ua_widget() && let Some(containing_shadow_root) = node.containing_shadow_root() { return containing_shadow_root.Host(); } // If the element is a label, the activable element is the control element. if node.type_id() == NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLLabelElement, )) { let label = element.downcast::<HTMLLabelElement>().unwrap(); if let Some(control) = label.GetControl() { return DomRoot::from_ref(control.upcast::<Element>()); } } DomRoot::from_ref(element) }; let element_for_activation = find_element_for_activation(original_target); // This might happen if the user is alternating between different pointing devices // such as two mice or a mouse and touch events. Only keep the latest activated. if let Some(currently_active_element) = self.current_active_element.get() { if currently_active_element == element_for_activation { return; } self.unset_active_element(); } element_for_activation.set_active_state(true); self.current_active_element .set(Some(&*element_for_activation)); } fn unset_active_element(&self) { if let Some(active_element) = self.current_active_element.take() { active_element.set_active_state(false); } } /// <https://w3c.github.io/uievents/#mouseevent-algorithms> /// Handles native mouse down, mouse up, mouse click. fn handle_native_mouse_button_event( &self, cx: &mut JSContext, event: MouseButtonEvent, input_event: &ConstellationInputEvent, ) { let flags = if input_event.primary_button_is_pressed() { HitTestFlags::IncludeDomPosition } else { HitTestFlags::empty() }; // Ignore all incoming events without a hit test. let Some(hit_test_result) = self.window.hit_test_from_input_event(flags, input_event) else { return; }; debug!( "{:?}: at {:?}", event.action, hit_test_result.point_in_frame ); // Set the sequential focus navigation starting point for any mouse button down event, no // matter if the target is not a node. let document = self.window.Document(); if event.action == MouseButtonAction::Down { document .focus_handler() .set_sequential_focus_navigation_starting_point(&hit_test_result.node); } let Some(element) = hit_test_result .node .inclusive_ancestors(ShadowIncluding::Yes) .find_map(DomRoot::downcast::<Element>) else { return; }; let node = element.upcast::<Node>(); debug!("{:?} on {:?}", event.action, node.debug_str()); // <https://html.spec.whatwg.org/multipage/#selector-active> // > If the element is being actively pointed at the element is being activated. // Disabled elements can also be activated, so this must happen before the // early return below. if event.button == MouseButton::Left { if event.action == MouseButtonAction::Down { self.set_active_element(&element); } if event.action == MouseButtonAction::Up { self.unset_active_element(); } } // https://w3c.github.io/uievents/#hit-test // Prevent mouse event if element is disabled. // TODO: also inert. if element.is_actually_disabled() { return; } let mouse_event_type = match event.action { embedder_traits::MouseButtonAction::Up => atom!("mouseup"), embedder_traits::MouseButtonAction::Down => atom!("mousedown"), }; // From <https://w3c.github.io/pointerevents/#dfn-mousedown> // and <https://w3c.github.io/pointerevents/#mouseup>: // // UIEvent.detail: indicates the current click count incremented by one. For // example, if no click happened before the mousedown, detail will contain // the value 1 if event.action == MouseButtonAction::Down { self.click_counting_info .safe_borrow_mut(cx.no_gc()) .reset_click_count_if_necessary(event.button, hit_test_result.point_in_frame); } let mouse_event = MouseEvent::for_platform_button_event( cx, mouse_event_type, event, input_event.pressed_mouse_buttons, &self.window, &hit_test_result, input_event.active_keyboard_modifiers, self.click_counting_info.borrow().count + 1, ); match event.action { MouseButtonAction::Down => { self.last_mouse_button_down_point .set(Some(hit_test_result.point_in_frame)); // Step 6. Dispatch pointerdown event. let mouse_buttons_down = self.mouse_buttons_down.get(); let pointer_event_name = if mouse_buttons_down == 0 { // From <https://w3c.github.io/pointerevents/#dfn-pointerdown> // > The user agent MUST fire a pointer event named pointerdown when a pointer enters // > the active buttons state. For mouse, this is when the device transitions from no // > buttons depressed to at least one button depressed. "pointerdown".into() } else { // From <https://w3c.github.io/pointerevents/#dfn-pointermove>: // > The user agent MUST fire a pointer event named pointermove when a pointer // > changes any properties that don't fire pointerdown or pointerup events. This // > includes any changes to coordinates, pressure, tangential pressure, tilt, twist, // > contact geometry (width and height) or chorded buttons. "pointermove".into() }; let pointer_event = mouse_event.to_pointer_event(cx, pointer_event_name); // Check for pointer capture target for mouse events let pointer_id = PointerId::Mouse as i32; // Release any disconnected capture target before firing pointer events let released_disconnected = self.release_disconnected_pointer_capture(cx, pointer_id, "mouse", true); // Get the current capture target (before processing pending changes) let pointer_target = self .get_pointer_capture_target(pointer_id) .map(DomRoot::upcast::<EventTarget>) .unwrap_or_else(|| DomRoot::from_ref(node.upcast::<EventTarget>())); // Increment button count before firing so setPointerCapture works in handler. self.mouse_buttons_down.set(mouse_buttons_down + 1); pointer_event.upcast::<Event>().fire(cx, &pointer_target); // Process pending pointer capture after firing event, but skip if we just // released a disconnected capture to avoid immediately re-capturing. // https://w3c.github.io/pointerevents/#process-pending-pointer-capture if !released_disconnected { self.process_pending_pointer_capture(cx, pointer_id, "mouse", true); } // Step 7. Let result = dispatch event at target let result = mouse_event .upcast::<Event>() .dispatch(cx, node.upcast(), false); // 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 { // Note that this differs from the specification, because we are going to look // for the first inclusive ancestor that is click focusable and then focus it. // See documentation for [`Node::find_click_focusable_area`]. document .focus_handler() .focus(cx, &node.find_click_focusable_area(cx)); } // Step 9. If mbutton is the secondary mouse button, then // Maybe show context menu with native, target. if let MouseButton::Right = event.button { self.maybe_show_context_menu(cx, node.upcast(), &hit_test_result, input_event); } }, // https://w3c.github.io/pointerevents/#dfn-handle-native-mouse-up MouseButtonAction::Up => { // Step 6. Dispatch pointerup event. let mouse_buttons_down = self.mouse_buttons_down.get(); let pointer_event_name = if mouse_buttons_down == 1 { // From <https://w3c.github.io/pointerevents/#dfn-pointerup>: // > The user agent MUST fire a pointer event named pointerup when a pointer leaves // > the active buttons state. For mouse, this is when the device transitions from at // > least one button depressed to no buttons depressed. "pointerup".into() } else { // From <https://w3c.github.io/pointerevents/#dfn-pointermove>: // > The user agent MUST fire a pointer event named pointermove when a pointer // > changes any properties that don't fire pointerdown or pointerup events. This // > includes any changes to coordinates, pressure, tangential pressure, tilt, twist, // > contact geometry (width and height) or chorded buttons. "pointermove".into() }; let pointer_event = mouse_event.to_pointer_event(cx, pointer_event_name); // Check for pointer capture target for mouse events let pointer_id = PointerId::Mouse as i32; // Release any disconnected capture target before firing pointer events let released_disconnected = self.release_disconnected_pointer_capture(cx, pointer_id, "mouse", true); // Get the current capture target (before any state changes) let pointer_target = self .get_pointer_capture_target(pointer_id) .map(DomRoot::upcast::<EventTarget>) .unwrap_or_else(|| DomRoot::from_ref(node.upcast::<EventTarget>())); pointer_event.upcast::<Event>().fire(cx, &pointer_target); // Decrement button count after firing event, so setPointerCapture/releasePointerCapture // work during the pointerup handler (pointer is still "active"). self.mouse_buttons_down .set(mouse_buttons_down.saturating_sub(1)); // Process pending pointer capture after decrementing button count, but skip // if we just released a disconnected capture to avoid immediately re-capturing. // https://w3c.github.io/pointerevents/#process-pending-pointer-capture if !released_disconnected { self.process_pending_pointer_capture(cx, pointer_id, "mouse", true); } // Implicitly release pointer capture when last button was released if mouse_buttons_down == 1 { self.implicit_release_pointer_capture(cx, pointer_id, "mouse", true); } // Step 7. dispatch event at target. mouse_event .upcast::<Event>() .dispatch(cx, node.upcast(), false); // Click counts should still work for other buttons even though they // do not trigger "click" and "dblclick" events, so we increment // even when those events are not fired. self.click_counting_info .safe_borrow_mut(cx.no_gc()) .increment_click_count(event.button, hit_test_result.point_in_frame); self.maybe_trigger_click_for_mouse_button_down_event( cx, event, input_event, &hit_test_result, &element, ); }, } } /// <https://w3c.github.io/pointerevents/#handle-native-mouse-click> /// <https://w3c.github.io/pointerevents/#handle-native-mouse-double-click> fn maybe_trigger_click_for_mouse_button_down_event( &self, cx: &mut JSContext, event: MouseButtonEvent, input_event: &ConstellationInputEvent, hit_test_result: &HitTestResult, element: &Element, ) { if event.button != MouseButton::Left { return; } let Some(last_mouse_button_down_point) = self.last_mouse_button_down_point.take() else { return; }; let distance = last_mouse_button_down_point.distance_to(hit_test_result.point_in_frame); let maximum_click_distance = 10.0 * self.window.device_pixel_ratio().get(); if distance > maximum_click_distance { return; } // From <https://w3c.github.io/pointerevents/#click> // > The click event type MUST be dispatched on the topmost event target indicated by the // > pointer, when the user presses down and releases the primary pointer button. let element = &element.inclusive_ancestor_element_in_non_ua_shadow_root(); self.most_recently_clicked_element.set(Some(element)); let click_count = self.click_counting_info.borrow().count; element.set_click_in_progress(true); MouseEvent::for_platform_button_event( cx, atom!("click"), event, input_event.pressed_mouse_buttons, &self.window, hit_test_result, input_event.active_keyboard_modifiers, click_count, ) .upcast::<Event>() .dispatch(cx, element.upcast(), false); element.set_click_in_progress(false); // The firing of "dbclick" events is dependent on the platform, so we have // some flexibility here. Some browsers on some platforms only fire a // "dbclick" when the click count is 2 and others essentially fire one for // every 2 clicks in a sequence. In all cases, browsers set the click count // `detail` property to 2. // // We follow the latter approach here, considering that every sequence of // even numbered clicks is a series of double clicks. if click_count.is_multiple_of(2) { MouseEvent::for_platform_button_event( cx, Atom::from("dblclick"), event, input_event.pressed_mouse_buttons, &self.window, hit_test_result, input_event.active_keyboard_modifiers, 2, ) .upcast::<Event>() .dispatch(cx, element.upcast(), false); } } /// <https://www.w3.org/TR/pointerevents4/#maybe-show-context-menu> fn maybe_show_context_menu( &self, cx: &mut js::context::JSContext, target: &EventTarget, hit_test_result: &HitTestResult, input_event: &ConstellationInputEvent, ) { // <https://w3c.github.io/pointerevents/#contextmenu> let menu_event = PointerEvent::new( cx, &self.window, // window "contextmenu".into(), // 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 ); menu_event.upcast::<Event>().set_composed(true); // Step 3. Let result = dispatch menuevent at target. let result = menu_event.upcast::<Event>().fire(cx, target); // Step 4. If result is true, then show the UA context menu if result { self.window .Document() .embedder_controls() .show_context_menu(hit_test_result); }; } fn handle_touch_event( &self, cx: &mut JSContext, event: EmbedderTouchEvent, input_event: &ConstellationInputEvent, ) -> InputEventResult { let flags = HitTestFlags::empty(); // Ignore all incoming events without a hit test. let Some(hit_test_result) = self.window.hit_test_from_input_event(flags, input_event) else { self.update_active_touch_points_when_early_return(event); return Default::default(); }; let TouchId(identifier) = event.touch_id; let Some(element) = hit_test_result .node .inclusive_ancestors(ShadowIncluding::Yes) .find_map(DomRoot::downcast::<Element>) else { self.update_active_touch_points_when_early_return(event); return Default::default(); }; let current_target = DomRoot::upcast::<EventTarget>(element.clone()); 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); // This is used to construct pointerevent and touchdown event. let pointer_touch = Touch::new( cx, window, identifier, ¤t_target, client_x, client_y, // TODO: Get real screen coordinates? client_x, client_y, page_x, page_y, ); // Dispatch pointer event before updating active touch points and before touch event. let pointer_event_name = match event.event_type { TouchEventType::Down => "pointerdown", TouchEventType::Move => "pointermove", TouchEventType::Up => "pointerup", TouchEventType::Cancel => "pointercancel", }; // Map the embedder-side subtype to the spec `pointerType` string. let pointer_type = match event.pointer_type { TouchPointerType::Pen => "pen", TouchPointerType::Touch => "touch", }; // Get or create pointer ID for this touch let pointer_id = self.get_or_create_pointer_id_for_touch(identifier); let is_primary = self.is_primary_pointer(pointer_id); // For touch devices (which don't support hover), fire pointerover/pointerenter // <https://w3c.github.io/pointerevents/#mapping-for-devices-that-do-not-support-hover> if matches!(event.event_type, TouchEventType::Down) { // Fire pointerover let pointer_over = pointer_touch.to_pointer_event( cx, window, "pointerover", pointer_id, is_primary, pointer_type, input_event.active_keyboard_modifiers, true, // cancelable Some(hit_test_result.point_in_node), ); pointer_over.upcast::<Event>().fire(cx, ¤t_target); // Fire pointerenter hierarchically (from topmost ancestor to target) self.fire_pointer_event_for_touch( cx, &element, &pointer_touch, pointer_id, "pointerenter", is_primary, pointer_type, input_event, &hit_test_result, ); } // Release any disconnected capture target before firing pointer events, // but not for pointercancel: let implicit_release handle that so // lostpointercapture fires after pointercancel per spec. let released_disconnected = if matches!(event.event_type, TouchEventType::Cancel) { false } else { self.release_disconnected_pointer_capture(cx, pointer_id, pointer_type, is_primary) }; // Get the current capture target (before processing pending changes) let pointer_target = self .get_pointer_capture_target(pointer_id) .map(DomRoot::upcast::<EventTarget>) .unwrap_or_else(|| current_target.clone()); let pointer_event = pointer_touch.to_pointer_event( cx, window, pointer_event_name, pointer_id, is_primary, pointer_type, input_event.active_keyboard_modifiers, event.is_cancelable(), Some(hit_test_result.point_in_node), ); pointer_event.upcast::<Event>().fire(cx, &pointer_target); // Process pending pointer capture after firing event, but skip if we just // released a disconnected capture (to avoid immediately re-capturing). // Also skip for pointercancel: per spec, process pending only runs for // pointerdown, pointermove, and pointerup, not pointercancel. // https://w3c.github.io/pointerevents/#process-pending-pointer-capture if !released_disconnected && !matches!(event.event_type, TouchEventType::Cancel) { self.process_pending_pointer_capture(cx, pointer_id, pointer_type, is_primary); } // Implicitly release pointer capture on pointerup or pointercancel // For pointercancel, this fires lostpointercapture after the pointercancel event. if matches!( event.event_type, TouchEventType::Up | TouchEventType::Cancel ) { self.implicit_release_pointer_capture(cx, pointer_id, pointer_type, is_primary); } // For touch devices, fire pointerout/pointerleave after pointerup/pointercancel // <https://w3c.github.io/pointerevents/#mapping-for-devices-that-do-not-support-hover> if matches!( event.event_type, TouchEventType::Up | TouchEventType::Cancel ) { // Fire pointerout let pointer_out = pointer_touch.to_pointer_event( cx, window, "pointerout", pointer_id, is_primary, pointer_type, input_event.active_keyboard_modifiers, true, // cancelable Some(hit_test_result.point_in_node), ); pointer_out.upcast::<Event>().fire(cx, ¤t_target); // Fire pointerleave hierarchically (from target to topmost ancestor) self.fire_pointer_event_for_touch( cx, &element, &pointer_touch, pointer_id, "pointerleave", is_primary, pointer_type, input_event, &hit_test_result, ); } let (touch_dispatch_target, changed_touch) = match event.event_type { TouchEventType::Down => { // Add a new touch point self.active_touch_points .safe_borrow_mut(cx.no_gc()) .push(Dom::from_ref(&*pointer_touch)); self.set_active_element(&element); (current_target, pointer_touch) }, _ => { // From <https://w3c.github.io/touch-events/#dfn-touchend>: // > For move/up/cancel: // > The target of this event must be the same Element on which the touch // > point started when it was first placed on the surface, even if the touch point // > has since moved outside the interactive area of the target element. let active_touch_points = self.active_touch_points.borrow(); let Some(index) = active_touch_points .iter() .position(|point| point.Identifier() == identifier) else { warn!("No active touch point for {:?}", event.event_type); return Default::default(); }; // This is the original target that was selected during `touchstart` event handling. let original_target = active_touch_points[index].Target(); drop(active_touch_points); let touch_with_touchstart_target = Touch::new( cx, window, identifier, &original_target, client_x, client_y, client_x, client_y, page_x, page_y, ); let mut active_touch_points = self.active_touch_points.safe_borrow_mut(cx.no_gc()); // Update or remove the stored touch match event.event_type { TouchEventType::Move => { active_touch_points[index] = Dom::from_ref(&*touch_with_touchstart_target); }, TouchEventType::Up | TouchEventType::Cancel => { active_touch_points.swap_remove(index); self.remove_pointer_id_for_touch(identifier); self.unset_active_element(); }, TouchEventType::Down => unreachable!("Should have been handled above"), } (original_target, touch_with_touchstart_target) }, }; rooted_vec!(let mut target_touches); target_touches.extend( self.active_touch_points .borrow() .iter() .filter(|touch| touch.Target() == touch_dispatch_target) .cloned(), ); let event_name = match event.event_type { TouchEventType::Down => "touchstart", TouchEventType::Move => "touchmove", TouchEventType::Up => "touchend", TouchEventType::Cancel => "touchcancel", }; let touches = TouchList::new(cx, window, self.active_touch_points.borrow().r()); let changed_touches = TouchList::new(cx, window, from_ref(&&*changed_touch)); let target_touches = TouchList::new(cx, window, target_touches.r()); let touch_event = TouchEvent::new( cx, window, event_name.into(), EventBubbles::Bubbles, EventCancelable::from(event.is_cancelable()), EventComposed::Composed, Some(window), 0i32, &touches, &changed_touches, &target_touches, // FIXME: modifier keys false, false, false, false, ); let event = touch_event.upcast::<Event>(); event.fire(cx, &touch_dispatch_target); event.flags().into() } /// Updates the active touch points when a hit test fails early. /// /// - For `Down`: No action needed; a failed down event won't create an active point. /// - For `Move`: No action needed; position information is unavailable, so we cannot update. /// - For `Up`/`Cancel`: Remove the corresponding touch point and its pointer ID mapping. /// /// When a touchup or touchcancel occurs at that touch point, /// a warning is triggered: Received touchup/touchcancel event for a non-active touch point. fn update_active_touch_points_when_early_return(&self, event: EmbedderTouchEvent) { match event.event_type { TouchEventType::Down | TouchEventType::Move => {}, TouchEventType::Up | TouchEventType::Cancel => { let mut active_touch_points = self.active_touch_points.borrow_mut(); if let Some(index) = active_touch_points .iter() .position(|t| t.Identifier() == event.touch_id.0) { active_touch_points.swap_remove(index); self.remove_pointer_id_for_touch(event.touch_id.0); } else { warn!( "Received {:?} for a non-active touch point {}", event.event_type, event.touch_id.0 ); } }, } } /// The entry point for all key processing for web content fn handle_keyboard_event( &self, cx: &mut JSContext, keyboard_event: EmbedderKeyboardEvent, ) -> InputEventResult { let target = &self.target_for_events_following_focus(); let keyevent = KeyboardEvent::new_with_platform_keyboard_event( cx, &self.window, keyboard_event.event.state.event_type().into(), &keyboard_event.event, ); let event = keyevent.upcast::<Event>(); event.set_composed(true); event.fire(cx, target); let mut flags = event.flags(); if flags.contains(EventFlags::Canceled) { return flags.into(); } // 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 { // https://w3c.github.io/uievents/#keypress-event-order let keypress_event = KeyboardEvent::new_with_platform_keyboard_event( cx, &self.window, atom!("keypress"), &keyboard_event.event, ); keypress_event.upcast::<Event>().set_composed(true); let event = keypress_event.upcast::<Event>(); event.fire(cx, target); flags = event.flags(); } flags.into() } fn handle_ime_event(&self, cx: &mut JSContext, event: ImeEvent) -> InputEventResult { let document = self.window.Document(); let composition_event = match event { ImeEvent::Dismissed => { document.focus_handler().focus(cx, &FocusableArea::Viewport); return Default::default(); }, 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_area = document.focus_handler().focused_area(); let Some(focused_element) = focused_area.element() else { // Event is only dispatched if there is a focused element. return Default::default(); }; let cancelable = composition_event.state == keyboard_types::CompositionState::Start; let event = CompositionEvent::new( cx, &self.window, composition_event.state.event_type().into(), true, cancelable, Some(&self.window), 0, DOMString::from(composition_event.data), ); let event = event.upcast::<Event>(); event.fire(cx, focused_element.upcast()); event.flags().into() } fn handle_wheel_event( &self, cx: &mut JSContext, event: EmbedderWheelEvent, input_event: &ConstellationInputEvent, ) -> InputEventResult { // Ignore all incoming events without a hit test. let flags = HitTestFlags::empty(); let Some(hit_test_result) = self.window.hit_test_from_input_event(flags, input_event) else { return Default::default(); }; let Some(el) = hit_test_result .node .inclusive_ancestors(ShadowIncluding::Yes) .find_map(DomRoot::downcast::<Element>) else { return Default::default(); }; let node = el.upcast::<Node>(); debug!( "wheel: on {:?} at {:?}", node.debug_str(), hit_test_result.point_in_frame ); let event_type = "wheel".into(); let cancelable = EventCancelable::from( self.window .upcast::<EventTarget>() .has_non_passive_listener(&event_type) || node.inclusive_ancestors(ShadowIncluding::Yes) .any(|target| { target .upcast::<EventTarget>() .has_non_passive_listener(&event_type) }), ); // https://w3c.github.io/uievents/#event-wheelevents let dom_event = WheelEvent::new( cx, &self.window, event_type, EventBubbles::Bubbles, 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, ); let dom_event = dom_event.upcast::<Event>(); dom_event.set_trusted(true); dom_event.set_composed(true); dom_event.fire(cx, node.upcast()); dom_event.flags().into() } #[cfg(feature = "gamepad")] 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); }, }; } /// <https://www.w3.org/TR/gamepad/#dfn-gamepadconnected> #[cfg(feature = "gamepad")] 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, ) { // Step 1. Let document be the current global object's associated Document; otherwise null. let doc = self.window.Document(); // Step 2. If document is not null and is not allowed to use the "gamepad" permission, // then abort these steps. if !doc.allowed_to_use_feature(PermissionName::Gamepad) { return; } let trusted_window = Trusted::new(&*self.window); // Step 3. Queue a global task on the gamepad task source with the current global object // to perform the following steps: self.window .upcast::<GlobalScope>() .task_manager() .gamepad_task_source() .queue(task!(gamepad_connected: move |cx| { let window = trusted_window.root(); // Step 3.1. Let gamepad be a new Gamepad representing the gamepad. // Step 3.2. Let navigator be gamepad's relevant global object's Navigator object. let navigator = window.Navigator(cx); let selected_index = navigator.select_gamepad_index(); let gamepad = Gamepad::new( cx, &window, selected_index, name, "standard".into(), axis_bounds, button_bounds, supported_haptic_effects, false, ); // Step 3.3. Set navigator.[[gamepads]][gamepad.index] to gamepad. navigator.set_gamepad(selected_index as usize, Some(&gamepad)); // Step 3.4. If navigator.[[hasGamepadGesture]] is true: if navigator.has_gamepad_gesture() { // Step 3.4.1. Set gamepad.[[exposed]] to true. gamepad.set_exposed(true); // Step 3.4.2. If document is not null and is fully active, then fire an // event named gamepadconnected at gamepad's relevant global // object using GamepadEvent with its gamepad attribute // initialized to gamepad. if window.Document().is_fully_active() { gamepad.notify_event(cx, GamepadEventType::Connected); } } })); } /// <https://www.w3.org/TR/gamepad/#dfn-gamepaddisconnected> #[cfg(feature = "gamepad")] fn handle_gamepad_disconnect(&self, index: usize) { // Step 1. Let gamepad be the Gamepad representing the unavailable device. // Step 2. Queue a global task on the gamepad task source with // gamepad's relevant global object to perform the following steps: let trusted_window = Trusted::new(&*self.window); self.window .upcast::<GlobalScope>() .task_manager() .gamepad_task_source() .queue(task!(gamepad_disconnected: move |cx| { let window = trusted_window.root(); let navigator = window.Navigator(cx); if let Some(gamepad) = navigator.get_gamepad(index) { // Step 2.1. Set gamepad.[[connected]] to false. gamepad.update_connected(false); // Step 2.2. Let document be gamepad's relevant global object's // associated Document; otherwise null. // Step 2.3. If gamepad.[[exposed]] is true and document is not null // and is fully active, then fire an event named // gamepaddisconnected at gamepad's relevant global object // using GamepadEvent with its gamepad attribute // initialized to gamepad. if gamepad.exposed() && window.Document().is_fully_active() { gamepad.notify_event(cx, GamepadEventType::Disconnected); } } // Step 2.4. Let navigator be gamepad's relevant global object's // Navigator object. // Step 2.5. Set navigator.[[gamepads]][gamepad.index] to null. navigator.set_gamepad(index, None); // Step 2.6. While navigator.[[gamepads]] is not empty and the last item of // navigator.[[gamepads]] is null, remove the last item of navigator.[[gamepads]]. navigator.shrink_gamepads_list(); })); } /// <https://www.w3.org/TR/gamepad/#receiving-inputs> #[cfg(feature = "gamepad")] fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) { // Step 1. Let gamepad be the Gamepad object representing the device that received // new button or axis input values. let trusted_window = Trusted::new(&*self.window); // Step 2. Queue a global task on the gamepad task source with gamepad's // relevant global object to update gamepad state for gamepad. self.window .upcast::<GlobalScope>() .task_manager() .gamepad_task_source() .queue(task!(update_gamepad_state: move |cx| { let window = trusted_window.root(); let document = window.Document(); document.event_handler().update_gamepad_state(cx, index, update_type); })); } /// <https://w3c.github.io/gamepad/#dfn-update-gamepad-state> #[cfg(feature = "gamepad")] fn update_gamepad_state( &self, cx: &mut JSContext, gamepad_index: usize, update_type: GamepadUpdateType, ) { use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods; // Step 1. Let now be the current high resolution time given // gamepad's relevant global object. let now = *self.window.Performance(cx).Now(); // Step 6. Let navigator be gamepad's relevant global object's // Navigator object. let navigator = self.window.Navigator(cx); if let Some(gamepad) = navigator.get_gamepad(gamepad_index) { // Step 2. Set gamepad.[[timestamp]] to now. gamepad.update_timestamp(now); // Step 3. Run the steps to map and normalize axes for gamepad. // Step 4. Run the steps to map and normalize buttons for gamepad. match update_type { GamepadUpdateType::Axis(axis_index, value) => { gamepad.map_and_normalize_axes(axis_index, value); }, GamepadUpdateType::Button(button_index, value) => { gamepad.map_and_normalize_buttons(button_index, value); }, }; // TODO Step 5. Run the steps to record touches for gamepad. // Step 7. If navigator.[[hasGamepadGesture]] is false and // gamepad contains a gamepad user gesture: if !navigator.has_gamepad_gesture() && contains_user_gesture(update_type) { // Step 7.1. Set navigator.[[hasGamepadGesture]] to true. navigator.set_has_gamepad_gesture(true); // Step 7.2. For each connectedGamepad of navigator.[[gamepads]]: navigator .get_connected_gamepad() .iter() .for_each(|connected_gamepad| { // Step 7.2.1. Set connectedGamepad.[[exposed]] to true. connected_gamepad.set_exposed(true); // Step 7.2.2. Set connectedGamepad.[[timestamp]] to now. connected_gamepad.update_timestamp(now); // Step 7.2.3. Let document be gamepad's relevant global // object's associated Document; otherwise null. // Step 7.2.4. If document is not null and is fully active, // then queue a global task on the gamepad task // source to fire an event named gamepadconnected // at gamepad's relevant global object. let trusted_gamepad = Trusted::new(&**connected_gamepad); if self.window.Document().is_fully_active() { self.window .upcast::<GlobalScope>() .task_manager() .gamepad_task_source() .queue(task!(fire_gamepad_connected: move |cx| { let gamepad = trusted_gamepad.root(); gamepad.notify_event(cx, GamepadEventType::Connected); })); } }); } } } /// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions> pub(crate) fn handle_editing_action( &self, cx: &mut JSContext, element: Option<DomRoot<Element>>, action: EditingActionEvent, ) -> InputEventResult { 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 InputEventResult::empty(); } // Step 2 Fire a clipboard event let clipboard_event = self.fire_clipboard_event(cx, element.clone(), clipboard_event_type); // Step 3 If a script doesn't call preventDefault() // the event will be handled inside target's VirtualMethods::handle_event let event = clipboard_event.upcast::<Event>(); if !event.IsTrusted() { return event.flags().into(); } // Step 4 If the event was canceled, then if event.DefaultPrevented() { let event_type = event.Type(); match_domstring_ascii!(event_type, "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) = clipboard_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) = clipboard_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_clipboard_event(cx, element, ClipboardEventType::Change); }, // Step 4.1 Return false. // Note: This function deviates from the specification a bit by returning // the `InputEventResult` below. "paste" => (), _ => (), ) } // Step 5: Return true from the action. // In this case we are returning the `InputEventResult` instead of true or false. event.flags().into() } /// <https://www.w3.org/TR/clipboard-apis/#fire-a-clipboard-event> pub(crate) fn fire_clipboard_event( &self, cx: &mut JSContext, target: Option<DomRoot<Element>>, clipboard_event_type: ClipboardEventType, ) -> DomRoot<ClipboardEvent> { let clipboard_event = ClipboardEvent::new( cx, &self.window, None, clipboard_event_type.as_str().into(), EventBubbles::Bubbles, EventCancelable::Cancelable, None, ); // 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 target = target .map(DomRoot::upcast) .unwrap_or_else(|| self.target_for_events_following_focus()); // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70 // Step 7 match clipboard_event_type { ClipboardEventType::Copy | ClipboardEventType::Cut => { // Step 7.2.1 drag_data_store.set_mode(Mode::ReadWrite); }, ClipboardEventType::Paste => { let (callback, receiver) = GenericCallback::new_blocking().expect("Could not create callback"); self.window.send_to_embedder(EmbedderMsg::GetClipboardText( self.window.webview_id(), callback, )); 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); 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( cx, &self.window, Rc::new(RefCell::new(Some(drag_data_store))), ); // Step 8 clipboard_event.set_clipboard_data(Some(&clipboard_event_data)); // Step 9 let event = clipboard_event.upcast::<Event>(); event.set_trusted(trusted); // Step 10 Set event’s composed to true. event.set_composed(true); // Step 11 event.dispatch(cx, &target, false); DomRoot::from(clipboard_event) } /// <https://www.w3.org/TR/clipboard-apis/#write-content-to-the-clipboard> 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 a scroll event triggered by user interactions from the embedder. /// <https://drafts.csswg.org/cssom-view/#scrolling-events> #[expect(unsafe_code)] pub(crate) fn handle_embedder_scroll_event(&self, scrolled_node: ExternalScrollId) { // If it is a viewport scroll. let document = self.window.Document(); if scrolled_node.is_root() { document.handle_viewport_scroll_event(); } else { // Otherwise, check whether it is for a relevant element within the document. For a `::before` or `::after` // pseudo element we follow Gecko or Chromium's behavior to ensure that the event reaches the originating // node. let node_id = node_id_from_scroll_id(scrolled_node.0 as usize); let node = unsafe { node::from_untrusted_node_address(UntrustedNodeAddress::from_id(node_id)) }; let Some(element) = node .inclusive_ancestors(ShadowIncluding::Yes) .find_map(DomRoot::downcast::<Element>) else { return; }; element.handle_scroll_event(); } } /// <https://w3c.github.io/uievents/#keydown> /// /// > If the key is the Enter or (Space) key and the current focus is on a state-changing element, /// > the default action MUST be to dispatch a click event, and a DOMActivate event if that event /// > type is supported by the user agent. pub(crate) fn maybe_dispatch_simulated_click( &self, cx: &mut JSContext, node: &Node, event: &KeyboardEvent, ) -> bool { if event.key() != Key::Named(NamedKey::Enter) && event.original_code() != Some(Code::Space) { return false; } // Check whether this node is a state-changing element. Note that the specification doesn't // seem to have a good definition of what "state-changing" means, so we merely check to // see if the element is activatable here. if node .downcast::<Element>() .and_then(Element::as_maybe_activatable) .is_none() { return false; } node.fire_synthetic_pointer_event_not_trusted(cx, atom!("click")); true } pub(crate) fn run_default_keyboard_event_handler( &self, cx: &mut js::context::JSContext, node: &Node, event: &KeyboardEvent, ) { if event.upcast::<Event>().type_() != atom!("keydown") { return; } if self.maybe_dispatch_simulated_click(cx, node, event) { return; } if self.maybe_handle_accesskey(cx, event) { return; } let mut is_space = false; let scroll = match event.key() { Key::Named(NamedKey::ArrowDown) => KeyboardScroll::Down, Key::Named(NamedKey::ArrowLeft) => KeyboardScroll::Left, Key::Named(NamedKey::ArrowRight) => KeyboardScroll::Right, Key::Named(NamedKey::ArrowUp) => KeyboardScroll::Up, Key::Named(NamedKey::End) => KeyboardScroll::End, Key::Named(NamedKey::Home) => KeyboardScroll::Home, Key::Named(NamedKey::PageDown) => KeyboardScroll::PageDown, Key::Named(NamedKey::PageUp) => KeyboardScroll::PageUp, Key::Character(string) if &string == " " => { is_space = true; if event.modifiers().contains(Modifiers::SHIFT) { KeyboardScroll::PageUp } else { KeyboardScroll::PageDown } }, Key::Named(NamedKey::Tab) => { // From <https://w3c.github.io/uievents/#keydown>: // // > If the key is the Tab key, the default action MUST be to shift the document focus // > from the currently focused element (if any) to the new focused element, as // > described in Focus Event Types self.window .Document() .focus_handler() .sequential_focus_navigation_via_keyboard_event(cx, event); return; }, _ => return, }; if !event.modifiers().is_empty() && !is_space { return; } self.do_keyboard_scroll(cx, scroll); } pub(crate) fn do_keyboard_scroll(&self, cx: &mut JSContext, scroll: KeyboardScroll) { let scroll_axis = match scroll { KeyboardScroll::Left | KeyboardScroll::Right => ScrollingBoxAxis::X, _ => ScrollingBoxAxis::Y, }; let document = self.window.Document(); let mut scrolling_box = document .focus_handler() .focused_area() .element() .or(self.most_recently_clicked_element.get().as_deref()) .and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive)) .unwrap_or_else(|| { document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive) }); while !scrolling_box.can_keyboard_scroll_in_axis(cx.no_gc(), scroll_axis) { // Always fall back to trying to scroll the entire document. if scrolling_box.is_viewport() { break; } let parent = scrolling_box.parent().unwrap_or_else(|| { document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive) }); scrolling_box = parent; } let calculate_current_scroll_offset_and_delta = || { const LINE_HEIGHT: f32 = 76.0; const LINE_WIDTH: f32 = 76.0; let current_scroll_offset = scrolling_box.scroll_position(); ( current_scroll_offset, match scroll { KeyboardScroll::Home => Vector2D::new(0.0, -current_scroll_offset.y), KeyboardScroll::End => Vector2D::new( 0.0, -current_scroll_offset.y + scrolling_box.content_size().height - scrolling_box.size(cx.no_gc()).height, ), KeyboardScroll::PageDown => Vector2D::new( 0.0, scrolling_box.size(cx.no_gc()).height - 2.0 * LINE_HEIGHT, ), KeyboardScroll::PageUp => Vector2D::new( 0.0, 2.0 * LINE_HEIGHT - scrolling_box.size(cx.no_gc()).height, ), KeyboardScroll::Up => Vector2D::new(0.0, -LINE_HEIGHT), KeyboardScroll::Down => Vector2D::new(0.0, LINE_HEIGHT), KeyboardScroll::Left => Vector2D::new(-LINE_WIDTH, 0.0), KeyboardScroll::Right => Vector2D::new(LINE_WIDTH, 0.0), }, ) }; // If trying to scroll the viewport of this `Window` and this is the root `Document` // of the `WebView`, then send the srolling operation to the renderer, so that it // can properly pan any pinch zoom viewport. let parent_pipeline = self.window.parent_info(); if scrolling_box.is_viewport() && parent_pipeline.is_none() { let (_, delta) = calculate_current_scroll_offset_and_delta(); self.window .paint_api() .scroll_viewport_by_delta(self.window.webview_id(), delta); } // If this is the viewport and we cannot scroll, try to ask a parent viewport to scroll, // if we are inside an `<iframe>`. if !scrolling_box.can_keyboard_scroll_in_axis(cx.no_gc(), scroll_axis) { assert!(scrolling_box.is_viewport()); let window_proxy = document.window().window_proxy(); if let Some(iframe) = window_proxy.frame_element() { // When the `<iframe>` is local (in this ScriptThread), we can // synchronously chain up the keyboard scrolling event. let iframe_window = iframe.owner_window(); let mut realm = enter_auto_realm(cx, &*iframe_window); let cx = &mut realm; iframe_window .Document() .event_handler() .do_keyboard_scroll(cx, scroll); } else if let Some(parent_pipeline) = parent_pipeline { // Otherwise, if we have a parent (presumably from a different origin) // asynchronously ask the Constellation to forward the event to the parent // pipeline, if we have one. document.window().send_to_constellation( ScriptToConstellationMessage::ForwardKeyboardScroll(parent_pipeline, scroll), ); }; return; } let (current_scroll_offset, delta) = calculate_current_scroll_offset_and_delta(); scrolling_box.scroll_to(cx, delta + current_scroll_offset, ScrollBehavior::Auto); } /// Get or create a pointer ID for the given touch identifier. /// Returns the pointer ID to use for this touch. fn get_or_create_pointer_id_for_touch(&self, touch_id: i32) -> i32 { let mut active_pointer_ids = self.active_pointer_ids.borrow_mut(); if let Some(&pointer_id) = active_pointer_ids.get(&touch_id) { return pointer_id; } let pointer_id = self.next_touch_pointer_id.get(); active_pointer_ids.insert(touch_id, pointer_id); self.next_touch_pointer_id.set(pointer_id + 1); pointer_id } /// Remove the pointer ID mapping for the given touch identifier. fn remove_pointer_id_for_touch(&self, touch_id: i32) { self.active_pointer_ids.borrow_mut().remove(&touch_id); } /// Check if this is the primary pointer (for touch events). /// The first touch to make contact is the primary pointer. fn is_primary_pointer(&self, pointer_id: i32) -> bool { // For touch, the primary pointer is the one with the smallest pointer ID // that is still active. self.active_pointer_ids .borrow() .values() .min() .is_some_and(|primary_pointer| *primary_pointer == pointer_id) } /// Fire pointerenter events hierarchically from topmost ancestor to target element. /// Fire pointerleave events hierarchically from target element to topmost ancestor. /// Used for touch devices that don't support hover. #[allow(clippy::too_many_arguments)] fn fire_pointer_event_for_touch( &self, cx: &mut js::context::JSContext, target_element: &Element, touch: &Touch, pointer_id: i32, event_name: &str, is_primary: bool, pointer_type: &str, input_event: &ConstellationInputEvent, hit_test_result: &HitTestResult, ) { let mut targets: Vec<_> = target_element .upcast::<Node>() .inclusive_ancestors_in_flat_tree() .collect(); // Reverse to dispatch from topmost ancestor to target if event_name == "pointerenter" { targets.reverse(); } for target in targets { let pointer_event = touch.to_pointer_event( cx, &self.window, event_name, pointer_id, is_primary, pointer_type, input_event.active_keyboard_modifiers, false, Some(hit_test_result.point_in_node), ); pointer_event.upcast::<Event>().fire(cx, target.upcast()); } } pub(crate) fn has_assigned_access_key(&self, element: &HTMLElement) -> bool { self.access_key_handlers .borrow() .values() .any(|value| &**value == element) } pub(crate) fn unassign_access_key(&self, element: &HTMLElement) { self.access_key_handlers .borrow_mut() .retain(|_, value| &**value != element) } pub(crate) fn assign_access_key(&self, element: &HTMLElement, code: Code) { let mut access_key_handlers = self.access_key_handlers.borrow_mut(); // If an element is already assigned this access key, ignore the request. access_key_handlers .entry(code.into()) .or_insert(Dom::from_ref(element)); } fn maybe_handle_accesskey( &self, cx: &mut js::context::JSContext, event: &KeyboardEvent, ) -> bool { #[cfg(target_os = "macos")] let access_key_modifiers = Modifiers::CONTROL | Modifiers::ALT; #[cfg(not(target_os = "macos"))] let access_key_modifiers = Modifiers::SHIFT | Modifiers::ALT; if event.modifiers() != access_key_modifiers { return false; } let Ok(code) = Code::from_str(&event.Code().str()) else { return false; }; let Some(html_element) = self .access_key_handlers .borrow() .get(&code.into()) .map(|html_element| html_element.as_rooted()) else { return false; }; // From <https://html.spec.whatwg.org/multipage/#the-accesskey-attribute>: // > When the user presses the key combination corresponding to the assigned access key for // > an element, if the element defines a command, the command's Hidden State facet is false // > (visible), the command's Disabled State facet is also false (enabled), the element is in // > a document that has a non-null browsing context, and neither the element nor any of its // > ancestors has a hidden attribute specified, then the user agent must trigger the Action // > of the command. let Ok(command) = InteractiveElementCommand::try_from(&*html_element) else { return false; }; if command.disabled() || command.hidden() { return false; } let node = html_element.upcast::<Node>(); if !node.is_connected() { return false; } for node in node.inclusive_ancestors_unrooted(cx.no_gc(), ShadowIncluding::Yes) { if node .downcast::<HTMLElement>() .is_some_and(|html_element| html_element.Hidden()) { return false; } } // This behavior is unspecified, but all browsers do this. When activating the element it is // focused and scrolled into view. self.focus_and_scroll_to_element_for_key_event(cx, html_element.upcast()); command.perform_action(cx); true } pub(crate) fn focus_and_scroll_to_element_for_key_event( &self, cx: &mut JSContext, element: &Element, ) { element.upcast::<Node>().run_the_focusing_steps(cx, None); let scroll_axis = ScrollAxisState { position: ScrollLogicalPosition::Center, requirement: ScrollRequirement::IfNotVisible, }; element.scroll_into_view_with_options( cx, ScrollBehavior::Auto, scroll_axis, scroll_axis, None, None, ); } /// Check if a pointer ID corresponds to an active pointer. /// <https://w3c.github.io/pointerevents/#dfn-active-pointer> pub(crate) fn is_active_pointer(&self, pointer_id: i32) -> bool { if pointer_id == PointerId::Mouse as i32 { // Mouse is active when buttons are down self.mouse_buttons_down.get() > 0 } else { // Touch pointers are tracked in active_pointer_ids self.active_pointer_ids .borrow() .values() .any(|&id| id == pointer_id) } } /// Set the pending pointer capture target override for a pointer. /// <https://w3c.github.io/pointerevents/#setting-pointer-capture> pub(crate) fn set_pending_pointer_capture(&self, pointer_id: i32, element: &Element) { self.pending_pointer_capture .borrow_mut() .insert(pointer_id, Dom::from_ref(element)); } /// Clear the pending pointer capture target override for a pointer. /// <https://w3c.github.io/pointerevents/#releasing-pointer-capture> pub(crate) fn clear_pending_pointer_capture(&self, pointer_id: i32) { self.pending_pointer_capture .borrow_mut() .remove(&pointer_id); } /// Check if an element has pointer capture for a given pointer ID. /// <https://w3c.github.io/pointerevents/#dom-element-haspointercapture> pub(crate) fn has_pointer_capture(&self, pointer_id: i32, element: &Element) -> bool { self.pending_pointer_capture .borrow() .get(&pointer_id) .is_some_and(|el| &**el == element) } /// Get the current pointer capture target for event dispatch. /// Returns the capture target if set and connected, otherwise None. /// This returns the actual/current target (pointer_capture_target), /// not the pending target that will be applied after process_pending. fn get_pointer_capture_target(&self, pointer_id: i32) -> Option<DomRoot<Element>> { self.pointer_capture_target .borrow() .get(&pointer_id) .map(|el| DomRoot::from_ref(&**el)) .filter(|el| el.upcast::<Node>().is_connected()) } /// Check if the capture target is disconnected and release it if so. /// This fires lostpointercapture at the document per spec. /// Must be called before firing any pointer event. /// Returns true if a disconnected capture was released. /// <https://w3c.github.io/pointerevents/#process-pending-pointer-capture> fn release_disconnected_pointer_capture( &self, cx: &mut JSContext, pointer_id: i32, pointer_type: &str, is_primary: bool, ) -> bool { let capture_target = self .pointer_capture_target .borrow() .get(&pointer_id) .map(|el| DomRoot::from_ref(&**el)); if let Some(capture_element) = capture_target && !capture_element.upcast::<Node>().is_connected() { // Fire lostpointercapture at the document, not the disconnected element. let document = self.window.Document(); self.fire_pointer_capture_event_at_target( cx, "lostpointercapture", pointer_id, pointer_type, is_primary, document.upcast::<EventTarget>(), ); // Clear both pending and current capture self.pending_pointer_capture .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); self.pointer_capture_target .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); return true; } false } /// Fire a gotpointercapture or lostpointercapture event at an Element. /// <https://w3c.github.io/pointerevents/#the-gotpointercapture-and-lostpointercapture-events> fn fire_pointer_capture_event( &self, cx: &mut JSContext, event_type: &str, pointer_id: i32, pointer_type: &str, is_primary: bool, target: &Element, ) { self.fire_pointer_capture_event_at_target( cx, event_type, pointer_id, pointer_type, is_primary, target.upcast::<EventTarget>(), ); } /// Fire a pointer capture event at a specific EventTarget (e.g., the document). fn fire_pointer_capture_event_at_target( &self, cx: &mut JSContext, event_type: &str, pointer_id: i32, pointer_type: &str, is_primary: bool, target: &EventTarget, ) { let pointer_event = PointerEvent::new( cx, &self.window, event_type.into(), EventBubbles::Bubbles, EventCancelable::NotCancelable, Some(&self.window), 0, Point2D::new(0, 0), Point2D::new(0, 0), Point2D::new(0, 0), Modifiers::empty(), 0, 0, None, None, pointer_id, 1, 1, 0.0, 0.0, 0, 0, 0, PI / 2.0, 0.0, DOMString::from(pointer_type), is_primary, vec![], vec![], ); pointer_event.upcast::<Event>().set_composed(true); pointer_event.upcast::<Event>().fire(cx, target); } /// Fire a single boundary `PointerEvent` (`pointerout`/`pointerleave`/ /// `pointerover`/`pointerenter`) at `target`, with `related_target` set to /// the element being entered from or left to. #[expect(clippy::too_many_arguments)] fn fire_pointer_boundary_event( &self, cx: &mut JSContext, event_type: &str, bubbles: EventBubbles, pointer_id: i32, pointer_type: &str, is_primary: bool, target: &Element, related_target: Option<&Element>, ) { let pointer_event = PointerEvent::new( cx, &self.window, event_type.into(), bubbles, EventCancelable::NotCancelable, Some(&self.window), 0, Point2D::new(0, 0), Point2D::new(0, 0), Point2D::new(0, 0), Modifiers::empty(), -1, // button: -1 for boundary events self.mouse_buttons_down.get() as u16, related_target.map(|el| el.upcast::<EventTarget>()), None, pointer_id, 1, 1, 0.0, 0.0, 0, 0, 0, PI / 2.0, 0.0, DOMString::from(pointer_type), is_primary, vec![], vec![], ); pointer_event.upcast::<Event>().set_composed(true); pointer_event.upcast::<Event>().fire(cx, target.upcast()); } /// Fire the pointer boundary events that accompany a pointer-capture /// transition from `old_target` to `new_target` for hoverable pointers. /// Per spec, only fired for pointer types that support hover (mouse, pen /// with hover); touch is skipped. fn fire_pointer_capture_boundary_transition( &self, cx: &mut JSContext, pointer_id: i32, pointer_type: &str, is_primary: bool, old_target: &Element, new_target: &Element, ) { if pointer_type != "mouse" { return; } if old_target == new_target { return; } self.fire_pointer_boundary_event( cx, "pointerout", EventBubbles::Bubbles, pointer_id, pointer_type, is_primary, old_target, Some(new_target), ); self.fire_pointer_boundary_event( cx, "pointerleave", EventBubbles::DoesNotBubble, pointer_id, pointer_type, is_primary, old_target, Some(new_target), ); self.fire_pointer_boundary_event( cx, "pointerover", EventBubbles::Bubbles, pointer_id, pointer_type, is_primary, new_target, Some(old_target), ); self.fire_pointer_boundary_event( cx, "pointerenter", EventBubbles::DoesNotBubble, pointer_id, pointer_type, is_primary, new_target, Some(old_target), ); } /// Implicitly release pointer capture when pointer is lifted or canceled. /// <https://w3c.github.io/pointerevents/#implicit-release-of-pointer-capture> fn implicit_release_pointer_capture( &self, cx: &mut JSContext, pointer_id: i32, pointer_type: &str, is_primary: bool, ) { let capture_element = self .pointer_capture_target .borrow() .get(&pointer_id) .map(|el| DomRoot::from_ref(&**el)); if let Some(capture_element) = capture_element { if capture_element.upcast::<Node>().is_connected() { self.fire_pointer_capture_event( cx, "lostpointercapture", pointer_id, pointer_type, is_primary, &capture_element, ); // Boundary events: transition hover from released capture // target back to the actual hover target. if let Some(hover_target) = self.current_hover_target.get() { self.fire_pointer_capture_boundary_transition( cx, pointer_id, pointer_type, is_primary, &capture_element, &hover_target, ); } } else { let document = self.window.Document(); self.fire_pointer_capture_event_at_target( cx, "lostpointercapture", pointer_id, pointer_type, is_primary, document.upcast::<EventTarget>(), ); } } self.pending_pointer_capture .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); self.pointer_capture_target .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); } /// Process pending pointer capture before dispatching a pointer event. /// Fires gotpointercapture/lostpointercapture as needed. /// <https://w3c.github.io/pointerevents/#process-pending-pointer-capture> fn process_pending_pointer_capture( &self, cx: &mut JSContext, pointer_id: i32, pointer_type: &str, is_primary: bool, ) { let pending = self .pending_pointer_capture .borrow() .get(&pointer_id) .map(|el| DomRoot::from_ref(&**el)); let current = self .pointer_capture_target .borrow() .get(&pointer_id) .map(|el| DomRoot::from_ref(&**el)); // Disconnected capture targets are handled by release_disconnected_pointer_capture // before any pointer event fires; by the time we get here they have been released. let pending_connected = pending .as_ref() .is_some_and(|el| el.upcast::<Node>().is_connected()); // If the pointer is no longer active (e.g., last button just released), we should // not fire gotpointercapture for new captures. let pointer_is_active = self.is_active_pointer(pointer_id); match (&pending, ¤t) { (Some(pending_el), None) if pending_connected => { if pointer_is_active { // Boundary events: transition hover from current hover target // to the new capture target, before firing gotpointercapture. if let Some(hover_target) = self.current_hover_target.get() { self.fire_pointer_capture_boundary_transition( cx, pointer_id, pointer_type, is_primary, &hover_target, pending_el, ); } self.fire_pointer_capture_event( cx, "gotpointercapture", pointer_id, pointer_type, is_primary, pending_el, ); self.pointer_capture_target .safe_borrow_mut(cx.no_gc()) .insert(pointer_id, Dom::from_ref(pending_el)); } else { self.pending_pointer_capture .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); } }, (Some(pending_el), Some(current_el)) if pending_connected && pending_el != current_el => { self.fire_pointer_capture_event( cx, "lostpointercapture", pointer_id, pointer_type, is_primary, current_el, ); if pointer_is_active { self.fire_pointer_capture_boundary_transition( cx, pointer_id, pointer_type, is_primary, current_el, pending_el, ); self.fire_pointer_capture_event( cx, "gotpointercapture", pointer_id, pointer_type, is_primary, pending_el, ); self.pointer_capture_target .safe_borrow_mut(cx.no_gc()) .insert(pointer_id, Dom::from_ref(pending_el)); } else { self.pending_pointer_capture .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); self.pointer_capture_target .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); } }, (None, Some(current_el)) | (Some(_), Some(current_el)) if !pending_connected => { self.fire_pointer_capture_event( cx, "lostpointercapture", pointer_id, pointer_type, is_primary, current_el, ); // Boundary events: transition hover from released capture // target back to the actual hover target. if let Some(hover_target) = self.current_hover_target.get() { self.fire_pointer_capture_boundary_transition( cx, pointer_id, pointer_type, is_primary, current_el, &hover_target, ); } self.pointer_capture_target .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); if !pending_connected { self.pending_pointer_capture .safe_borrow_mut(cx.no_gc()) .remove(&pointer_id); } }, _ => {}, } } } pub(crate) fn character_to_code(character: char) -> Option<Code> { Some(match character.to_ascii_lowercase() { '`' => Code::Backquote, '\\' => Code::Backslash, '[' | '{' => Code::BracketLeft, ']' | '}' => Code::BracketRight, ',' | '<' => Code::Comma, '0' => Code::Digit0, '1' => Code::Digit1, '2' => Code::Digit2, '3' => Code::Digit3, '4' => Code::Digit4, '5' => Code::Digit5, '6' => Code::Digit6, '7' => Code::Digit7, '8' => Code::Digit8, '9' => Code::Digit9, '=' => Code::Equal, 'a' => Code::KeyA, 'b' => Code::KeyB, 'c' => Code::KeyC, 'd' => Code::KeyD, 'e' => Code::KeyE, 'f' => Code::KeyF, 'g' => Code::KeyG, 'h' => Code::KeyH, 'i' => Code::KeyI, 'j' => Code::KeyJ, 'k' => Code::KeyK, 'l' => Code::KeyL, 'm' => Code::KeyM, 'n' => Code::KeyN, 'o' => Code::KeyO, 'p' => Code::KeyP, 'q' => Code::KeyQ, 'r' => Code::KeyR, 's' => Code::KeyS, 't' => Code::KeyT, 'u' => Code::KeyU, 'v' => Code::KeyV, 'w' => Code::KeyW, 'x' => Code::KeyX, 'y' => Code::KeyY, 'z' => Code::KeyZ, '-' => Code::Minus, '.' => Code::Period, '\'' | '"' => Code::Quote, ';' => Code::Semicolon, '/' => Code::Slash, ' ' => Code::Space, _ => return None, }) } impl Element { /// Find the first inclusive ancestor of this [`Element`] that is not in a UA shadow root. fn inclusive_ancestor_element_in_non_ua_shadow_root(&self) -> DomRoot<Element> { if !self.upcast::<Node>().is_in_ua_widget() { return DomRoot::from_ref(self); } let Some(shadow_root) = self.containing_shadow_root() else { return DomRoot::from_ref(self); }; shadow_root .Host() .inclusive_ancestor_element_in_non_ua_shadow_root() } }