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