Skip to main content

script/dom/document/
document_event_handler.rs

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