Skip to main content

script/dom/document/
document_event_handler.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::array::from_ref;
6use std::cell::{Cell, RefCell};
7use std::f64::consts::PI;
8use std::mem;
9use std::rc::Rc;
10use std::str::FromStr;
11use std::time::{Duration, Instant};
12
13use embedder_traits::{
14    Cursor, EditingActionEvent, EmbedderMsg, ImeEvent, InputEvent, InputEventId, InputEventOutcome,
15    InputEventResult, KeyboardEvent as EmbedderKeyboardEvent, MouseButton, MouseButtonAction,
16    MouseButtonEvent, MouseLeftViewportEvent, TouchEvent as EmbedderTouchEvent, TouchEventType,
17    TouchId, TouchPointerType, UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent,
18};
19#[cfg(feature = "gamepad")]
20use embedder_traits::{
21    GamepadEvent as EmbedderGamepadEvent, GamepadSupportedHapticEffects, GamepadUpdateType,
22};
23use euclid::{Point2D, Vector2D};
24use js::context::JSContext;
25use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey};
26use layout_api::{ScrollContainerQueryFlags, node_id_from_scroll_id};
27use rustc_hash::FxHashMap;
28use script_bindings::cell::DomRefCell;
29use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
30use script_bindings::codegen::GenericBindings::ElementBinding::ScrollLogicalPosition;
31use script_bindings::codegen::GenericBindings::EventBinding::EventMethods;
32use script_bindings::codegen::GenericBindings::HTMLElementBinding::HTMLElementMethods;
33use script_bindings::codegen::GenericBindings::HTMLLabelElementBinding::HTMLLabelElementMethods;
34use script_bindings::codegen::GenericBindings::KeyboardEventBinding::KeyboardEventMethods;
35use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
36use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods;
37use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods};
38use script_bindings::inheritance::Castable;
39use script_bindings::match_domstring_ascii;
40use script_bindings::num::Finite;
41use script_bindings::root::{Dom, DomRoot, DomSlice};
42use script_bindings::script_runtime::CanGc;
43use script_bindings::str::DOMString;
44use script_traits::ConstellationInputEvent;
45use servo_base::generic_channel::GenericCallback;
46use servo_config::pref;
47use servo_constellation_traits::{KeyboardScroll, ScriptToConstellationMessage};
48use style::Atom;
49use style_traits::CSSPixel;
50use webrender_api::ExternalScrollId;
51
52#[cfg(feature = "gamepad")]
53use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionName;
54use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
55use crate::dom::bindings::refcounted::Trusted;
56use crate::dom::bindings::root::MutNullableDom;
57use crate::dom::bindings::trace::NoTrace;
58use crate::dom::clipboardevent::ClipboardEventType;
59use crate::dom::document::FireMouseEventType;
60use crate::dom::document::focus::FocusableArea;
61use crate::dom::event::{EventBubbles, EventCancelable, EventComposed, EventFlags};
62#[cfg(feature = "gamepad")]
63use crate::dom::gamepad::gamepad::{Gamepad, contains_user_gesture};
64#[cfg(feature = "gamepad")]
65use crate::dom::gamepad::gamepadevent::GamepadEventType;
66use crate::dom::inputevent::HitTestResult;
67use crate::dom::interactive_element_command::InteractiveElementCommand;
68use crate::dom::iterators::ShadowIncluding;
69use crate::dom::keyboardevent::KeyboardEvent;
70use crate::dom::node::{self, Node, NodeTraits};
71use crate::dom::pointerevent::{PointerEvent, PointerId};
72use crate::dom::scrolling_box::{ScrollAxisState, ScrollRequirement, ScrollingBoxAxis};
73use crate::dom::types::{
74    ClipboardEvent, CompositionEvent, DataTransfer, Element, Event, EventTarget, GlobalScope,
75    HTMLAnchorElement, HTMLElement, HTMLLabelElement, MouseEvent, Touch, TouchEvent, TouchList,
76    WheelEvent, Window,
77};
78use crate::drag_data_store::{DragDataStore, Kind, Mode};
79use crate::realms::{enter_auto_realm, enter_realm};
80
81/// A data structure used for tracking the current click count. This can be
82/// reset to 0 if a mouse button event happens at a sufficient distance or time
83/// from the previous one.
84///
85/// From <https://w3c.github.io/uievents/#current-click-count>:
86/// > Implementations MUST maintain the current click count when generating mouse
87/// > events. This MUST be a non-negative integer indicating the number of consecutive
88/// > clicks of a pointing device button within a specific time. The delay after which
89/// > the count resets is specific to the environment configuration.
90#[derive(Default, JSTraceable, MallocSizeOf)]
91struct ClickCountingInfo {
92    time: Option<Instant>,
93    #[no_trace]
94    point: Option<Point2D<f32, CSSPixel>>,
95    #[no_trace]
96    button: Option<MouseButton>,
97    count: usize,
98}
99
100impl ClickCountingInfo {
101    fn reset_click_count_if_necessary(
102        &mut self,
103        button: MouseButton,
104        point_in_frame: Point2D<f32, CSSPixel>,
105    ) {
106        let (Some(previous_button), Some(previous_point), Some(previous_time)) =
107            (self.button, self.point, self.time)
108        else {
109            assert_eq!(self.count, 0);
110            return;
111        };
112
113        let double_click_timeout =
114            Duration::from_millis(pref!(dom_document_dblclick_timeout) as u64);
115        let double_click_distance_threshold = pref!(dom_document_dblclick_dist) as u64;
116
117        // Calculate distance between this click and the previous click.
118        let line = point_in_frame - previous_point;
119        let distance = (line.dot(line) as f64).sqrt();
120        if previous_button != button ||
121            Instant::now().duration_since(previous_time) > double_click_timeout ||
122            distance > double_click_distance_threshold as f64
123        {
124            self.count = 0;
125            self.time = None;
126            self.point = None;
127        }
128    }
129
130    fn increment_click_count(
131        &mut self,
132        button: MouseButton,
133        point: Point2D<f32, CSSPixel>,
134    ) -> usize {
135        self.time = Some(Instant::now());
136        self.point = Some(point);
137        self.button = Some(button);
138        self.count += 1;
139        self.count
140    }
141}
142
143/// The [`DocumentEventHandler`] is a structure responsible for handling input events for
144/// the [`crate::Document`] and storing data related to event handling. It exists to
145/// decrease the size of the [`crate::Document`] structure.
146#[derive(JSTraceable, MallocSizeOf)]
147#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
148pub(crate) struct DocumentEventHandler {
149    /// The [`Window`] element for this [`DocumentEventHandler`].
150    window: Dom<Window>,
151    /// Pending input events, to be handled at the next rendering opportunity.
152    #[no_trace]
153    #[ignore_malloc_size_of = "InputEvent contains data from outside crates"]
154    pending_input_events: DomRefCell<Vec<ConstellationInputEvent>>,
155    /// The index of the last mouse move event in the pending input events queue.
156    mouse_move_event_index: DomRefCell<Option<usize>>,
157    /// The [`InputEventId`]s of mousemove events that have been coalesced.
158    #[no_trace]
159    #[ignore_malloc_size_of = "InputEventId contains data from outside crates"]
160    coalesced_mouse_move_event_ids: DomRefCell<Vec<InputEventId>>,
161    /// The index of the last wheel event in the pending input events queue.
162    /// This is non-standard behaviour.
163    /// According to <https://www.w3.org/TR/pointerevents/#dfn-coalesced-events>,
164    /// we should only coalesce `pointermove` events.
165    wheel_event_index: DomRefCell<Option<usize>>,
166    /// The [`InputEventId`]s of wheel events that have been coalesced.
167    #[no_trace]
168    #[ignore_malloc_size_of = "InputEventId contains data from outside crates"]
169    coalesced_wheel_event_ids: DomRefCell<Vec<InputEventId>>,
170    /// <https://w3c.github.io/uievents/#event-type-dblclick>
171    click_counting_info: DomRefCell<ClickCountingInfo>,
172    #[no_trace]
173    last_mouse_button_down_point: Cell<Option<Point2D<f32, CSSPixel>>>,
174    /// The number of mouse buttons currently being pressed. This is used to ensure
175    /// that `pointerup` and `pointerdown` events are only sent when transitioning from
176    /// having no mouse buttons pressed to having any and vice-versa.
177    mouse_buttons_down: Cell<u32>,
178    /// The element that is currently hovered by the cursor.
179    current_hover_target: MutNullableDom<Element>,
180    /// The element that was most recently activated during a mouse button press or touch
181    /// event.
182    current_active_element: MutNullableDom<Element>,
183    /// The element that was most recently clicked.
184    most_recently_clicked_element: MutNullableDom<Element>,
185    /// The most recent mouse movement point, used for processing `mouseleave` events.
186    #[no_trace]
187    most_recent_mousemove_point: Cell<Option<Point2D<f32, CSSPixel>>>,
188    /// The currently set [`Cursor`] or `None` if the `Document` isn't being hovered
189    /// by the cursor.
190    #[no_trace]
191    current_cursor: Cell<Option<Cursor>>,
192    /// <http://w3c.github.io/touch-events/#dfn-active-touch-point>
193    active_touch_points: DomRefCell<Vec<Dom<Touch>>>,
194    /// The active keyboard modifiers for the WebView. This is updated when receiving any input event.
195    #[no_trace]
196    active_keyboard_modifiers: Cell<Modifiers>,
197    /// Map from touch identifier to pointer ID for active touch points
198    active_pointer_ids: DomRefCell<FxHashMap<i32, i32>>,
199    /// Counter for generating unique pointer IDs for touch inputs
200    next_touch_pointer_id: Cell<i32>,
201    /// A map holding information about currently registered access key handlers.
202    access_key_handlers: DomRefCell<FxHashMap<NoTrace<Code>, Dom<HTMLElement>>>,
203    /// Map from pointer ID to pending pointer capture target override element.
204    /// This is set by setPointerCapture and cleared by releasePointerCapture.
205    /// <https://w3c.github.io/pointerevents/#pointer-capture>
206    pending_pointer_capture: DomRefCell<FxHashMap<i32, Dom<Element>>>,
207    /// Map from pointer ID to the actual/current pointer capture target.
208    /// Updated during process_pending_pointer_capture when events are dispatched.
209    pointer_capture_target: DomRefCell<FxHashMap<i32, Dom<Element>>>,
210}
211
212impl DocumentEventHandler {
213    pub(crate) fn new(window: &Window) -> Self {
214        Self {
215            window: Dom::from_ref(window),
216            pending_input_events: Default::default(),
217            mouse_move_event_index: Default::default(),
218            coalesced_mouse_move_event_ids: Default::default(),
219            wheel_event_index: Default::default(),
220            coalesced_wheel_event_ids: Default::default(),
221            click_counting_info: Default::default(),
222            last_mouse_button_down_point: Default::default(),
223            mouse_buttons_down: Cell::new(0),
224            current_hover_target: Default::default(),
225            current_active_element: Default::default(),
226            most_recently_clicked_element: Default::default(),
227            most_recent_mousemove_point: Default::default(),
228            current_cursor: Default::default(),
229            active_touch_points: Default::default(),
230            active_keyboard_modifiers: Default::default(),
231            active_pointer_ids: Default::default(),
232            next_touch_pointer_id: Cell::new(1),
233            access_key_handlers: Default::default(),
234            pending_pointer_capture: Default::default(),
235            pointer_capture_target: Default::default(),
236        }
237    }
238
239    /// Note a pending input event, to be processed at the next `update_the_rendering` task.
240    pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) {
241        let mut pending_input_events = self.pending_input_events.borrow_mut();
242        if matches!(event.event.event, InputEvent::MouseMove(..)) {
243            // First try to replace any existing mouse move event.
244            if let Some(mouse_move_event) = self
245                .mouse_move_event_index
246                .borrow()
247                .and_then(|index| pending_input_events.get_mut(index))
248            {
249                self.coalesced_mouse_move_event_ids
250                    .borrow_mut()
251                    .push(mouse_move_event.event.id);
252                *mouse_move_event = event;
253                return;
254            }
255
256            *self.mouse_move_event_index.borrow_mut() = Some(pending_input_events.len());
257        }
258
259        if let InputEvent::Wheel(ref new_wheel_event) = event.event.event {
260            // Coalesce with any existing pending wheel event by summing deltas.
261            if let Some(existing_constellation_wheel_event) = self
262                .wheel_event_index
263                .borrow()
264                .and_then(|index| pending_input_events.get_mut(index)) &&
265                let InputEvent::Wheel(ref mut existing_wheel_event) =
266                    existing_constellation_wheel_event.event.event &&
267                existing_wheel_event.delta.mode == new_wheel_event.delta.mode
268            {
269                self.coalesced_wheel_event_ids
270                    .borrow_mut()
271                    .push(existing_constellation_wheel_event.event.id);
272                existing_wheel_event.delta.x += new_wheel_event.delta.x;
273                existing_wheel_event.delta.y += new_wheel_event.delta.y;
274                existing_wheel_event.delta.z += new_wheel_event.delta.z;
275                existing_wheel_event.point = new_wheel_event.point;
276                existing_constellation_wheel_event.event.id = event.event.id;
277                return;
278            }
279
280            *self.wheel_event_index.borrow_mut() = Some(pending_input_events.len());
281        }
282
283        pending_input_events.push(event);
284    }
285
286    /// Whether or not this [`Document`] has any pending input events to be processed during
287    /// "update the rendering."
288    pub(crate) fn has_pending_input_events(&self) -> bool {
289        !self.pending_input_events.borrow().is_empty()
290    }
291
292    pub(crate) fn alternate_action_keyboard_modifier_active(&self) -> bool {
293        #[cfg(target_os = "macos")]
294        {
295            self.active_keyboard_modifiers
296                .get()
297                .contains(Modifiers::META)
298        }
299        #[cfg(not(target_os = "macos"))]
300        {
301            self.active_keyboard_modifiers
302                .get()
303                .contains(Modifiers::CONTROL)
304        }
305    }
306
307    pub(crate) fn handle_pending_input_events(&self, cx: &mut JSContext) {
308        debug_assert!(
309            !self.pending_input_events.borrow().is_empty(),
310            "handle_pending_input_events called with no events"
311        );
312        let _realm = enter_realm(&*self.window);
313
314        // Reset the mouse and wheel event indices.
315        *self.mouse_move_event_index.borrow_mut() = None;
316        *self.wheel_event_index.borrow_mut() = None;
317        let pending_input_events = mem::take(&mut *self.pending_input_events.borrow_mut());
318        let mut coalesced_mouse_move_event_ids =
319            mem::take(&mut *self.coalesced_mouse_move_event_ids.borrow_mut());
320        let mut coalesced_wheel_event_ids =
321            mem::take(&mut *self.coalesced_wheel_event_ids.borrow_mut());
322
323        let mut input_event_outcomes = Vec::with_capacity(
324            pending_input_events.len() +
325                coalesced_mouse_move_event_ids.len() +
326                coalesced_wheel_event_ids.len(),
327        );
328        // TODO: For some of these we still aren't properly calculating whether or not
329        // the event was handled or if `preventDefault()` was called on it. Each of
330        // these cases needs to be examined and some of them either fire more than one
331        // event or fire events later. We have to make a good decision about what to
332        // return to the embedder when that happens.
333        for event in pending_input_events {
334            self.active_keyboard_modifiers
335                .set(event.active_keyboard_modifiers);
336            let result = match event.event.event {
337                InputEvent::MouseButton(mouse_button_event) => {
338                    self.handle_native_mouse_button_event(cx, mouse_button_event, &event);
339                    InputEventResult::default()
340                },
341                InputEvent::MouseMove(_) => {
342                    self.handle_native_mouse_move_event(cx, &event);
343                    input_event_outcomes.extend(
344                        mem::take(&mut coalesced_mouse_move_event_ids)
345                            .into_iter()
346                            .map(|id| InputEventOutcome {
347                                id,
348                                result: InputEventResult::default(),
349                            }),
350                    );
351                    InputEventResult::default()
352                },
353                InputEvent::MouseLeftViewport(mouse_leave_event) => {
354                    self.handle_mouse_left_viewport_event(cx, &event, &mouse_leave_event);
355                    InputEventResult::default()
356                },
357                InputEvent::Touch(touch_event) => self.handle_touch_event(cx, touch_event, &event),
358                InputEvent::Wheel(wheel_event) => {
359                    let result = self.handle_wheel_event(cx, wheel_event, &event);
360                    input_event_outcomes.extend(
361                        mem::take(&mut coalesced_wheel_event_ids)
362                            .into_iter()
363                            .map(|id| InputEventOutcome { id, result }),
364                    );
365                    result
366                },
367                InputEvent::Keyboard(keyboard_event) => {
368                    self.handle_keyboard_event(cx, keyboard_event)
369                },
370                InputEvent::Ime(ime_event) => self.handle_ime_event(cx, ime_event),
371                #[cfg(feature = "gamepad")]
372                InputEvent::Gamepad(gamepad_event) => {
373                    self.handle_gamepad_event(gamepad_event);
374                    InputEventResult::default()
375                },
376                InputEvent::EditingAction(editing_action_event) => {
377                    self.handle_editing_action(cx, None, editing_action_event)
378                },
379            };
380
381            input_event_outcomes.push(InputEventOutcome {
382                id: event.event.id,
383                result,
384            });
385        }
386
387        self.notify_embedder_that_events_were_handled(input_event_outcomes);
388    }
389
390    fn notify_embedder_that_events_were_handled(
391        &self,
392        input_event_outcomes: Vec<InputEventOutcome>,
393    ) {
394        // Wait to to notify the embedder that the event was handled until all pending DOM
395        // event processing is finished.
396        let trusted_window = Trusted::new(&*self.window);
397        self.window
398            .as_global_scope()
399            .task_manager()
400            .dom_manipulation_task_source()
401            .queue(task!(notify_webdriver_input_event_completed: move || {
402                let window = trusted_window.root();
403                window.send_to_embedder(
404                    EmbedderMsg::InputEventsHandled(window.webview_id(), input_event_outcomes));
405            }));
406    }
407
408    /// When an event should be fired on the element that has focus, this returns the target. If
409    /// there is no associated element with the focused area (such as when the viewport is focused),
410    /// then the body is returned. If no body is returned then the `Window` is returned.
411    fn target_for_events_following_focus(&self) -> DomRoot<EventTarget> {
412        let document = self.window.Document();
413        match &*document.focus_handler().focused_area() {
414            FocusableArea::Node { node, .. } => DomRoot::from_ref(node.upcast()),
415            FocusableArea::IFrameViewport { iframe_element, .. } => {
416                DomRoot::from_ref(iframe_element.upcast())
417            },
418            FocusableArea::Viewport => document
419                .GetBody()
420                .map(DomRoot::upcast)
421                .unwrap_or_else(|| DomRoot::from_ref(self.window.upcast())),
422        }
423    }
424
425    pub(crate) fn set_cursor(&self, cursor: Option<Cursor>) {
426        if cursor == self.current_cursor.get() {
427            return;
428        }
429        self.current_cursor.set(cursor);
430        self.window.send_to_embedder(EmbedderMsg::SetCursor(
431            self.window.webview_id(),
432            cursor.unwrap_or_default(),
433        ));
434    }
435
436    fn handle_mouse_left_viewport_event(
437        &self,
438        cx: &mut JSContext,
439        input_event: &ConstellationInputEvent,
440        mouse_leave_event: &MouseLeftViewportEvent,
441    ) {
442        if let Some(current_hover_target) = self.current_hover_target.get() {
443            let current_hover_target = current_hover_target.upcast::<Node>();
444            for element in current_hover_target
445                .inclusive_ancestors(ShadowIncluding::Yes)
446                .filter_map(DomRoot::downcast::<Element>)
447            {
448                element.set_hover_state(false);
449            }
450
451            if let Some(hit_test_result) = self
452                .most_recent_mousemove_point
453                .get()
454                .and_then(|point| self.window.hit_test_from_point_in_viewport(point))
455            {
456                let mouse_out_event = MouseEvent::new_for_platform_motion_event(
457                    cx,
458                    &self.window,
459                    FireMouseEventType::Out,
460                    &hit_test_result,
461                    input_event,
462                );
463
464                // Fire pointerout before mouseout
465                mouse_out_event
466                    .to_pointer_hover_event("pointerout", CanGc::from_cx(cx))
467                    .upcast::<Event>()
468                    .fire(cx, current_hover_target.upcast());
469
470                mouse_out_event
471                    .upcast::<Event>()
472                    .fire(cx, current_hover_target.upcast());
473
474                self.handle_mouse_enter_leave_event(
475                    cx,
476                    DomRoot::from_ref(current_hover_target),
477                    None,
478                    FireMouseEventType::Leave,
479                    &hit_test_result,
480                    input_event,
481                );
482            }
483        }
484
485        // We do not want to always inform the embedder that cursor has been set to the
486        // default cursor, in order to avoid a timing issue when moving between `<iframe>`
487        // elements. There is currently no way to control which `SetCursor` message will
488        // reach the embedder first. This is safer when leaving the `WebView` entirely.
489        if !mouse_leave_event.focus_moving_to_another_iframe {
490            // If focus is moving to another frame, it will decide what the new status
491            // text is, but if this mouse leave event is leaving the WebView entirely,
492            // then clear it.
493            self.window
494                .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None));
495            self.set_cursor(None);
496        } else {
497            self.current_cursor.set(None);
498        }
499
500        self.current_hover_target.set(None);
501        self.most_recent_mousemove_point.set(None);
502    }
503
504    fn handle_mouse_enter_leave_event(
505        &self,
506        cx: &mut JSContext,
507        event_target: DomRoot<Node>,
508        related_target: Option<DomRoot<Node>>,
509        event_type: FireMouseEventType,
510        hit_test_result: &HitTestResult,
511        input_event: &ConstellationInputEvent,
512    ) {
513        assert!(matches!(
514            event_type,
515            FireMouseEventType::Enter | FireMouseEventType::Leave
516        ));
517
518        let common_ancestor = match related_target.as_ref() {
519            Some(related_target) => event_target
520                .common_ancestor_in_flat_tree(related_target)
521                .unwrap_or_else(|| DomRoot::from_ref(&*event_target)),
522            None => DomRoot::from_ref(&*event_target),
523        };
524
525        // We need to create a target chain in case the event target shares
526        // its boundaries with its ancestors.
527        let mut targets = vec![];
528        let mut current = Some(event_target);
529        while let Some(node) = current {
530            if node == common_ancestor {
531                break;
532            }
533            current = node.parent_in_flat_tree();
534            targets.push(node);
535        }
536
537        // The order for dispatching mouseenter/pointerenter events starts from the topmost
538        // common ancestor of the event target and the related target.
539        if event_type == FireMouseEventType::Enter {
540            targets = targets.into_iter().rev().collect();
541        }
542
543        let pointer_event_name = match event_type {
544            FireMouseEventType::Enter => "pointerenter",
545            FireMouseEventType::Leave => "pointerleave",
546            _ => unreachable!(),
547        };
548
549        for target in targets {
550            let mouse_event = MouseEvent::new_for_platform_motion_event(
551                cx,
552                &self.window,
553                event_type,
554                hit_test_result,
555                input_event,
556            );
557            mouse_event
558                .upcast::<Event>()
559                .set_related_target(related_target.as_ref().map(|target| target.upcast()));
560
561            // Fire pointer event before mouse event
562            mouse_event
563                .to_pointer_hover_event(pointer_event_name, CanGc::from_cx(cx))
564                .upcast::<Event>()
565                .fire(cx, target.upcast());
566
567            // Fire mouse event
568            mouse_event.upcast::<Event>().fire(cx, target.upcast());
569        }
570    }
571
572    /// <https://w3c.github.io/uievents/#handle-native-mouse-move>
573    fn handle_native_mouse_move_event(
574        &self,
575        cx: &mut JSContext,
576        input_event: &ConstellationInputEvent,
577    ) {
578        // First check if the capture target is disconnected and release it if so.
579        // This must happen before any pointer event fires.
580        let pointer_id = PointerId::Mouse as i32;
581        let released_disconnected =
582            self.release_disconnected_pointer_capture(cx, pointer_id, "mouse", true);
583
584        // Always do full hit test so we can keep `current_hover_target` in sync
585        // with the actual element under the pointer. Boundary events for hover
586        // transitions are suppressed while pointer capture is active: per spec,
587        // pointer events are retargeted to the capture element.
588        let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
589            return;
590        };
591
592        let old_mouse_move_point = self
593            .most_recent_mousemove_point
594            .replace(Some(hit_test_result.point_in_frame));
595        if old_mouse_move_point == Some(hit_test_result.point_in_frame) {
596            return;
597        }
598
599        // Update the cursor when the mouse moves, if it has changed.
600        self.set_cursor(Some(hit_test_result.cursor));
601
602        let Some(new_target) = hit_test_result
603            .node
604            .inclusive_ancestors(ShadowIncluding::Yes)
605            .find_map(DomRoot::downcast::<Element>)
606        else {
607            return;
608        };
609
610        let capture_is_active = self.get_pointer_capture_target(pointer_id).is_some();
611        let old_hover_target = self.current_hover_target.get();
612        let target_has_changed = old_hover_target
613            .as_ref()
614            .is_none_or(|old_target| *old_target != new_target);
615
616        // Here we know the target has changed, so we must update the state,
617        // dispatch mouseout to the previous one, mouseover to the new one.
618        if target_has_changed {
619            // Dispatch pointerout/mouseout and pointerleave/mouseleave to previous target.
620            if let Some(old_target) = self.current_hover_target.get() {
621                let old_target_is_ancestor_of_new_target = old_target
622                    .upcast::<Node>()
623                    .is_ancestor_of(new_target.upcast::<Node>());
624
625                // If the old target is an ancestor of the new target, this can be skipped
626                // completely, since the node's hover state will be reset below.
627                if !old_target_is_ancestor_of_new_target {
628                    for element in old_target
629                        .upcast::<Node>()
630                        .inclusive_ancestors(ShadowIncluding::Yes)
631                        .filter_map(DomRoot::downcast::<Element>)
632                    {
633                        element.set_hover_state(false);
634                    }
635                }
636
637                if !capture_is_active {
638                    let mouse_out_event = MouseEvent::new_for_platform_motion_event(
639                        cx,
640                        &self.window,
641                        FireMouseEventType::Out,
642                        &hit_test_result,
643                        input_event,
644                    );
645                    mouse_out_event
646                        .upcast::<Event>()
647                        .set_related_target(Some(new_target.upcast()));
648
649                    // Fire pointerout before mouseout
650                    mouse_out_event
651                        .to_pointer_hover_event("pointerout", CanGc::from_cx(cx))
652                        .upcast::<Event>()
653                        .fire(cx, old_target.upcast());
654
655                    mouse_out_event
656                        .upcast::<Event>()
657                        .fire(cx, old_target.upcast());
658
659                    if !old_target_is_ancestor_of_new_target {
660                        let event_target = DomRoot::from_ref(old_target.upcast::<Node>());
661                        let moving_into = Some(DomRoot::from_ref(new_target.upcast::<Node>()));
662                        self.handle_mouse_enter_leave_event(
663                            cx,
664                            event_target,
665                            moving_into,
666                            FireMouseEventType::Leave,
667                            &hit_test_result,
668                            input_event,
669                        );
670                    }
671                }
672            }
673
674            // Dispatch pointerover/mouseover and pointerenter/mouseenter to new target.
675            for element in new_target
676                .upcast::<Node>()
677                .inclusive_ancestors(ShadowIncluding::Yes)
678                .filter_map(DomRoot::downcast::<Element>)
679            {
680                element.set_hover_state(true);
681            }
682
683            if !capture_is_active {
684                let mouse_over_event = MouseEvent::new_for_platform_motion_event(
685                    cx,
686                    &self.window,
687                    FireMouseEventType::Over,
688                    &hit_test_result,
689                    input_event,
690                );
691                mouse_over_event
692                    .upcast::<Event>()
693                    .set_related_target(old_hover_target.as_ref().map(|target| target.upcast()));
694
695                // Fire pointerover before mouseover
696                mouse_over_event
697                    .to_pointer_hover_event("pointerover", CanGc::from_cx(cx))
698                    .upcast::<Event>()
699                    .dispatch(cx, new_target.upcast(), false);
700
701                mouse_over_event
702                    .upcast::<Event>()
703                    .dispatch(cx, new_target.upcast(), false);
704
705                let moving_from = old_hover_target
706                    .map(|old_target| DomRoot::from_ref(old_target.upcast::<Node>()));
707                let event_target = DomRoot::from_ref(new_target.upcast::<Node>());
708                self.handle_mouse_enter_leave_event(
709                    cx,
710                    event_target,
711                    moving_from,
712                    FireMouseEventType::Enter,
713                    &hit_test_result,
714                    input_event,
715                );
716            }
717        }
718
719        // Send mousemove event to topmost target, unless it's an iframe, in which case
720        // `Paint` should have also sent an event to the inner document.
721        let mouse_event = MouseEvent::new_for_platform_motion_event(
722            cx,
723            &self.window,
724            FireMouseEventType::Move,
725            &hit_test_result,
726            input_event,
727        );
728
729        // Send pointermove event before mousemove.
730        // If pointer capture is active, retarget the pointer/mouse events to
731        // the capture element. Boundary events (pointerover/out/enter/leave)
732        // already fired above use the actual hit-test target.
733        let pointer_target = self
734            .get_pointer_capture_target(pointer_id)
735            .map(DomRoot::upcast::<EventTarget>)
736            .unwrap_or_else(|| DomRoot::from_ref(new_target.upcast::<EventTarget>()));
737
738        let pointer_event =
739            mouse_event.to_pointer_event(Atom::from("pointermove"), CanGc::from_cx(cx));
740        pointer_event.upcast::<Event>().set_composed(true);
741        pointer_event.upcast::<Event>().fire(cx, &pointer_target);
742
743        // Process pending pointer capture after firing event, but skip if we just
744        // released a disconnected capture to avoid immediately re-capturing.
745        // https://w3c.github.io/pointerevents/#process-pending-pointer-capture
746        if !released_disconnected {
747            self.process_pending_pointer_capture(cx, pointer_id, "mouse", true);
748        }
749
750        // Send mousemove event. Routed to the capture target when capture is active.
751        mouse_event.upcast::<Event>().fire(cx, &pointer_target);
752
753        self.update_current_hover_target_and_status(Some(new_target));
754    }
755
756    fn update_current_hover_target_and_status(&self, new_hover_target: Option<DomRoot<Element>>) {
757        let current_hover_target = self.current_hover_target.get();
758        if current_hover_target == new_hover_target {
759            return;
760        }
761
762        let previous_hover_target = self.current_hover_target.get();
763        self.current_hover_target.set(new_hover_target.as_deref());
764
765        // If the new hover target is an anchor with a status value, inform the embedder
766        // of the new value.
767        if let Some(target) = self.current_hover_target.get() &&
768            let Some(anchor) = target
769                .upcast::<Node>()
770                .inclusive_ancestors(ShadowIncluding::Yes)
771                .find_map(DomRoot::downcast::<HTMLAnchorElement>)
772        {
773            let status = anchor
774                .full_href_url_for_user_interface()
775                .map(|url| url.to_string());
776            self.window
777                .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), status));
778            return;
779        }
780
781        // No state was set above, which means that the new value of the status in the embedder
782        // should be `None`. Set that now. If `previous_hover_target` is `None` that means this
783        // is the first mouse move event we are seeing after getting the cursor. In that case,
784        // we also clear the status.
785        if previous_hover_target.is_none_or(|previous_hover_target| {
786            previous_hover_target
787                .upcast::<Node>()
788                .inclusive_ancestors(ShadowIncluding::Yes)
789                .any(|node| node.is::<HTMLAnchorElement>())
790        }) {
791            self.window
792                .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None));
793        }
794    }
795
796    pub(crate) fn handle_refresh_cursor(&self) {
797        let Some(most_recent_mousemove_point) = self.most_recent_mousemove_point.get() else {
798            return;
799        };
800
801        let Some(hit_test_result) = self
802            .window
803            .hit_test_from_point_in_viewport(most_recent_mousemove_point)
804        else {
805            return;
806        };
807
808        self.set_cursor(Some(hit_test_result.cursor));
809    }
810
811    fn set_active_element(&self, original_target: &Element) {
812        let find_element_for_activation = |element: &Element| {
813            let node: &Node = element.upcast();
814            if node.is_in_ua_widget() &&
815                let Some(containing_shadow_root) = node.containing_shadow_root()
816            {
817                return containing_shadow_root.Host();
818            }
819
820            // If the element is a label, the activable element is the control element.
821            if node.type_id() ==
822                NodeTypeId::Element(ElementTypeId::HTMLElement(
823                    HTMLElementTypeId::HTMLLabelElement,
824                ))
825            {
826                let label = element.downcast::<HTMLLabelElement>().unwrap();
827                if let Some(control) = label.GetControl() {
828                    return DomRoot::from_ref(control.upcast::<Element>());
829                }
830            }
831
832            DomRoot::from_ref(element)
833        };
834        let element_for_activation = find_element_for_activation(original_target);
835
836        // This might happen if the user is alternating between different pointing devices
837        // such as two mice or a mouse and touch events. Only keep the latest activated.
838        if let Some(currently_active_element) = self.current_active_element.get() {
839            if currently_active_element == element_for_activation {
840                return;
841            }
842            self.unset_active_element();
843        }
844
845        element_for_activation.set_active_state(true);
846        self.current_active_element
847            .set(Some(&*element_for_activation));
848    }
849
850    fn unset_active_element(&self) {
851        if let Some(active_element) = self.current_active_element.take() {
852            active_element.set_active_state(false);
853        }
854    }
855
856    /// <https://w3c.github.io/uievents/#mouseevent-algorithms>
857    /// Handles native mouse down, mouse up, mouse click.
858    fn handle_native_mouse_button_event(
859        &self,
860        cx: &mut JSContext,
861        event: MouseButtonEvent,
862        input_event: &ConstellationInputEvent,
863    ) {
864        // Ignore all incoming events without a hit test.
865        let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
866            return;
867        };
868
869        debug!(
870            "{:?}: at {:?}",
871            event.action, hit_test_result.point_in_frame
872        );
873
874        // Set the sequential focus navigation starting point for any mouse button down event, no
875        // matter if the target is not a node.
876        let document = self.window.Document();
877        if event.action == MouseButtonAction::Down {
878            document
879                .focus_handler()
880                .set_sequential_focus_navigation_starting_point(&hit_test_result.node);
881        }
882
883        let Some(element) = hit_test_result
884            .node
885            .inclusive_ancestors(ShadowIncluding::Yes)
886            .find_map(DomRoot::downcast::<Element>)
887        else {
888            return;
889        };
890
891        let node = element.upcast::<Node>();
892        debug!("{:?} on {:?}", event.action, node.debug_str());
893
894        // <https://html.spec.whatwg.org/multipage/#selector-active>
895        // > If the element is being actively pointed at the element is being activated.
896        // Disabled elements can also be activated, so this must happen before the
897        // early return below.
898        if event.button == MouseButton::Left {
899            if event.action == MouseButtonAction::Down {
900                self.set_active_element(&element);
901            }
902            if event.action == MouseButtonAction::Up {
903                self.unset_active_element();
904            }
905        }
906
907        // https://w3c.github.io/uievents/#hit-test
908        // Prevent mouse event if element is disabled.
909        // TODO: also inert.
910        if element.is_actually_disabled() {
911            return;
912        }
913
914        let mouse_event_type = match event.action {
915            embedder_traits::MouseButtonAction::Up => atom!("mouseup"),
916            embedder_traits::MouseButtonAction::Down => atom!("mousedown"),
917        };
918
919        // From <https://w3c.github.io/pointerevents/#dfn-mousedown>
920        // and <https://w3c.github.io/pointerevents/#mouseup>:
921        //
922        // UIEvent.detail: indicates the current click count incremented by one. For
923        // example, if no click happened before the mousedown, detail will contain
924        // the value 1
925        if event.action == MouseButtonAction::Down {
926            self.click_counting_info
927                .borrow_mut()
928                .reset_click_count_if_necessary(event.button, hit_test_result.point_in_frame);
929        }
930
931        let mouse_event = MouseEvent::for_platform_button_event(
932            cx,
933            mouse_event_type,
934            event,
935            input_event.pressed_mouse_buttons,
936            &self.window,
937            &hit_test_result,
938            input_event.active_keyboard_modifiers,
939            self.click_counting_info.borrow().count + 1,
940        );
941
942        match event.action {
943            MouseButtonAction::Down => {
944                self.last_mouse_button_down_point
945                    .set(Some(hit_test_result.point_in_frame));
946
947                // Step 6. Dispatch pointerdown event.
948                let mouse_buttons_down = self.mouse_buttons_down.get();
949                let pointer_event_name = if mouse_buttons_down == 0 {
950                    // From <https://w3c.github.io/pointerevents/#dfn-pointerdown>
951                    // > The user agent MUST fire a pointer event named pointerdown when a pointer enters
952                    // > the active buttons state. For mouse, this is when the device transitions from no
953                    // > buttons depressed to at least one button depressed.
954                    "pointerdown".into()
955                } else {
956                    // From <https://w3c.github.io/pointerevents/#dfn-pointermove>:
957                    // > The user agent MUST fire a pointer event named pointermove when a pointer
958                    // > changes any properties that don't fire pointerdown or pointerup events. This
959                    // > includes any changes to coordinates, pressure, tangential pressure, tilt, twist,
960                    // > contact geometry (width and height) or chorded buttons.
961                    "pointermove".into()
962                };
963                let pointer_event =
964                    mouse_event.to_pointer_event(pointer_event_name, CanGc::from_cx(cx));
965
966                // Check for pointer capture target for mouse events
967                let pointer_id = PointerId::Mouse as i32;
968
969                // Release any disconnected capture target before firing pointer events
970                let released_disconnected =
971                    self.release_disconnected_pointer_capture(cx, pointer_id, "mouse", true);
972
973                // Get the current capture target (before processing pending changes)
974                let pointer_target = self
975                    .get_pointer_capture_target(pointer_id)
976                    .map(DomRoot::upcast::<EventTarget>)
977                    .unwrap_or_else(|| DomRoot::from_ref(node.upcast::<EventTarget>()));
978
979                // Increment button count before firing so setPointerCapture works in handler.
980                self.mouse_buttons_down.set(mouse_buttons_down + 1);
981
982                pointer_event.upcast::<Event>().fire(cx, &pointer_target);
983
984                // Process pending pointer capture after firing event, but skip if we just
985                // released a disconnected capture to avoid immediately re-capturing.
986                // https://w3c.github.io/pointerevents/#process-pending-pointer-capture
987                if !released_disconnected {
988                    self.process_pending_pointer_capture(cx, pointer_id, "mouse", true);
989                }
990
991                // Step 7. Let result = dispatch event at target
992                let result = mouse_event
993                    .upcast::<Event>()
994                    .dispatch(cx, node.upcast(), false);
995
996                // Step 8. If result is true and target is a focusable area
997                // that is click focusable, then Run the focusing steps at target.
998                if result {
999                    // Note that this differs from the specification, because we are going to look
1000                    // for the first inclusive ancestor that is click focusable and then focus it.
1001                    // See documentation for [`Node::find_click_focusable_area`].
1002                    document
1003                        .focus_handler()
1004                        .focus(cx, node.find_click_focusable_area());
1005                }
1006
1007                // Step 9. If mbutton is the secondary mouse button, then
1008                // Maybe show context menu with native, target.
1009                if let MouseButton::Right = event.button {
1010                    self.maybe_show_context_menu(cx, node.upcast(), &hit_test_result, input_event);
1011                }
1012            },
1013            // https://w3c.github.io/pointerevents/#dfn-handle-native-mouse-up
1014            MouseButtonAction::Up => {
1015                // Step 6. Dispatch pointerup event.
1016                let mouse_buttons_down = self.mouse_buttons_down.get();
1017                let pointer_event_name = if mouse_buttons_down == 1 {
1018                    // From <https://w3c.github.io/pointerevents/#dfn-pointerup>:
1019                    // > The user agent MUST fire a pointer event named pointerup when a pointer leaves
1020                    // > the active buttons state. For mouse, this is when the device transitions from at
1021                    // > least one button depressed to no buttons depressed.
1022                    "pointerup".into()
1023                } else {
1024                    // From <https://w3c.github.io/pointerevents/#dfn-pointermove>:
1025                    // > The user agent MUST fire a pointer event named pointermove when a pointer
1026                    // > changes any properties that don't fire pointerdown or pointerup events. This
1027                    // > includes any changes to coordinates, pressure, tangential pressure, tilt, twist,
1028                    // > contact geometry (width and height) or chorded buttons.
1029                    "pointermove".into()
1030                };
1031                let pointer_event =
1032                    mouse_event.to_pointer_event(pointer_event_name, CanGc::from_cx(cx));
1033
1034                // Check for pointer capture target for mouse events
1035                let pointer_id = PointerId::Mouse as i32;
1036
1037                // Release any disconnected capture target before firing pointer events
1038                let released_disconnected =
1039                    self.release_disconnected_pointer_capture(cx, pointer_id, "mouse", true);
1040
1041                // Get the current capture target (before any state changes)
1042                let pointer_target = self
1043                    .get_pointer_capture_target(pointer_id)
1044                    .map(DomRoot::upcast::<EventTarget>)
1045                    .unwrap_or_else(|| DomRoot::from_ref(node.upcast::<EventTarget>()));
1046
1047                pointer_event.upcast::<Event>().fire(cx, &pointer_target);
1048
1049                // Decrement button count after firing event, so setPointerCapture/releasePointerCapture
1050                // work during the pointerup handler (pointer is still "active").
1051                self.mouse_buttons_down
1052                    .set(mouse_buttons_down.saturating_sub(1));
1053
1054                // Process pending pointer capture after decrementing button count, but skip
1055                // if we just released a disconnected capture to avoid immediately re-capturing.
1056                // https://w3c.github.io/pointerevents/#process-pending-pointer-capture
1057                if !released_disconnected {
1058                    self.process_pending_pointer_capture(cx, pointer_id, "mouse", true);
1059                }
1060
1061                // Implicitly release pointer capture when last button was released
1062                if mouse_buttons_down == 1 {
1063                    self.implicit_release_pointer_capture(cx, pointer_id, "mouse", true);
1064                }
1065
1066                // Step 7. dispatch event at target.
1067                mouse_event
1068                    .upcast::<Event>()
1069                    .dispatch(cx, node.upcast(), false);
1070
1071                // Click counts should still work for other buttons even though they
1072                // do not trigger "click" and "dblclick" events, so we increment
1073                // even when those events are not fired.
1074                self.click_counting_info
1075                    .borrow_mut()
1076                    .increment_click_count(event.button, hit_test_result.point_in_frame);
1077
1078                self.maybe_trigger_click_for_mouse_button_down_event(
1079                    cx,
1080                    event,
1081                    input_event,
1082                    &hit_test_result,
1083                    &element,
1084                );
1085            },
1086        }
1087    }
1088
1089    /// <https://w3c.github.io/pointerevents/#handle-native-mouse-click>
1090    /// <https://w3c.github.io/pointerevents/#handle-native-mouse-double-click>
1091    fn maybe_trigger_click_for_mouse_button_down_event(
1092        &self,
1093        cx: &mut JSContext,
1094        event: MouseButtonEvent,
1095        input_event: &ConstellationInputEvent,
1096        hit_test_result: &HitTestResult,
1097        element: &Element,
1098    ) {
1099        if event.button != MouseButton::Left {
1100            return;
1101        }
1102
1103        let Some(last_mouse_button_down_point) = self.last_mouse_button_down_point.take() else {
1104            return;
1105        };
1106
1107        let distance = last_mouse_button_down_point.distance_to(hit_test_result.point_in_frame);
1108        let maximum_click_distance = 10.0 * self.window.device_pixel_ratio().get();
1109        if distance > maximum_click_distance {
1110            return;
1111        }
1112
1113        // From <https://w3c.github.io/pointerevents/#click>
1114        // > The click event type MUST be dispatched on the topmost event target indicated by the
1115        // > pointer, when the user presses down and releases the primary pointer button.
1116        //
1117        // For nodes inside a text input UA shadow DOM, dispatch dblclick at the shadow host.
1118        // TODO: This should likely be handled via event retargeting.
1119        let element = match hit_test_result.node.find_click_focusable_area() {
1120            FocusableArea::Node { node, .. } => DomRoot::downcast::<Element>(node),
1121            _ => None,
1122        }
1123        .unwrap_or_else(|| DomRoot::from_ref(element));
1124        self.most_recently_clicked_element.set(Some(&*element));
1125
1126        let click_count = self.click_counting_info.borrow().count;
1127        element.set_click_in_progress(true);
1128        MouseEvent::for_platform_button_event(
1129            cx,
1130            atom!("click"),
1131            event,
1132            input_event.pressed_mouse_buttons,
1133            &self.window,
1134            hit_test_result,
1135            input_event.active_keyboard_modifiers,
1136            click_count,
1137        )
1138        .upcast::<Event>()
1139        .dispatch(cx, element.upcast(), false);
1140        element.set_click_in_progress(false);
1141
1142        // The firing of "dbclick" events is dependent on the platform, so we have
1143        // some flexibility here. Some browsers on some platforms only fire a
1144        // "dbclick" when the click count is 2 and others essentially fire one for
1145        // every 2 clicks in a sequence. In all cases, browsers set the click count
1146        // `detail` property to 2.
1147        //
1148        // We follow the latter approach here, considering that every sequence of
1149        // even numbered clicks is a series of double clicks.
1150        if click_count.is_multiple_of(2) {
1151            MouseEvent::for_platform_button_event(
1152                cx,
1153                Atom::from("dblclick"),
1154                event,
1155                input_event.pressed_mouse_buttons,
1156                &self.window,
1157                hit_test_result,
1158                input_event.active_keyboard_modifiers,
1159                2,
1160            )
1161            .upcast::<Event>()
1162            .dispatch(cx, element.upcast(), false);
1163        }
1164    }
1165
1166    /// <https://www.w3.org/TR/pointerevents4/#maybe-show-context-menu>
1167    fn maybe_show_context_menu(
1168        &self,
1169        cx: &mut js::context::JSContext,
1170        target: &EventTarget,
1171        hit_test_result: &HitTestResult,
1172        input_event: &ConstellationInputEvent,
1173    ) {
1174        // <https://w3c.github.io/pointerevents/#contextmenu>
1175        let menu_event = PointerEvent::new(
1176            &self.window,                // window
1177            "contextmenu".into(),        // type
1178            EventBubbles::Bubbles,       // can_bubble
1179            EventCancelable::Cancelable, // cancelable
1180            Some(&self.window),          // view
1181            0,                           // detail
1182            hit_test_result.point_in_frame.to_i32(),
1183            hit_test_result.point_in_frame.to_i32(),
1184            hit_test_result
1185                .point_relative_to_initial_containing_block
1186                .to_i32(),
1187            input_event.active_keyboard_modifiers,
1188            2i16, // button, right mouse button
1189            input_event.pressed_mouse_buttons,
1190            None,                     // related_target
1191            None,                     // point_in_target
1192            PointerId::Mouse as i32,  // pointer_id
1193            1,                        // width
1194            1,                        // height
1195            0.5,                      // pressure
1196            0.0,                      // tangential_pressure
1197            0,                        // tilt_x
1198            0,                        // tilt_y
1199            0,                        // twist
1200            PI / 2.0,                 // altitude_angle
1201            0.0,                      // azimuth_angle
1202            DOMString::from("mouse"), // pointer_type
1203            true,                     // is_primary
1204            vec![],                   // coalesced_events
1205            vec![],                   // predicted_events
1206            CanGc::from_cx(cx),
1207        );
1208        menu_event.upcast::<Event>().set_composed(true);
1209
1210        // Step 3. Let result = dispatch menuevent at target.
1211        let result = menu_event.upcast::<Event>().fire(cx, target);
1212
1213        // Step 4. If result is true, then show the UA context menu
1214        if result {
1215            self.window
1216                .Document()
1217                .embedder_controls()
1218                .show_context_menu(hit_test_result);
1219        };
1220    }
1221
1222    fn handle_touch_event(
1223        &self,
1224        cx: &mut JSContext,
1225        event: EmbedderTouchEvent,
1226        input_event: &ConstellationInputEvent,
1227    ) -> InputEventResult {
1228        // Ignore all incoming events without a hit test.
1229        let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
1230            self.update_active_touch_points_when_early_return(event);
1231            return Default::default();
1232        };
1233
1234        let TouchId(identifier) = event.touch_id;
1235
1236        let Some(element) = hit_test_result
1237            .node
1238            .inclusive_ancestors(ShadowIncluding::Yes)
1239            .find_map(DomRoot::downcast::<Element>)
1240        else {
1241            self.update_active_touch_points_when_early_return(event);
1242            return Default::default();
1243        };
1244
1245        let current_target = DomRoot::upcast::<EventTarget>(element.clone());
1246        let window = &*self.window;
1247
1248        let client_x = Finite::wrap(hit_test_result.point_in_frame.x as f64);
1249        let client_y = Finite::wrap(hit_test_result.point_in_frame.y as f64);
1250        let page_x =
1251            Finite::wrap(hit_test_result.point_in_frame.x as f64 + window.PageXOffset() as f64);
1252        let page_y =
1253            Finite::wrap(hit_test_result.point_in_frame.y as f64 + window.PageYOffset() as f64);
1254
1255        // This is used to construct pointerevent and touchdown event.
1256        let pointer_touch = Touch::new(
1257            window,
1258            identifier,
1259            &current_target,
1260            client_x,
1261            client_y, // TODO: Get real screen coordinates?
1262            client_x,
1263            client_y,
1264            page_x,
1265            page_y,
1266            CanGc::from_cx(cx),
1267        );
1268
1269        // Dispatch pointer event before updating active touch points and before touch event.
1270        let pointer_event_name = match event.event_type {
1271            TouchEventType::Down => "pointerdown",
1272            TouchEventType::Move => "pointermove",
1273            TouchEventType::Up => "pointerup",
1274            TouchEventType::Cancel => "pointercancel",
1275        };
1276
1277        // Map the embedder-side subtype to the spec `pointerType` string.
1278        let pointer_type = match event.pointer_type {
1279            TouchPointerType::Pen => "pen",
1280            TouchPointerType::Touch => "touch",
1281        };
1282
1283        // Get or create pointer ID for this touch
1284        let pointer_id = self.get_or_create_pointer_id_for_touch(identifier);
1285        let is_primary = self.is_primary_pointer(pointer_id);
1286
1287        // For touch devices (which don't support hover), fire pointerover/pointerenter
1288        // <https://w3c.github.io/pointerevents/#mapping-for-devices-that-do-not-support-hover>
1289        if matches!(event.event_type, TouchEventType::Down) {
1290            // Fire pointerover
1291            let pointer_over = pointer_touch.to_pointer_event(
1292                window,
1293                "pointerover",
1294                pointer_id,
1295                is_primary,
1296                pointer_type,
1297                input_event.active_keyboard_modifiers,
1298                true, // cancelable
1299                Some(hit_test_result.point_in_node),
1300                CanGc::from_cx(cx),
1301            );
1302            pointer_over.upcast::<Event>().fire(cx, &current_target);
1303
1304            // Fire pointerenter hierarchically (from topmost ancestor to target)
1305            self.fire_pointer_event_for_touch(
1306                cx,
1307                &element,
1308                &pointer_touch,
1309                pointer_id,
1310                "pointerenter",
1311                is_primary,
1312                pointer_type,
1313                input_event,
1314                &hit_test_result,
1315            );
1316        }
1317
1318        // Release any disconnected capture target before firing pointer events,
1319        // but not for pointercancel: let implicit_release handle that so
1320        // lostpointercapture fires after pointercancel per spec.
1321        let released_disconnected = if matches!(event.event_type, TouchEventType::Cancel) {
1322            false
1323        } else {
1324            self.release_disconnected_pointer_capture(cx, pointer_id, pointer_type, is_primary)
1325        };
1326
1327        // Get the current capture target (before processing pending changes)
1328        let pointer_target = self
1329            .get_pointer_capture_target(pointer_id)
1330            .map(DomRoot::upcast::<EventTarget>)
1331            .unwrap_or_else(|| current_target.clone());
1332
1333        let pointer_event = pointer_touch.to_pointer_event(
1334            window,
1335            pointer_event_name,
1336            pointer_id,
1337            is_primary,
1338            pointer_type,
1339            input_event.active_keyboard_modifiers,
1340            event.is_cancelable(),
1341            Some(hit_test_result.point_in_node),
1342            CanGc::from_cx(cx),
1343        );
1344        pointer_event.upcast::<Event>().fire(cx, &pointer_target);
1345
1346        // Process pending pointer capture after firing event, but skip if we just
1347        // released a disconnected capture (to avoid immediately re-capturing).
1348        // Also skip for pointercancel: per spec, process pending only runs for
1349        // pointerdown, pointermove, and pointerup, not pointercancel.
1350        // https://w3c.github.io/pointerevents/#process-pending-pointer-capture
1351        if !released_disconnected && !matches!(event.event_type, TouchEventType::Cancel) {
1352            self.process_pending_pointer_capture(cx, pointer_id, pointer_type, is_primary);
1353        }
1354
1355        // Implicitly release pointer capture on pointerup or pointercancel
1356        // For pointercancel, this fires lostpointercapture after the pointercancel event.
1357        if matches!(
1358            event.event_type,
1359            TouchEventType::Up | TouchEventType::Cancel
1360        ) {
1361            self.implicit_release_pointer_capture(cx, pointer_id, pointer_type, is_primary);
1362        }
1363
1364        // For touch devices, fire pointerout/pointerleave after pointerup/pointercancel
1365        // <https://w3c.github.io/pointerevents/#mapping-for-devices-that-do-not-support-hover>
1366        if matches!(
1367            event.event_type,
1368            TouchEventType::Up | TouchEventType::Cancel
1369        ) {
1370            // Fire pointerout
1371            let pointer_out = pointer_touch.to_pointer_event(
1372                window,
1373                "pointerout",
1374                pointer_id,
1375                is_primary,
1376                pointer_type,
1377                input_event.active_keyboard_modifiers,
1378                true, // cancelable
1379                Some(hit_test_result.point_in_node),
1380                CanGc::from_cx(cx),
1381            );
1382            pointer_out.upcast::<Event>().fire(cx, &current_target);
1383
1384            // Fire pointerleave hierarchically (from target to topmost ancestor)
1385            self.fire_pointer_event_for_touch(
1386                cx,
1387                &element,
1388                &pointer_touch,
1389                pointer_id,
1390                "pointerleave",
1391                is_primary,
1392                pointer_type,
1393                input_event,
1394                &hit_test_result,
1395            );
1396        }
1397
1398        let (touch_dispatch_target, changed_touch) = match event.event_type {
1399            TouchEventType::Down => {
1400                // Add a new touch point
1401                self.active_touch_points
1402                    .borrow_mut()
1403                    .push(Dom::from_ref(&*pointer_touch));
1404                self.set_active_element(&element);
1405                (current_target, pointer_touch)
1406            },
1407            _ => {
1408                // From <https://w3c.github.io/touch-events/#dfn-touchend>:
1409                // > For move/up/cancel:
1410                // > The target of this event must be the same Element on which the touch
1411                // > point started when it was first placed on the surface, even if the touch point
1412                // > has since moved outside the interactive area of the target element.
1413                let mut active_touch_points = self.active_touch_points.borrow_mut();
1414                let Some(index) = active_touch_points
1415                    .iter()
1416                    .position(|point| point.Identifier() == identifier)
1417                else {
1418                    warn!("No active touch point for {:?}", event.event_type);
1419                    return Default::default();
1420                };
1421                // This is the original target that was selected during `touchstart` event handling.
1422                let original_target = active_touch_points[index].Target();
1423
1424                let touch_with_touchstart_target = Touch::new(
1425                    window,
1426                    identifier,
1427                    &original_target,
1428                    client_x,
1429                    client_y,
1430                    client_x,
1431                    client_y,
1432                    page_x,
1433                    page_y,
1434                    CanGc::from_cx(cx),
1435                );
1436
1437                // Update or remove the stored touch
1438                match event.event_type {
1439                    TouchEventType::Move => {
1440                        active_touch_points[index] = Dom::from_ref(&*touch_with_touchstart_target);
1441                    },
1442                    TouchEventType::Up | TouchEventType::Cancel => {
1443                        active_touch_points.swap_remove(index);
1444                        self.remove_pointer_id_for_touch(identifier);
1445                        self.unset_active_element();
1446                    },
1447                    TouchEventType::Down => unreachable!("Should have been handled above"),
1448                }
1449                (original_target, touch_with_touchstart_target)
1450            },
1451        };
1452
1453        rooted_vec!(let mut target_touches);
1454        target_touches.extend(
1455            self.active_touch_points
1456                .borrow()
1457                .iter()
1458                .filter(|touch| touch.Target() == touch_dispatch_target)
1459                .cloned(),
1460        );
1461
1462        let event_name = match event.event_type {
1463            TouchEventType::Down => "touchstart",
1464            TouchEventType::Move => "touchmove",
1465            TouchEventType::Up => "touchend",
1466            TouchEventType::Cancel => "touchcancel",
1467        };
1468
1469        let touch_event = TouchEvent::new(
1470            window,
1471            event_name.into(),
1472            EventBubbles::Bubbles,
1473            EventCancelable::from(event.is_cancelable()),
1474            EventComposed::Composed,
1475            Some(window),
1476            0i32,
1477            &TouchList::new(
1478                window,
1479                self.active_touch_points.borrow().r(),
1480                CanGc::from_cx(cx),
1481            ),
1482            &TouchList::new(window, from_ref(&&*changed_touch), CanGc::from_cx(cx)),
1483            &TouchList::new(window, target_touches.r(), CanGc::from_cx(cx)),
1484            // FIXME: modifier keys
1485            false,
1486            false,
1487            false,
1488            false,
1489            CanGc::from_cx(cx),
1490        );
1491        let event = touch_event.upcast::<Event>();
1492        event.fire(cx, &touch_dispatch_target);
1493        event.flags().into()
1494    }
1495
1496    /// Updates the active touch points when a hit test fails early.
1497    ///
1498    /// - For `Down`: No action needed; a failed down event won't create an active point.
1499    /// - For `Move`: No action needed; position information is unavailable, so we cannot update.
1500    /// - For `Up`/`Cancel`: Remove the corresponding touch point and its pointer ID mapping.
1501    ///
1502    /// When a touchup or touchcancel occurs at that touch point,
1503    /// a warning is triggered: Received touchup/touchcancel event for a non-active touch point.
1504    fn update_active_touch_points_when_early_return(&self, event: EmbedderTouchEvent) {
1505        match event.event_type {
1506            TouchEventType::Down | TouchEventType::Move => {},
1507            TouchEventType::Up | TouchEventType::Cancel => {
1508                let mut active_touch_points = self.active_touch_points.borrow_mut();
1509                if let Some(index) = active_touch_points
1510                    .iter()
1511                    .position(|t| t.Identifier() == event.touch_id.0)
1512                {
1513                    active_touch_points.swap_remove(index);
1514                    self.remove_pointer_id_for_touch(event.touch_id.0);
1515                } else {
1516                    warn!(
1517                        "Received {:?} for a non-active touch point {}",
1518                        event.event_type, event.touch_id.0
1519                    );
1520                }
1521            },
1522        }
1523    }
1524
1525    /// The entry point for all key processing for web content
1526    fn handle_keyboard_event(
1527        &self,
1528        cx: &mut JSContext,
1529        keyboard_event: EmbedderKeyboardEvent,
1530    ) -> InputEventResult {
1531        let target = &self.target_for_events_following_focus();
1532        let keyevent = KeyboardEvent::new_with_platform_keyboard_event(
1533            cx,
1534            &self.window,
1535            keyboard_event.event.state.event_type().into(),
1536            &keyboard_event.event,
1537        );
1538
1539        let event = keyevent.upcast::<Event>();
1540
1541        event.set_composed(true);
1542
1543        event.fire(cx, target);
1544
1545        let mut flags = event.flags();
1546        if flags.contains(EventFlags::Canceled) {
1547            return flags.into();
1548        }
1549
1550        // https://w3c.github.io/uievents/#keys-cancelable-keys
1551        // it MUST prevent the respective beforeinput and input
1552        // (and keypress if supported) events from being generated
1553        // TODO: keypress should be deprecated and superceded by beforeinput
1554
1555        let is_character_value_key = matches!(
1556            keyboard_event.event.key,
1557            Key::Character(_) | Key::Named(NamedKey::Enter)
1558        );
1559        if keyboard_event.event.state == KeyState::Down &&
1560            is_character_value_key &&
1561            !keyboard_event.event.is_composing
1562        {
1563            // https://w3c.github.io/uievents/#keypress-event-order
1564            let keypress_event = KeyboardEvent::new_with_platform_keyboard_event(
1565                cx,
1566                &self.window,
1567                atom!("keypress"),
1568                &keyboard_event.event,
1569            );
1570            keypress_event.upcast::<Event>().set_composed(true);
1571            let event = keypress_event.upcast::<Event>();
1572            event.fire(cx, target);
1573            flags = event.flags();
1574        }
1575
1576        flags.into()
1577    }
1578
1579    fn handle_ime_event(&self, cx: &mut JSContext, event: ImeEvent) -> InputEventResult {
1580        let document = self.window.Document();
1581        let composition_event = match event {
1582            ImeEvent::Dismissed => {
1583                document.focus_handler().focus(cx, FocusableArea::Viewport);
1584                return Default::default();
1585            },
1586            ImeEvent::Composition(composition_event) => composition_event,
1587        };
1588
1589        // spec: https://w3c.github.io/uievents/#compositionstart
1590        // spec: https://w3c.github.io/uievents/#compositionupdate
1591        // spec: https://w3c.github.io/uievents/#compositionend
1592        // > Event.target : focused element processing the composition
1593        let focused_area = document.focus_handler().focused_area();
1594        let Some(focused_element) = focused_area.element() else {
1595            // Event is only dispatched if there is a focused element.
1596            return Default::default();
1597        };
1598
1599        let cancelable = composition_event.state == keyboard_types::CompositionState::Start;
1600        let event = CompositionEvent::new(
1601            &self.window,
1602            composition_event.state.event_type().into(),
1603            true,
1604            cancelable,
1605            Some(&self.window),
1606            0,
1607            DOMString::from(composition_event.data),
1608            CanGc::from_cx(cx),
1609        );
1610
1611        let event = event.upcast::<Event>();
1612        event.fire(cx, focused_element.upcast());
1613        event.flags().into()
1614    }
1615
1616    fn handle_wheel_event(
1617        &self,
1618        cx: &mut JSContext,
1619        event: EmbedderWheelEvent,
1620        input_event: &ConstellationInputEvent,
1621    ) -> InputEventResult {
1622        // Ignore all incoming events without a hit test.
1623        let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
1624            return Default::default();
1625        };
1626
1627        let Some(el) = hit_test_result
1628            .node
1629            .inclusive_ancestors(ShadowIncluding::Yes)
1630            .find_map(DomRoot::downcast::<Element>)
1631        else {
1632            return Default::default();
1633        };
1634
1635        let node = el.upcast::<Node>();
1636        debug!(
1637            "wheel: on {:?} at {:?}",
1638            node.debug_str(),
1639            hit_test_result.point_in_frame
1640        );
1641
1642        // https://w3c.github.io/uievents/#event-wheelevents
1643        let dom_event = WheelEvent::new(
1644            &self.window,
1645            "wheel".into(),
1646            EventBubbles::Bubbles,
1647            EventCancelable::Cancelable,
1648            Some(&self.window),
1649            0i32,
1650            hit_test_result.point_in_frame.to_i32(),
1651            hit_test_result.point_in_frame.to_i32(),
1652            hit_test_result
1653                .point_relative_to_initial_containing_block
1654                .to_i32(),
1655            input_event.active_keyboard_modifiers,
1656            0i16,
1657            input_event.pressed_mouse_buttons,
1658            None,
1659            None,
1660            // winit defines positive wheel delta values as revealing more content left/up.
1661            // https://docs.rs/winit-gtk/latest/winit/event/enum.MouseScrollDelta.html
1662            // This is the opposite of wheel delta in uievents
1663            // https://w3c.github.io/uievents/#dom-wheeleventinit-deltaz
1664            Finite::wrap(-event.delta.x),
1665            Finite::wrap(-event.delta.y),
1666            Finite::wrap(-event.delta.z),
1667            event.delta.mode as u32,
1668            CanGc::from_cx(cx),
1669        );
1670
1671        let dom_event = dom_event.upcast::<Event>();
1672        dom_event.set_trusted(true);
1673        dom_event.set_composed(true);
1674        dom_event.fire(cx, node.upcast());
1675
1676        dom_event.flags().into()
1677    }
1678
1679    #[cfg(feature = "gamepad")]
1680    fn handle_gamepad_event(&self, gamepad_event: EmbedderGamepadEvent) {
1681        match gamepad_event {
1682            EmbedderGamepadEvent::Connected(index, name, bounds, supported_haptic_effects) => {
1683                self.handle_gamepad_connect(
1684                    index.0,
1685                    name,
1686                    bounds.axis_bounds,
1687                    bounds.button_bounds,
1688                    supported_haptic_effects,
1689                );
1690            },
1691            EmbedderGamepadEvent::Disconnected(index) => {
1692                self.handle_gamepad_disconnect(index.0);
1693            },
1694            EmbedderGamepadEvent::Updated(index, update_type) => {
1695                self.receive_new_gamepad_button_or_axis(index.0, update_type);
1696            },
1697        };
1698    }
1699
1700    /// <https://www.w3.org/TR/gamepad/#dfn-gamepadconnected>
1701    #[cfg(feature = "gamepad")]
1702    fn handle_gamepad_connect(
1703        &self,
1704        // As the spec actually defines how to set the gamepad index, the GilRs index
1705        // is currently unused, though in practice it will almost always be the same.
1706        // More infra is currently needed to track gamepads across windows.
1707        _index: usize,
1708        name: String,
1709        axis_bounds: (f64, f64),
1710        button_bounds: (f64, f64),
1711        supported_haptic_effects: GamepadSupportedHapticEffects,
1712    ) {
1713        // Step 1. Let document be the current global object's associated Document; otherwise null.
1714        let doc = self.window.Document();
1715
1716        // Step 2. If document is not null and is not allowed to use the "gamepad" permission,
1717        //         then abort these steps.
1718        if !doc.allowed_to_use_feature(PermissionName::Gamepad) {
1719            return;
1720        }
1721
1722        let trusted_window = Trusted::new(&*self.window);
1723
1724        // Step 3. Queue a global task on the gamepad task source with the current global object
1725        //         to perform the following steps:
1726        self.window
1727            .upcast::<GlobalScope>()
1728            .task_manager()
1729            .gamepad_task_source()
1730            .queue(task!(gamepad_connected: move |cx| {
1731                let window = trusted_window.root();
1732
1733                // Step 3.1. Let gamepad be a new Gamepad representing the gamepad.
1734                // Step 3.2. Let navigator be gamepad's relevant global object's Navigator object.
1735                let navigator = window.Navigator();
1736                let selected_index = navigator.select_gamepad_index();
1737                let gamepad = Gamepad::new(
1738                    cx,
1739                    &window,
1740                    selected_index,
1741                    name,
1742                    "standard".into(),
1743                    axis_bounds,
1744                    button_bounds,
1745                    supported_haptic_effects,
1746                    false,
1747                );
1748
1749                // Step 3.3. Set navigator.[[gamepads]][gamepad.index] to gamepad.
1750                navigator.set_gamepad(selected_index as usize, Some(&gamepad));
1751
1752                // Step 3.4. If navigator.[[hasGamepadGesture]] is true:
1753                if navigator.has_gamepad_gesture() {
1754                    // Step 3.4.1. Set gamepad.[[exposed]] to true.
1755                    gamepad.set_exposed(true);
1756                    // Step 3.4.2. If document is not null and is fully active, then fire an
1757                    //            event named gamepadconnected at gamepad's relevant global
1758                    //            object using GamepadEvent with its gamepad attribute
1759                    //            initialized to gamepad.
1760                    if window.Document().is_fully_active() {
1761                        gamepad.notify_event(cx, GamepadEventType::Connected);
1762                    }
1763                }
1764            }));
1765    }
1766
1767    /// <https://www.w3.org/TR/gamepad/#dfn-gamepaddisconnected>
1768    #[cfg(feature = "gamepad")]
1769    fn handle_gamepad_disconnect(&self, index: usize) {
1770        // Step 1. Let gamepad be the Gamepad representing the unavailable device.
1771        // Step 2. Queue a global task on the gamepad task source with
1772        //         gamepad's relevant global object to perform the following steps:
1773        let trusted_window = Trusted::new(&*self.window);
1774        self.window
1775            .upcast::<GlobalScope>()
1776            .task_manager()
1777            .gamepad_task_source()
1778            .queue(task!(gamepad_disconnected: move |cx| {
1779                let window = trusted_window.root();
1780                let navigator = window.Navigator();
1781
1782                if let Some(gamepad) = navigator.get_gamepad(index) {
1783                    // Step 2.1. Set gamepad.[[connected]] to false.
1784                    gamepad.update_connected(false);
1785                    // Step 2.2. Let document be gamepad's relevant global object's
1786                    //           associated Document; otherwise null.
1787                    // Step 2.3. If gamepad.[[exposed]] is true and document is not null
1788                    //           and is fully active, then fire an event named
1789                    //           gamepaddisconnected at gamepad's relevant global object
1790                    //           using GamepadEvent with its gamepad attribute
1791                    //           initialized to gamepad.
1792                    if gamepad.exposed() && window.Document().is_fully_active() {
1793                        gamepad.notify_event(cx, GamepadEventType::Disconnected);
1794                    }
1795                }
1796
1797                // Step 2.4. Let navigator be gamepad's relevant global object's
1798                //           Navigator object.
1799                // Step 2.5. Set navigator.[[gamepads]][gamepad.index] to null.
1800                navigator.set_gamepad(index, None);
1801                // Step 2.6. While navigator.[[gamepads]] is not empty and the last item of
1802                //           navigator.[[gamepads]] is null, remove the last item of navigator.[[gamepads]].
1803                navigator.shrink_gamepads_list();
1804            }));
1805    }
1806
1807    /// <https://www.w3.org/TR/gamepad/#receiving-inputs>
1808    #[cfg(feature = "gamepad")]
1809    fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) {
1810        // Step 1. Let gamepad be the Gamepad object representing the device that received
1811        //         new button or axis input values.
1812        let trusted_window = Trusted::new(&*self.window);
1813
1814        // Step 2. Queue a global task on the gamepad task source with gamepad's
1815        //         relevant global object to update gamepad state for gamepad.
1816        self.window
1817            .upcast::<GlobalScope>()
1818            .task_manager()
1819            .gamepad_task_source()
1820            .queue(task!(update_gamepad_state: move || {
1821                let window = trusted_window.root();
1822                let document = window.Document();
1823                document.event_handler().update_gamepad_state(index, update_type);
1824            }));
1825    }
1826
1827    /// <https://w3c.github.io/gamepad/#dfn-update-gamepad-state>
1828    #[cfg(feature = "gamepad")]
1829    fn update_gamepad_state(&self, gamepad_index: usize, update_type: GamepadUpdateType) {
1830        use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
1831        // Step 1. Let now be the current high resolution time given
1832        //         gamepad's relevant global object.
1833        let now = *self.window.Performance().Now();
1834
1835        // Step 6. Let navigator be gamepad's relevant global object's
1836        //         Navigator object.
1837        let navigator = self.window.Navigator();
1838
1839        if let Some(gamepad) = navigator.get_gamepad(gamepad_index) {
1840            // Step 2. Set gamepad.[[timestamp]] to now.
1841            gamepad.update_timestamp(now);
1842            // Step 3. Run the steps to map and normalize axes for gamepad.
1843            // Step 4. Run the steps to map and normalize buttons for gamepad.
1844            match update_type {
1845                GamepadUpdateType::Axis(axis_index, value) => {
1846                    gamepad.map_and_normalize_axes(axis_index, value);
1847                },
1848                GamepadUpdateType::Button(button_index, value) => {
1849                    gamepad.map_and_normalize_buttons(button_index, value);
1850                },
1851            };
1852            // TODO Step 5. Run the steps to record touches for gamepad.
1853
1854            // Step 7. If navigator.[[hasGamepadGesture]] is false and
1855            //         gamepad contains a gamepad user gesture:
1856            if !navigator.has_gamepad_gesture() && contains_user_gesture(update_type) {
1857                // Step 7.1. Set navigator.[[hasGamepadGesture]] to true.
1858                navigator.set_has_gamepad_gesture(true);
1859                // Step 7.2. For each connectedGamepad of navigator.[[gamepads]]:
1860                navigator
1861                    .get_connected_gamepad()
1862                    .iter()
1863                    .for_each(|connected_gamepad| {
1864                        // Step 7.2.1. Set connectedGamepad.[[exposed]] to true.
1865                        connected_gamepad.set_exposed(true);
1866                        // Step 7.2.2. Set connectedGamepad.[[timestamp]] to now.
1867                        connected_gamepad.update_timestamp(now);
1868                        // Step 7.2.3. Let document be gamepad's relevant global
1869                        //             object's associated Document; otherwise null.
1870                        // Step 7.2.4. If document is not null and is fully active,
1871                        //             then queue a global task on the gamepad task
1872                        //             source to fire an event named gamepadconnected
1873                        //             at gamepad's relevant global object.
1874                        let trusted_gamepad = Trusted::new(&**connected_gamepad);
1875                        if self.window.Document().is_fully_active() {
1876                            self.window
1877                                .upcast::<GlobalScope>()
1878                                .task_manager()
1879                                .gamepad_task_source()
1880                                .queue(task!(fire_gamepad_connected: move |cx| {
1881                                    let gamepad = trusted_gamepad.root();
1882                                    gamepad.notify_event(cx, GamepadEventType::Connected);
1883                                }));
1884                        }
1885                    });
1886            }
1887        }
1888    }
1889
1890    /// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions>
1891    pub(crate) fn handle_editing_action(
1892        &self,
1893        cx: &mut JSContext,
1894        element: Option<DomRoot<Element>>,
1895        action: EditingActionEvent,
1896    ) -> InputEventResult {
1897        let clipboard_event_type = match action {
1898            EditingActionEvent::Copy => ClipboardEventType::Copy,
1899            EditingActionEvent::Cut => ClipboardEventType::Cut,
1900            EditingActionEvent::Paste => ClipboardEventType::Paste,
1901        };
1902
1903        // The script_triggered flag is set if the action runs because of a script, e.g. document.execCommand()
1904        let script_triggered = false;
1905
1906        // The script_may_access_clipboard flag is set
1907        // if action is paste and the script thread is allowed to read from clipboard or
1908        // if action is copy or cut and the script thread is allowed to modify the clipboard
1909        let script_may_access_clipboard = false;
1910
1911        // Step 1 If the script-triggered flag is set and the script-may-access-clipboard flag is unset
1912        if script_triggered && !script_may_access_clipboard {
1913            return InputEventResult::empty();
1914        }
1915
1916        // Step 2 Fire a clipboard event
1917        let clipboard_event = self.fire_clipboard_event(cx, element.clone(), clipboard_event_type);
1918
1919        // Step 3 If a script doesn't call preventDefault()
1920        // the event will be handled inside target's VirtualMethods::handle_event
1921        let event = clipboard_event.upcast::<Event>();
1922        if !event.IsTrusted() {
1923            return event.flags().into();
1924        }
1925
1926        // Step 4 If the event was canceled, then
1927        if event.DefaultPrevented() {
1928            let event_type = event.Type();
1929            match_domstring_ascii!(event_type,
1930
1931                "copy" => {
1932                    // Step 4.1 Call the write content to the clipboard algorithm,
1933                    // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list.
1934                    if let Some(clipboard_data) = clipboard_event.get_clipboard_data() {
1935                        let drag_data_store =
1936                            clipboard_data.data_store().expect("This shouldn't fail");
1937                        self.write_content_to_the_clipboard(&drag_data_store);
1938                    }
1939                },
1940                "cut" => {
1941                    // Step 4.1 Call the write content to the clipboard algorithm,
1942                    // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list.
1943                    if let Some(clipboard_data) = clipboard_event.get_clipboard_data() {
1944                        let drag_data_store =
1945                            clipboard_data.data_store().expect("This shouldn't fail");
1946                        self.write_content_to_the_clipboard(&drag_data_store);
1947                    }
1948
1949                    // Step 4.2 Fire a clipboard event named clipboardchange
1950                    self.fire_clipboard_event(cx, element, ClipboardEventType::Change);
1951                },
1952                // Step 4.1 Return false.
1953                // Note: This function deviates from the specification a bit by returning
1954                // the `InputEventResult` below.
1955                "paste" => (),
1956                _ => (),
1957            )
1958        }
1959
1960        // Step 5: Return true from the action.
1961        // In this case we are returning the `InputEventResult` instead of true or false.
1962        event.flags().into()
1963    }
1964
1965    /// <https://www.w3.org/TR/clipboard-apis/#fire-a-clipboard-event>
1966    pub(crate) fn fire_clipboard_event(
1967        &self,
1968        cx: &mut JSContext,
1969        target: Option<DomRoot<Element>>,
1970        clipboard_event_type: ClipboardEventType,
1971    ) -> DomRoot<ClipboardEvent> {
1972        let clipboard_event = ClipboardEvent::new(
1973            &self.window,
1974            None,
1975            clipboard_event_type.as_str().into(),
1976            EventBubbles::Bubbles,
1977            EventCancelable::Cancelable,
1978            None,
1979            CanGc::from_cx(cx),
1980        );
1981
1982        // Step 1 Let clear_was_called be false
1983        // Step 2 Let types_to_clear an empty list
1984        let mut drag_data_store = DragDataStore::new();
1985
1986        // Step 4 let clipboard-entry be the sequence number of clipboard content, null if the OS doesn't support it.
1987
1988        // Step 5 let trusted be true if the event is generated by the user agent, false otherwise
1989        let trusted = true;
1990
1991        // Step 6 if the context is editable:
1992        let target = target
1993            .map(DomRoot::upcast)
1994            .unwrap_or_else(|| self.target_for_events_following_focus());
1995
1996        // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70
1997        // Step 7
1998        match clipboard_event_type {
1999            ClipboardEventType::Copy | ClipboardEventType::Cut => {
2000                // Step 7.2.1
2001                drag_data_store.set_mode(Mode::ReadWrite);
2002            },
2003            ClipboardEventType::Paste => {
2004                let (callback, receiver) =
2005                    GenericCallback::new_blocking().expect("Could not create callback");
2006                self.window.send_to_embedder(EmbedderMsg::GetClipboardText(
2007                    self.window.webview_id(),
2008                    callback,
2009                ));
2010                let text_contents = receiver
2011                    .recv()
2012                    .map(Result::unwrap_or_default)
2013                    .unwrap_or_default();
2014
2015                // Step 7.1.1
2016                drag_data_store.set_mode(Mode::ReadOnly);
2017                // Step 7.1.2 If trusted or the implementation gives script-generated events access to the clipboard
2018                if trusted {
2019                    // Step 7.1.2.1 For each clipboard-part on the OS clipboard:
2020
2021                    // Step 7.1.2.1.1 If clipboard-part contains plain text, then
2022                    let data = DOMString::from(text_contents);
2023                    let type_ = DOMString::from("text/plain");
2024                    let _ = drag_data_store.add(Kind::Text { data, type_ });
2025
2026                    // Step 7.1.2.1.2 TODO If clipboard-part represents file references, then for each file reference
2027                    // Step 7.1.2.1.3 TODO If clipboard-part contains HTML- or XHTML-formatted text then
2028
2029                    // Step 7.1.3 Update clipboard-event-data’s files to match clipboard-event-data’s items
2030                    // Step 7.1.4 Update clipboard-event-data’s types to match clipboard-event-data’s items
2031                }
2032            },
2033            ClipboardEventType::Change => (),
2034        }
2035
2036        // Step 3
2037        let clipboard_event_data = DataTransfer::new(
2038            &self.window,
2039            Rc::new(RefCell::new(Some(drag_data_store))),
2040            CanGc::from_cx(cx),
2041        );
2042
2043        // Step 8
2044        clipboard_event.set_clipboard_data(Some(&clipboard_event_data));
2045
2046        // Step 9
2047        let event = clipboard_event.upcast::<Event>();
2048        event.set_trusted(trusted);
2049
2050        // Step 10 Set event’s composed to true.
2051        event.set_composed(true);
2052
2053        // Step 11
2054        event.dispatch(cx, &target, false);
2055
2056        DomRoot::from(clipboard_event)
2057    }
2058
2059    /// <https://www.w3.org/TR/clipboard-apis/#write-content-to-the-clipboard>
2060    fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) {
2061        // Step 1
2062        if drag_data_store.list_len() > 0 {
2063            // Step 1.1 Clear the clipboard.
2064            self.window
2065                .send_to_embedder(EmbedderMsg::ClearClipboard(self.window.webview_id()));
2066            // Step 1.2
2067            for item in drag_data_store.iter_item_list() {
2068                match item {
2069                    Kind::Text { data, .. } => {
2070                        // Step 1.2.1.1 Ensure encoding is correct per OS and locale conventions
2071                        // Step 1.2.1.2 Normalize line endings according to platform conventions
2072                        // Step 1.2.1.3
2073                        self.window.send_to_embedder(EmbedderMsg::SetClipboardText(
2074                            self.window.webview_id(),
2075                            data.to_string(),
2076                        ));
2077                    },
2078                    Kind::File { .. } => {
2079                        // Step 1.2.2 If data is of a type listed in the mandatory data types list, then
2080                        // Step 1.2.2.1 Place part on clipboard with the appropriate OS clipboard format description
2081                        // Step 1.2.3 Else this is left to the implementation
2082                    },
2083                }
2084            }
2085        } else {
2086            // Step 2.1
2087            if drag_data_store.clear_was_called {
2088                // Step 2.1.1 If types-to-clear list is empty, clear the clipboard
2089                self.window
2090                    .send_to_embedder(EmbedderMsg::ClearClipboard(self.window.webview_id()));
2091                // Step 2.1.2 Else remove the types in the list from the clipboard
2092                // As of now this can't be done with Arboard, and it's possible that will be removed from the spec
2093            }
2094        }
2095    }
2096
2097    /// Handle a scroll event triggered by user interactions from the embedder.
2098    /// <https://drafts.csswg.org/cssom-view/#scrolling-events>
2099    #[expect(unsafe_code)]
2100    pub(crate) fn handle_embedder_scroll_event(&self, scrolled_node: ExternalScrollId) {
2101        // If it is a viewport scroll.
2102        let document = self.window.Document();
2103        if scrolled_node.is_root() {
2104            document.handle_viewport_scroll_event();
2105        } else {
2106            // Otherwise, check whether it is for a relevant element within the document. For a `::before` or `::after`
2107            // pseudo element we follow Gecko or Chromium's behavior to ensure that the event reaches the originating
2108            // node.
2109            let node_id = node_id_from_scroll_id(scrolled_node.0 as usize);
2110            let node = unsafe {
2111                node::from_untrusted_node_address(UntrustedNodeAddress::from_id(node_id))
2112            };
2113            let Some(element) = node
2114                .inclusive_ancestors(ShadowIncluding::Yes)
2115                .find_map(DomRoot::downcast::<Element>)
2116            else {
2117                return;
2118            };
2119
2120            element.handle_scroll_event();
2121        }
2122    }
2123
2124    /// <https://w3c.github.io/uievents/#keydown>
2125    ///
2126    /// > If the key is the Enter or (Space) key and the current focus is on a state-changing element,
2127    /// > the default action MUST be to dispatch a click event, and a DOMActivate event if that event
2128    /// > type is supported by the user agent.
2129    pub(crate) fn maybe_dispatch_simulated_click(
2130        &self,
2131        cx: &mut JSContext,
2132        node: &Node,
2133        event: &KeyboardEvent,
2134    ) -> bool {
2135        if event.key() != Key::Named(NamedKey::Enter) && event.original_code() != Some(Code::Space)
2136        {
2137            return false;
2138        }
2139
2140        // Check whether this node is a state-changing element. Note that the specification doesn't
2141        // seem to have a good definition of what "state-changing" means, so we merely check to
2142        // see if the element is activatable here.
2143        if node
2144            .downcast::<Element>()
2145            .and_then(Element::as_maybe_activatable)
2146            .is_none()
2147        {
2148            return false;
2149        }
2150
2151        node.fire_synthetic_pointer_event_not_trusted(cx, atom!("click"));
2152        true
2153    }
2154
2155    pub(crate) fn run_default_keyboard_event_handler(
2156        &self,
2157        cx: &mut js::context::JSContext,
2158        node: &Node,
2159        event: &KeyboardEvent,
2160    ) {
2161        if event.upcast::<Event>().type_() != atom!("keydown") {
2162            return;
2163        }
2164
2165        if self.maybe_dispatch_simulated_click(cx, node, event) {
2166            return;
2167        }
2168
2169        if self.maybe_handle_accesskey(cx, event) {
2170            return;
2171        }
2172
2173        let mut is_space = false;
2174        let scroll = match event.key() {
2175            Key::Named(NamedKey::ArrowDown) => KeyboardScroll::Down,
2176            Key::Named(NamedKey::ArrowLeft) => KeyboardScroll::Left,
2177            Key::Named(NamedKey::ArrowRight) => KeyboardScroll::Right,
2178            Key::Named(NamedKey::ArrowUp) => KeyboardScroll::Up,
2179            Key::Named(NamedKey::End) => KeyboardScroll::End,
2180            Key::Named(NamedKey::Home) => KeyboardScroll::Home,
2181            Key::Named(NamedKey::PageDown) => KeyboardScroll::PageDown,
2182            Key::Named(NamedKey::PageUp) => KeyboardScroll::PageUp,
2183            Key::Character(string) if &string == " " => {
2184                is_space = true;
2185                if event.modifiers().contains(Modifiers::SHIFT) {
2186                    KeyboardScroll::PageUp
2187                } else {
2188                    KeyboardScroll::PageDown
2189                }
2190            },
2191            Key::Named(NamedKey::Tab) => {
2192                // From <https://w3c.github.io/uievents/#keydown>:
2193                //
2194                // > If the key is the Tab key, the default action MUST be to shift the document focus
2195                // > from the currently focused element (if any) to the new focused element, as
2196                // > described in Focus Event Types
2197                self.window
2198                    .Document()
2199                    .focus_handler()
2200                    .sequential_focus_navigation_via_keyboard_event(cx, event);
2201                return;
2202            },
2203            _ => return,
2204        };
2205
2206        if !event.modifiers().is_empty() && !is_space {
2207            return;
2208        }
2209
2210        self.do_keyboard_scroll(cx, scroll);
2211    }
2212
2213    pub(crate) fn do_keyboard_scroll(&self, cx: &mut JSContext, scroll: KeyboardScroll) {
2214        let scroll_axis = match scroll {
2215            KeyboardScroll::Left | KeyboardScroll::Right => ScrollingBoxAxis::X,
2216            _ => ScrollingBoxAxis::Y,
2217        };
2218
2219        let document = self.window.Document();
2220        let mut scrolling_box = document
2221            .focus_handler()
2222            .focused_area()
2223            .element()
2224            .or(self.most_recently_clicked_element.get().as_deref())
2225            .and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive))
2226            .unwrap_or_else(|| {
2227                document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive)
2228            });
2229
2230        while !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
2231            // Always fall back to trying to scroll the entire document.
2232            if scrolling_box.is_viewport() {
2233                break;
2234            }
2235            let parent = scrolling_box.parent().unwrap_or_else(|| {
2236                document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive)
2237            });
2238            scrolling_box = parent;
2239        }
2240
2241        let calculate_current_scroll_offset_and_delta = || {
2242            const LINE_HEIGHT: f32 = 76.0;
2243            const LINE_WIDTH: f32 = 76.0;
2244
2245            let current_scroll_offset = scrolling_box.scroll_position();
2246            (
2247                current_scroll_offset,
2248                match scroll {
2249                    KeyboardScroll::Home => Vector2D::new(0.0, -current_scroll_offset.y),
2250                    KeyboardScroll::End => Vector2D::new(
2251                        0.0,
2252                        -current_scroll_offset.y + scrolling_box.content_size().height -
2253                            scrolling_box.size().height,
2254                    ),
2255                    KeyboardScroll::PageDown => {
2256                        Vector2D::new(0.0, scrolling_box.size().height - 2.0 * LINE_HEIGHT)
2257                    },
2258                    KeyboardScroll::PageUp => {
2259                        Vector2D::new(0.0, 2.0 * LINE_HEIGHT - scrolling_box.size().height)
2260                    },
2261                    KeyboardScroll::Up => Vector2D::new(0.0, -LINE_HEIGHT),
2262                    KeyboardScroll::Down => Vector2D::new(0.0, LINE_HEIGHT),
2263                    KeyboardScroll::Left => Vector2D::new(-LINE_WIDTH, 0.0),
2264                    KeyboardScroll::Right => Vector2D::new(LINE_WIDTH, 0.0),
2265                },
2266            )
2267        };
2268
2269        // If trying to scroll the viewport of this `Window` and this is the root `Document`
2270        // of the `WebView`, then send the srolling operation to the renderer, so that it
2271        // can properly pan any pinch zoom viewport.
2272        let parent_pipeline = self.window.parent_info();
2273        if scrolling_box.is_viewport() && parent_pipeline.is_none() {
2274            let (_, delta) = calculate_current_scroll_offset_and_delta();
2275            self.window
2276                .paint_api()
2277                .scroll_viewport_by_delta(self.window.webview_id(), delta);
2278        }
2279
2280        // If this is the viewport and we cannot scroll, try to ask a parent viewport to scroll,
2281        // if we are inside an `<iframe>`.
2282        if !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
2283            assert!(scrolling_box.is_viewport());
2284
2285            let window_proxy = document.window().window_proxy();
2286            if let Some(iframe) = window_proxy.frame_element() {
2287                // When the `<iframe>` is local (in this ScriptThread), we can
2288                // synchronously chain up the keyboard scrolling event.
2289                let iframe_window = iframe.owner_window();
2290                let mut realm = enter_auto_realm(cx, &*iframe_window);
2291                let cx = &mut realm;
2292                iframe_window
2293                    .Document()
2294                    .event_handler()
2295                    .do_keyboard_scroll(cx, scroll);
2296            } else if let Some(parent_pipeline) = parent_pipeline {
2297                // Otherwise, if we have a parent (presumably from a different origin)
2298                // asynchronously ask the Constellation to forward the event to the parent
2299                // pipeline, if we have one.
2300                document.window().send_to_constellation(
2301                    ScriptToConstellationMessage::ForwardKeyboardScroll(parent_pipeline, scroll),
2302                );
2303            };
2304            return;
2305        }
2306
2307        let (current_scroll_offset, delta) = calculate_current_scroll_offset_and_delta();
2308        scrolling_box.scroll_to(cx, delta + current_scroll_offset, ScrollBehavior::Auto);
2309    }
2310
2311    /// Get or create a pointer ID for the given touch identifier.
2312    /// Returns the pointer ID to use for this touch.
2313    fn get_or_create_pointer_id_for_touch(&self, touch_id: i32) -> i32 {
2314        let mut active_pointer_ids = self.active_pointer_ids.borrow_mut();
2315
2316        if let Some(&pointer_id) = active_pointer_ids.get(&touch_id) {
2317            return pointer_id;
2318        }
2319
2320        let pointer_id = self.next_touch_pointer_id.get();
2321        active_pointer_ids.insert(touch_id, pointer_id);
2322        self.next_touch_pointer_id.set(pointer_id + 1);
2323        pointer_id
2324    }
2325
2326    /// Remove the pointer ID mapping for the given touch identifier.
2327    fn remove_pointer_id_for_touch(&self, touch_id: i32) {
2328        self.active_pointer_ids.borrow_mut().remove(&touch_id);
2329    }
2330
2331    /// Check if this is the primary pointer (for touch events).
2332    /// The first touch to make contact is the primary pointer.
2333    fn is_primary_pointer(&self, pointer_id: i32) -> bool {
2334        // For touch, the primary pointer is the one with the smallest pointer ID
2335        // that is still active.
2336        self.active_pointer_ids
2337            .borrow()
2338            .values()
2339            .min()
2340            .is_some_and(|primary_pointer| *primary_pointer == pointer_id)
2341    }
2342
2343    /// Fire pointerenter events hierarchically from topmost ancestor to target element.
2344    /// Fire pointerleave events hierarchically from target element to topmost ancestor.
2345    /// Used for touch devices that don't support hover.
2346    #[allow(clippy::too_many_arguments)]
2347    fn fire_pointer_event_for_touch(
2348        &self,
2349        cx: &mut js::context::JSContext,
2350        target_element: &Element,
2351        touch: &Touch,
2352        pointer_id: i32,
2353        event_name: &str,
2354        is_primary: bool,
2355        pointer_type: &str,
2356        input_event: &ConstellationInputEvent,
2357        hit_test_result: &HitTestResult,
2358    ) {
2359        // Collect ancestors from target to root
2360        let mut targets: Vec<DomRoot<Node>> = vec![];
2361        let mut current: Option<DomRoot<Node>> = Some(DomRoot::from_ref(target_element.upcast()));
2362        while let Some(node) = current {
2363            targets.push(DomRoot::from_ref(&*node));
2364            current = node.parent_in_flat_tree();
2365        }
2366
2367        // Reverse to dispatch from topmost ancestor to target
2368        if event_name == "pointerenter" {
2369            targets.reverse();
2370        }
2371
2372        for target in targets {
2373            let pointer_event = touch.to_pointer_event(
2374                &self.window,
2375                event_name,
2376                pointer_id,
2377                is_primary,
2378                pointer_type,
2379                input_event.active_keyboard_modifiers,
2380                false,
2381                Some(hit_test_result.point_in_node),
2382                CanGc::from_cx(cx),
2383            );
2384            pointer_event.upcast::<Event>().fire(cx, target.upcast());
2385        }
2386    }
2387
2388    pub(crate) fn has_assigned_access_key(&self, element: &HTMLElement) -> bool {
2389        self.access_key_handlers
2390            .borrow()
2391            .values()
2392            .any(|value| &**value == element)
2393    }
2394
2395    pub(crate) fn unassign_access_key(&self, element: &HTMLElement) {
2396        self.access_key_handlers
2397            .borrow_mut()
2398            .retain(|_, value| &**value != element)
2399    }
2400
2401    pub(crate) fn assign_access_key(&self, element: &HTMLElement, code: Code) {
2402        let mut access_key_handlers = self.access_key_handlers.borrow_mut();
2403        // If an element is already assigned this access key, ignore the request.
2404        access_key_handlers
2405            .entry(code.into())
2406            .or_insert(Dom::from_ref(element));
2407    }
2408
2409    fn maybe_handle_accesskey(
2410        &self,
2411        cx: &mut js::context::JSContext,
2412        event: &KeyboardEvent,
2413    ) -> bool {
2414        #[cfg(target_os = "macos")]
2415        let access_key_modifiers = Modifiers::CONTROL | Modifiers::ALT;
2416        #[cfg(not(target_os = "macos"))]
2417        let access_key_modifiers = Modifiers::SHIFT | Modifiers::ALT;
2418
2419        if event.modifiers() != access_key_modifiers {
2420            return false;
2421        }
2422
2423        let Ok(code) = Code::from_str(&event.Code().str()) else {
2424            return false;
2425        };
2426
2427        let Some(html_element) = self
2428            .access_key_handlers
2429            .borrow()
2430            .get(&code.into())
2431            .map(|html_element| html_element.as_rooted())
2432        else {
2433            return false;
2434        };
2435
2436        // From <https://html.spec.whatwg.org/multipage/#the-accesskey-attribute>:
2437        // > When the user presses the key combination corresponding to the assigned access key for
2438        // > an element, if the element defines a command, the command's Hidden State facet is false
2439        // > (visible), the command's Disabled State facet is also false (enabled), the element is in
2440        // > a document that has a non-null browsing context, and neither the element nor any of its
2441        // > ancestors has a hidden attribute specified, then the user agent must trigger the Action
2442        // > of the command.
2443        let Ok(command) = InteractiveElementCommand::try_from(&*html_element) else {
2444            return false;
2445        };
2446
2447        if command.disabled() || command.hidden() {
2448            return false;
2449        }
2450
2451        let node = html_element.upcast::<Node>();
2452        if !node.is_connected() {
2453            return false;
2454        }
2455
2456        for node in node.inclusive_ancestors_unrooted(cx.no_gc(), ShadowIncluding::Yes) {
2457            if node
2458                .downcast::<HTMLElement>()
2459                .is_some_and(|html_element| html_element.Hidden())
2460            {
2461                return false;
2462            }
2463        }
2464
2465        // This behavior is unspecified, but all browsers do this. When activating the element it is
2466        // focused and scrolled into view.
2467        self.focus_and_scroll_to_element_for_key_event(cx, html_element.upcast());
2468        command.perform_action(cx);
2469        true
2470    }
2471
2472    pub(crate) fn focus_and_scroll_to_element_for_key_event(
2473        &self,
2474        cx: &mut JSContext,
2475        element: &Element,
2476    ) {
2477        element.upcast::<Node>().run_the_focusing_steps(cx, None);
2478        let scroll_axis = ScrollAxisState {
2479            position: ScrollLogicalPosition::Center,
2480            requirement: ScrollRequirement::IfNotVisible,
2481        };
2482        element.scroll_into_view_with_options(
2483            cx,
2484            ScrollBehavior::Auto,
2485            scroll_axis,
2486            scroll_axis,
2487            None,
2488            None,
2489        );
2490    }
2491
2492    /// Check if a pointer ID corresponds to an active pointer.
2493    /// <https://w3c.github.io/pointerevents/#dfn-active-pointer>
2494    pub(crate) fn is_active_pointer(&self, pointer_id: i32) -> bool {
2495        if pointer_id == PointerId::Mouse as i32 {
2496            // Mouse is active when buttons are down
2497            self.mouse_buttons_down.get() > 0
2498        } else {
2499            // Touch pointers are tracked in active_pointer_ids
2500            self.active_pointer_ids
2501                .borrow()
2502                .values()
2503                .any(|&id| id == pointer_id)
2504        }
2505    }
2506
2507    /// Set the pending pointer capture target override for a pointer.
2508    /// <https://w3c.github.io/pointerevents/#setting-pointer-capture>
2509    pub(crate) fn set_pending_pointer_capture(&self, pointer_id: i32, element: &Element) {
2510        self.pending_pointer_capture
2511            .borrow_mut()
2512            .insert(pointer_id, Dom::from_ref(element));
2513    }
2514
2515    /// Clear the pending pointer capture target override for a pointer.
2516    /// <https://w3c.github.io/pointerevents/#releasing-pointer-capture>
2517    pub(crate) fn clear_pending_pointer_capture(&self, pointer_id: i32) {
2518        self.pending_pointer_capture
2519            .borrow_mut()
2520            .remove(&pointer_id);
2521    }
2522
2523    /// Get the pending pointer capture target override for a pointer.
2524    pub(crate) fn get_pending_pointer_capture(&self, pointer_id: i32) -> Option<DomRoot<Element>> {
2525        self.pending_pointer_capture
2526            .borrow()
2527            .get(&pointer_id)
2528            .map(|el| DomRoot::from_ref(&**el))
2529    }
2530
2531    /// Check if an element has pointer capture for a given pointer ID.
2532    /// <https://w3c.github.io/pointerevents/#dom-element-haspointercapture>
2533    pub(crate) fn has_pointer_capture(&self, pointer_id: i32, element: &Element) -> bool {
2534        self.pending_pointer_capture
2535            .borrow()
2536            .get(&pointer_id)
2537            .is_some_and(|el| &**el == element)
2538    }
2539
2540    /// Get the current pointer capture target for event dispatch.
2541    /// Returns the capture target if set and connected, otherwise None.
2542    /// This returns the actual/current target (pointer_capture_target),
2543    /// not the pending target that will be applied after process_pending.
2544    fn get_pointer_capture_target(&self, pointer_id: i32) -> Option<DomRoot<Element>> {
2545        self.pointer_capture_target
2546            .borrow()
2547            .get(&pointer_id)
2548            .map(|el| DomRoot::from_ref(&**el))
2549            .filter(|el| el.upcast::<Node>().is_connected())
2550    }
2551
2552    /// Check if the capture target is disconnected and release it if so.
2553    /// This fires lostpointercapture at the document per spec.
2554    /// Must be called before firing any pointer event.
2555    /// Returns true if a disconnected capture was released.
2556    /// <https://w3c.github.io/pointerevents/#process-pending-pointer-capture>
2557    fn release_disconnected_pointer_capture(
2558        &self,
2559        cx: &mut JSContext,
2560        pointer_id: i32,
2561        pointer_type: &str,
2562        is_primary: bool,
2563    ) -> bool {
2564        let capture_target = self
2565            .pointer_capture_target
2566            .borrow()
2567            .get(&pointer_id)
2568            .map(|el| DomRoot::from_ref(&**el));
2569        if let Some(capture_element) = capture_target &&
2570            !capture_element.upcast::<Node>().is_connected()
2571        {
2572            // Fire lostpointercapture at the document, not the disconnected element.
2573            let document = self.window.Document();
2574            self.fire_pointer_capture_event_at_target(
2575                cx,
2576                "lostpointercapture",
2577                pointer_id,
2578                pointer_type,
2579                is_primary,
2580                document.upcast::<EventTarget>(),
2581            );
2582            // Clear both pending and current capture
2583            self.pending_pointer_capture
2584                .borrow_mut()
2585                .remove(&pointer_id);
2586            self.pointer_capture_target.borrow_mut().remove(&pointer_id);
2587            return true;
2588        }
2589        false
2590    }
2591
2592    /// Fire a gotpointercapture or lostpointercapture event at an Element.
2593    /// <https://w3c.github.io/pointerevents/#the-gotpointercapture-and-lostpointercapture-events>
2594    fn fire_pointer_capture_event(
2595        &self,
2596        cx: &mut JSContext,
2597        event_type: &str,
2598        pointer_id: i32,
2599        pointer_type: &str,
2600        is_primary: bool,
2601        target: &Element,
2602    ) {
2603        self.fire_pointer_capture_event_at_target(
2604            cx,
2605            event_type,
2606            pointer_id,
2607            pointer_type,
2608            is_primary,
2609            target.upcast::<EventTarget>(),
2610        );
2611    }
2612
2613    /// Fire a pointer capture event at a specific EventTarget (e.g., the document).
2614    fn fire_pointer_capture_event_at_target(
2615        &self,
2616        cx: &mut JSContext,
2617        event_type: &str,
2618        pointer_id: i32,
2619        pointer_type: &str,
2620        is_primary: bool,
2621        target: &EventTarget,
2622    ) {
2623        let pointer_event = PointerEvent::new(
2624            &self.window,
2625            event_type.into(),
2626            EventBubbles::Bubbles,
2627            EventCancelable::NotCancelable,
2628            Some(&self.window),
2629            0,
2630            Point2D::new(0, 0),
2631            Point2D::new(0, 0),
2632            Point2D::new(0, 0),
2633            Modifiers::empty(),
2634            0,
2635            0,
2636            None,
2637            None,
2638            pointer_id,
2639            1,
2640            1,
2641            0.0,
2642            0.0,
2643            0,
2644            0,
2645            0,
2646            PI / 2.0,
2647            0.0,
2648            DOMString::from(pointer_type),
2649            is_primary,
2650            vec![],
2651            vec![],
2652            CanGc::from_cx(cx),
2653        );
2654        pointer_event.upcast::<Event>().set_composed(true);
2655        pointer_event.upcast::<Event>().fire(cx, target);
2656    }
2657
2658    /// Fire a single boundary `PointerEvent` (`pointerout`/`pointerleave`/
2659    /// `pointerover`/`pointerenter`) at `target`, with `related_target` set to
2660    /// the element being entered from or left to.
2661    #[expect(clippy::too_many_arguments)]
2662    fn fire_pointer_boundary_event(
2663        &self,
2664        cx: &mut JSContext,
2665        event_type: &str,
2666        bubbles: EventBubbles,
2667        pointer_id: i32,
2668        pointer_type: &str,
2669        is_primary: bool,
2670        target: &Element,
2671        related_target: Option<&Element>,
2672    ) {
2673        let pointer_event = PointerEvent::new(
2674            &self.window,
2675            event_type.into(),
2676            bubbles,
2677            EventCancelable::NotCancelable,
2678            Some(&self.window),
2679            0,
2680            Point2D::new(0, 0),
2681            Point2D::new(0, 0),
2682            Point2D::new(0, 0),
2683            Modifiers::empty(),
2684            -1, // button: -1 for boundary events
2685            self.mouse_buttons_down.get() as u16,
2686            related_target.map(|el| el.upcast::<EventTarget>()),
2687            None,
2688            pointer_id,
2689            1,
2690            1,
2691            0.0,
2692            0.0,
2693            0,
2694            0,
2695            0,
2696            PI / 2.0,
2697            0.0,
2698            DOMString::from(pointer_type),
2699            is_primary,
2700            vec![],
2701            vec![],
2702            CanGc::from_cx(cx),
2703        );
2704        pointer_event.upcast::<Event>().set_composed(true);
2705        pointer_event.upcast::<Event>().fire(cx, target.upcast());
2706    }
2707
2708    /// Fire the pointer boundary events that accompany a pointer-capture
2709    /// transition from `old_target` to `new_target` for hoverable pointers.
2710    /// Per spec, only fired for pointer types that support hover (mouse, pen
2711    /// with hover); touch is skipped.
2712    fn fire_pointer_capture_boundary_transition(
2713        &self,
2714        cx: &mut JSContext,
2715        pointer_id: i32,
2716        pointer_type: &str,
2717        is_primary: bool,
2718        old_target: &Element,
2719        new_target: &Element,
2720    ) {
2721        if pointer_type != "mouse" {
2722            return;
2723        }
2724        if old_target == new_target {
2725            return;
2726        }
2727        self.fire_pointer_boundary_event(
2728            cx,
2729            "pointerout",
2730            EventBubbles::Bubbles,
2731            pointer_id,
2732            pointer_type,
2733            is_primary,
2734            old_target,
2735            Some(new_target),
2736        );
2737        self.fire_pointer_boundary_event(
2738            cx,
2739            "pointerleave",
2740            EventBubbles::DoesNotBubble,
2741            pointer_id,
2742            pointer_type,
2743            is_primary,
2744            old_target,
2745            Some(new_target),
2746        );
2747        self.fire_pointer_boundary_event(
2748            cx,
2749            "pointerover",
2750            EventBubbles::Bubbles,
2751            pointer_id,
2752            pointer_type,
2753            is_primary,
2754            new_target,
2755            Some(old_target),
2756        );
2757        self.fire_pointer_boundary_event(
2758            cx,
2759            "pointerenter",
2760            EventBubbles::DoesNotBubble,
2761            pointer_id,
2762            pointer_type,
2763            is_primary,
2764            new_target,
2765            Some(old_target),
2766        );
2767    }
2768
2769    /// Implicitly release pointer capture when pointer is lifted or canceled.
2770    /// <https://w3c.github.io/pointerevents/#implicit-release-of-pointer-capture>
2771    fn implicit_release_pointer_capture(
2772        &self,
2773        cx: &mut JSContext,
2774        pointer_id: i32,
2775        pointer_type: &str,
2776        is_primary: bool,
2777    ) {
2778        let capture_element = self
2779            .pointer_capture_target
2780            .borrow()
2781            .get(&pointer_id)
2782            .map(|el| DomRoot::from_ref(&**el));
2783        if let Some(capture_element) = capture_element {
2784            if capture_element.upcast::<Node>().is_connected() {
2785                self.fire_pointer_capture_event(
2786                    cx,
2787                    "lostpointercapture",
2788                    pointer_id,
2789                    pointer_type,
2790                    is_primary,
2791                    &capture_element,
2792                );
2793                // Boundary events: transition hover from released capture
2794                // target back to the actual hover target.
2795                if let Some(hover_target) = self.current_hover_target.get() {
2796                    self.fire_pointer_capture_boundary_transition(
2797                        cx,
2798                        pointer_id,
2799                        pointer_type,
2800                        is_primary,
2801                        &capture_element,
2802                        &hover_target,
2803                    );
2804                }
2805            } else {
2806                let document = self.window.Document();
2807                self.fire_pointer_capture_event_at_target(
2808                    cx,
2809                    "lostpointercapture",
2810                    pointer_id,
2811                    pointer_type,
2812                    is_primary,
2813                    document.upcast::<EventTarget>(),
2814                );
2815            }
2816        }
2817        self.pending_pointer_capture
2818            .borrow_mut()
2819            .remove(&pointer_id);
2820        self.pointer_capture_target.borrow_mut().remove(&pointer_id);
2821    }
2822
2823    /// Process pending pointer capture before dispatching a pointer event.
2824    /// Fires gotpointercapture/lostpointercapture as needed.
2825    /// <https://w3c.github.io/pointerevents/#process-pending-pointer-capture>
2826    fn process_pending_pointer_capture(
2827        &self,
2828        cx: &mut JSContext,
2829        pointer_id: i32,
2830        pointer_type: &str,
2831        is_primary: bool,
2832    ) {
2833        let pending = self
2834            .pending_pointer_capture
2835            .borrow()
2836            .get(&pointer_id)
2837            .map(|el| DomRoot::from_ref(&**el));
2838        let current = self
2839            .pointer_capture_target
2840            .borrow()
2841            .get(&pointer_id)
2842            .map(|el| DomRoot::from_ref(&**el));
2843
2844        // Disconnected capture targets are handled by release_disconnected_pointer_capture
2845        // before any pointer event fires; by the time we get here they have been released.
2846        let pending_connected = pending
2847            .as_ref()
2848            .is_some_and(|el| el.upcast::<Node>().is_connected());
2849
2850        // If the pointer is no longer active (e.g., last button just released), we should
2851        // not fire gotpointercapture for new captures.
2852        let pointer_is_active = self.is_active_pointer(pointer_id);
2853
2854        match (&pending, &current) {
2855            (Some(pending_el), None) if pending_connected => {
2856                if pointer_is_active {
2857                    // Boundary events: transition hover from current hover target
2858                    // to the new capture target, before firing gotpointercapture.
2859                    if let Some(hover_target) = self.current_hover_target.get() {
2860                        self.fire_pointer_capture_boundary_transition(
2861                            cx,
2862                            pointer_id,
2863                            pointer_type,
2864                            is_primary,
2865                            &hover_target,
2866                            pending_el,
2867                        );
2868                    }
2869                    self.fire_pointer_capture_event(
2870                        cx,
2871                        "gotpointercapture",
2872                        pointer_id,
2873                        pointer_type,
2874                        is_primary,
2875                        pending_el,
2876                    );
2877                    self.pointer_capture_target
2878                        .borrow_mut()
2879                        .insert(pointer_id, Dom::from_ref(pending_el));
2880                } else {
2881                    self.pending_pointer_capture
2882                        .borrow_mut()
2883                        .remove(&pointer_id);
2884                }
2885            },
2886            (Some(pending_el), Some(current_el))
2887                if pending_connected && pending_el != current_el =>
2888            {
2889                self.fire_pointer_capture_event(
2890                    cx,
2891                    "lostpointercapture",
2892                    pointer_id,
2893                    pointer_type,
2894                    is_primary,
2895                    current_el,
2896                );
2897                if pointer_is_active {
2898                    self.fire_pointer_capture_boundary_transition(
2899                        cx,
2900                        pointer_id,
2901                        pointer_type,
2902                        is_primary,
2903                        current_el,
2904                        pending_el,
2905                    );
2906                    self.fire_pointer_capture_event(
2907                        cx,
2908                        "gotpointercapture",
2909                        pointer_id,
2910                        pointer_type,
2911                        is_primary,
2912                        pending_el,
2913                    );
2914                    self.pointer_capture_target
2915                        .borrow_mut()
2916                        .insert(pointer_id, Dom::from_ref(pending_el));
2917                } else {
2918                    self.pending_pointer_capture
2919                        .borrow_mut()
2920                        .remove(&pointer_id);
2921                    self.pointer_capture_target.borrow_mut().remove(&pointer_id);
2922                }
2923            },
2924            (None, Some(current_el)) | (Some(_), Some(current_el)) if !pending_connected => {
2925                self.fire_pointer_capture_event(
2926                    cx,
2927                    "lostpointercapture",
2928                    pointer_id,
2929                    pointer_type,
2930                    is_primary,
2931                    current_el,
2932                );
2933                // Boundary events: transition hover from released capture
2934                // target back to the actual hover target.
2935                if let Some(hover_target) = self.current_hover_target.get() {
2936                    self.fire_pointer_capture_boundary_transition(
2937                        cx,
2938                        pointer_id,
2939                        pointer_type,
2940                        is_primary,
2941                        current_el,
2942                        &hover_target,
2943                    );
2944                }
2945                self.pointer_capture_target.borrow_mut().remove(&pointer_id);
2946                if !pending_connected {
2947                    self.pending_pointer_capture
2948                        .borrow_mut()
2949                        .remove(&pointer_id);
2950                }
2951            },
2952            _ => {},
2953        }
2954    }
2955}
2956
2957pub(crate) fn character_to_code(character: char) -> Option<Code> {
2958    Some(match character.to_ascii_lowercase() {
2959        '`' => Code::Backquote,
2960        '\\' => Code::Backslash,
2961        '[' | '{' => Code::BracketLeft,
2962        ']' | '}' => Code::BracketRight,
2963        ',' | '<' => Code::Comma,
2964        '0' => Code::Digit0,
2965        '1' => Code::Digit1,
2966        '2' => Code::Digit2,
2967        '3' => Code::Digit3,
2968        '4' => Code::Digit4,
2969        '5' => Code::Digit5,
2970        '6' => Code::Digit6,
2971        '7' => Code::Digit7,
2972        '8' => Code::Digit8,
2973        '9' => Code::Digit9,
2974        '=' => Code::Equal,
2975        'a' => Code::KeyA,
2976        'b' => Code::KeyB,
2977        'c' => Code::KeyC,
2978        'd' => Code::KeyD,
2979        'e' => Code::KeyE,
2980        'f' => Code::KeyF,
2981        'g' => Code::KeyG,
2982        'h' => Code::KeyH,
2983        'i' => Code::KeyI,
2984        'j' => Code::KeyJ,
2985        'k' => Code::KeyK,
2986        'l' => Code::KeyL,
2987        'm' => Code::KeyM,
2988        'n' => Code::KeyN,
2989        'o' => Code::KeyO,
2990        'p' => Code::KeyP,
2991        'q' => Code::KeyQ,
2992        'r' => Code::KeyR,
2993        's' => Code::KeyS,
2994        't' => Code::KeyT,
2995        'u' => Code::KeyU,
2996        'v' => Code::KeyV,
2997        'w' => Code::KeyW,
2998        'x' => Code::KeyX,
2999        'y' => Code::KeyY,
3000        'z' => Code::KeyZ,
3001        '-' => Code::Minus,
3002        '.' => Code::Period,
3003        '\'' | '"' => Code::Quote,
3004        ';' => Code::Semicolon,
3005        '/' => Code::Slash,
3006        ' ' => Code::Space,
3007        _ => return None,
3008    })
3009}