Skip to main content

script/dom/document/
document_event_handler.rs

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