Skip to main content

script/dom/document/
document_event_handler.rs

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