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