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