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