script/dom/
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::time::{Duration, Instant};
11
12use base::generic_channel::GenericCallback;
13use constellation_traits::{KeyboardScroll, ScriptToConstellationMessage};
14use embedder_traits::{
15    Cursor, EditingActionEvent, EmbedderMsg, ImeEvent, InputEvent, InputEventAndId,
16    InputEventResult, KeyboardEvent as EmbedderKeyboardEvent, MouseButton, MouseButtonAction,
17    MouseButtonEvent, MouseLeftViewportEvent, ScrollEvent, TouchEvent as EmbedderTouchEvent,
18    TouchEventType, TouchId, UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent,
19};
20#[cfg(feature = "gamepad")]
21use embedder_traits::{
22    GamepadEvent as EmbedderGamepadEvent, GamepadSupportedHapticEffects, GamepadUpdateType,
23};
24use euclid::{Point2D, Vector2D};
25use js::jsapi::JSAutoRealm;
26use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey};
27use layout_api::{ScrollContainerQueryFlags, node_id_from_scroll_id};
28use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
29use script_bindings::codegen::GenericBindings::EventBinding::EventMethods;
30use script_bindings::codegen::GenericBindings::NavigatorBinding::NavigatorMethods;
31use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
32use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
33use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods;
34use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods};
35use script_bindings::inheritance::Castable;
36use script_bindings::match_domstring_ascii;
37use script_bindings::num::Finite;
38use script_bindings::reflector::DomObject;
39use script_bindings::root::{Dom, DomRoot, DomSlice};
40use script_bindings::script_runtime::CanGc;
41use script_bindings::str::DOMString;
42use script_traits::ConstellationInputEvent;
43use servo_config::pref;
44use style_traits::CSSPixel;
45
46use crate::dom::bindings::cell::DomRefCell;
47use crate::dom::bindings::refcounted::Trusted;
48use crate::dom::bindings::root::MutNullableDom;
49use crate::dom::clipboardevent::ClipboardEventType;
50use crate::dom::document::{FireMouseEventType, FocusInitiator};
51use crate::dom::event::{EventBubbles, EventCancelable, EventComposed, EventFlags};
52#[cfg(feature = "gamepad")]
53use crate::dom::gamepad::gamepad::{Gamepad, contains_user_gesture};
54#[cfg(feature = "gamepad")]
55use crate::dom::gamepad::gamepadevent::GamepadEventType;
56use crate::dom::inputevent::HitTestResult;
57use crate::dom::node::{self, Node, NodeTraits, ShadowIncluding};
58use crate::dom::pointerevent::PointerId;
59use crate::dom::scrolling_box::ScrollingBoxAxis;
60use crate::dom::types::{
61    ClipboardEvent, CompositionEvent, DataTransfer, Element, Event, EventTarget, GlobalScope,
62    HTMLAnchorElement, KeyboardEvent, MouseEvent, PointerEvent, Touch, TouchEvent, TouchList,
63    WheelEvent, Window,
64};
65use crate::drag_data_store::{DragDataStore, Kind, Mode};
66use crate::realms::enter_realm;
67
68/// A data structure used for tracking the current click count. This can be
69/// reset to 0 if a mouse button event happens at a sufficient distance or time
70/// from the previous one.
71///
72/// From <https://w3c.github.io/uievents/#current-click-count>:
73/// > Implementations MUST maintain the current click count when generating mouse
74/// > events. This MUST be a non-negative integer indicating the number of consecutive
75/// > clicks of a pointing device button within a specific time. The delay after which
76/// > the count resets is specific to the environment configuration.
77#[derive(Default, JSTraceable, MallocSizeOf)]
78struct ClickCountingInfo {
79    time: Option<Instant>,
80    #[no_trace]
81    point: Option<Point2D<f32, CSSPixel>>,
82    #[no_trace]
83    button: Option<MouseButton>,
84    count: usize,
85}
86
87impl ClickCountingInfo {
88    fn reset_click_count_if_necessary(
89        &mut self,
90        button: MouseButton,
91        point_in_frame: Point2D<f32, CSSPixel>,
92    ) {
93        let (Some(previous_button), Some(previous_point), Some(previous_time)) =
94            (self.button, self.point, self.time)
95        else {
96            assert_eq!(self.count, 0);
97            return;
98        };
99
100        let double_click_timeout =
101            Duration::from_millis(pref!(dom_document_dblclick_timeout) as u64);
102        let double_click_distance_threshold = pref!(dom_document_dblclick_dist) as u64;
103
104        // Calculate distance between this click and the previous click.
105        let line = point_in_frame - previous_point;
106        let distance = (line.dot(line) as f64).sqrt();
107        if previous_button != button ||
108            Instant::now().duration_since(previous_time) > double_click_timeout ||
109            distance > double_click_distance_threshold as f64
110        {
111            self.count = 0;
112            self.time = None;
113            self.point = None;
114        }
115    }
116
117    fn increment_click_count(
118        &mut self,
119        button: MouseButton,
120        point: Point2D<f32, CSSPixel>,
121    ) -> usize {
122        self.time = Some(Instant::now());
123        self.point = Some(point);
124        self.button = Some(button);
125        self.count += 1;
126        self.count
127    }
128}
129
130/// The [`DocumentEventHandler`] is a structure responsible for handling input events for
131/// the [`crate::Document`] and storing data related to event handling. It exists to
132/// decrease the size of the [`crate::Document`] structure.
133#[derive(JSTraceable, MallocSizeOf)]
134#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
135pub(crate) struct DocumentEventHandler {
136    /// The [`Window`] element for this [`DocumentEventHandler`].
137    window: Dom<Window>,
138    /// Pending input events, to be handled at the next rendering opportunity.
139    #[no_trace]
140    #[ignore_malloc_size_of = "InputEvent contains data from outside crates"]
141    pending_input_events: DomRefCell<Vec<ConstellationInputEvent>>,
142    /// The index of the last mouse move event in the pending input events queue.
143    mouse_move_event_index: DomRefCell<Option<usize>>,
144    /// <https://w3c.github.io/uievents/#event-type-dblclick>
145    click_counting_info: DomRefCell<ClickCountingInfo>,
146    #[no_trace]
147    last_mouse_button_down_point: Cell<Option<Point2D<f32, CSSPixel>>>,
148    /// The element that is currently hovered by the cursor.
149    current_hover_target: MutNullableDom<Element>,
150    /// The element that was most recently clicked.
151    most_recently_clicked_element: MutNullableDom<Element>,
152    /// The most recent mouse movement point, used for processing `mouseleave` events.
153    #[no_trace]
154    most_recent_mousemove_point: Cell<Option<Point2D<f32, CSSPixel>>>,
155    /// The currently set [`Cursor`] or `None` if the `Document` isn't being hovered
156    /// by the cursor.
157    #[no_trace]
158    current_cursor: Cell<Option<Cursor>>,
159    /// <http://w3c.github.io/touch-events/#dfn-active-touch-point>
160    active_touch_points: DomRefCell<Vec<Dom<Touch>>>,
161    /// The active keyboard modifiers for the WebView. This is updated when receiving any input event.
162    #[no_trace]
163    active_keyboard_modifiers: Cell<Modifiers>,
164}
165
166impl DocumentEventHandler {
167    pub(crate) fn new(window: &Window) -> Self {
168        Self {
169            window: Dom::from_ref(window),
170            pending_input_events: Default::default(),
171            mouse_move_event_index: Default::default(),
172            click_counting_info: Default::default(),
173            last_mouse_button_down_point: Default::default(),
174            current_hover_target: Default::default(),
175            most_recently_clicked_element: Default::default(),
176            most_recent_mousemove_point: Default::default(),
177            current_cursor: Default::default(),
178            active_touch_points: Default::default(),
179            active_keyboard_modifiers: Default::default(),
180        }
181    }
182
183    /// Note a pending input event, to be processed at the next `update_the_rendering` task.
184    pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) {
185        let mut pending_input_events = self.pending_input_events.borrow_mut();
186        if matches!(event.event.event, InputEvent::MouseMove(..)) {
187            // First try to replace any existing mouse move event.
188            if let Some(mouse_move_event) = self
189                .mouse_move_event_index
190                .borrow()
191                .and_then(|index| pending_input_events.get_mut(index))
192            {
193                *mouse_move_event = event;
194                return;
195            }
196
197            *self.mouse_move_event_index.borrow_mut() = Some(pending_input_events.len());
198        }
199
200        pending_input_events.push(event);
201    }
202
203    /// Whether or not this [`Document`] has any pending input events to be processed during
204    /// "update the rendering."
205    pub(crate) fn has_pending_input_events(&self) -> bool {
206        !self.pending_input_events.borrow().is_empty()
207    }
208
209    pub(crate) fn alternate_action_keyboard_modifier_active(&self) -> bool {
210        #[cfg(target_os = "macos")]
211        {
212            self.active_keyboard_modifiers
213                .get()
214                .contains(Modifiers::META)
215        }
216        #[cfg(not(target_os = "macos"))]
217        {
218            self.active_keyboard_modifiers
219                .get()
220                .contains(Modifiers::CONTROL)
221        }
222    }
223
224    pub(crate) fn handle_pending_input_events(&self, can_gc: CanGc) {
225        let _realm = enter_realm(&*self.window);
226
227        // Reset the mouse event index.
228        *self.mouse_move_event_index.borrow_mut() = None;
229        let pending_input_events = mem::take(&mut *self.pending_input_events.borrow_mut());
230
231        for event in pending_input_events {
232            self.active_keyboard_modifiers
233                .set(event.active_keyboard_modifiers);
234
235            // TODO: For some of these we still aren't properly calculating whether or not
236            // the event was handled or if `preventDefault()` was called on it. Each of
237            // these cases needs to be examined and some of them either fire more than one
238            // event or fire events later. We have to make a good decision about what to
239            // return to the embedder when that happens.
240            let result = match event.event.event.clone() {
241                InputEvent::MouseButton(mouse_button_event) => {
242                    self.handle_native_mouse_button_event(mouse_button_event, &event, can_gc);
243                    InputEventResult::default()
244                },
245                InputEvent::MouseMove(_) => {
246                    self.handle_native_mouse_move_event(&event, can_gc);
247                    InputEventResult::default()
248                },
249                InputEvent::MouseLeftViewport(mouse_leave_event) => {
250                    self.handle_mouse_left_viewport_event(&event, &mouse_leave_event, can_gc);
251                    InputEventResult::default()
252                },
253                InputEvent::Touch(touch_event) => {
254                    self.handle_touch_event(touch_event, &event, can_gc)
255                },
256                InputEvent::Wheel(wheel_event) => {
257                    self.handle_wheel_event(wheel_event, &event, can_gc)
258                },
259                InputEvent::Keyboard(keyboard_event) => {
260                    self.handle_keyboard_event(keyboard_event, can_gc)
261                },
262                InputEvent::Ime(ime_event) => self.handle_ime_event(ime_event, can_gc),
263                #[cfg(feature = "gamepad")]
264                InputEvent::Gamepad(gamepad_event) => {
265                    self.handle_gamepad_event(gamepad_event);
266                    InputEventResult::default()
267                },
268                InputEvent::EditingAction(editing_action_event) => {
269                    self.handle_editing_action(None, editing_action_event, can_gc)
270                },
271                InputEvent::Scroll(scroll_event) => {
272                    self.handle_embedder_scroll_event(scroll_event);
273                    InputEventResult::default()
274                },
275            };
276
277            self.notify_embedder_that_event_was_handled(event.event, result);
278        }
279    }
280
281    fn notify_embedder_that_event_was_handled(
282        &self,
283        event: InputEventAndId,
284        result: InputEventResult,
285    ) {
286        // Wait to to notify the embedder that the vent was handled until all pending DOM
287        // event processing is finished.
288        let id = event.id;
289        let trusted_window = Trusted::new(&*self.window);
290        self.window
291            .as_global_scope()
292            .task_manager()
293            .dom_manipulation_task_source()
294            .queue(task!(notify_webdriver_input_event_completed: move || {
295                let window = trusted_window.root();
296                window.send_to_embedder(
297                    EmbedderMsg::InputEventHandled(window.webview_id(), id, result));
298            }));
299    }
300
301    pub(crate) fn set_cursor(&self, cursor: Option<Cursor>) {
302        if cursor == self.current_cursor.get() {
303            return;
304        }
305        self.current_cursor.set(cursor);
306        self.window.send_to_embedder(EmbedderMsg::SetCursor(
307            self.window.webview_id(),
308            cursor.unwrap_or_default(),
309        ));
310    }
311
312    fn handle_mouse_left_viewport_event(
313        &self,
314        input_event: &ConstellationInputEvent,
315        mouse_leave_event: &MouseLeftViewportEvent,
316        can_gc: CanGc,
317    ) {
318        if let Some(current_hover_target) = self.current_hover_target.get() {
319            let current_hover_target = current_hover_target.upcast::<Node>();
320            for element in current_hover_target
321                .inclusive_ancestors(ShadowIncluding::Yes)
322                .filter_map(DomRoot::downcast::<Element>)
323            {
324                element.set_hover_state(false);
325                element.set_active_state(false);
326            }
327
328            if let Some(hit_test_result) = self
329                .most_recent_mousemove_point
330                .get()
331                .and_then(|point| self.window.hit_test_from_point_in_viewport(point))
332            {
333                MouseEvent::new_for_platform_motion_event(
334                    &self.window,
335                    FireMouseEventType::Out,
336                    &hit_test_result,
337                    input_event,
338                    can_gc,
339                )
340                .upcast::<Event>()
341                .fire(current_hover_target.upcast(), can_gc);
342                self.handle_mouse_enter_leave_event(
343                    DomRoot::from_ref(current_hover_target),
344                    None,
345                    FireMouseEventType::Leave,
346                    &hit_test_result,
347                    input_event,
348                    can_gc,
349                );
350            }
351        }
352
353        // We do not want to always inform the embedder that cursor has been set to the
354        // default cursor, in order to avoid a timing issue when moving between `<iframe>`
355        // elements. There is currently no way to control which `SetCursor` message will
356        // reach the embedder first. This is safer when leaving the `WebView` entirely.
357        if !mouse_leave_event.focus_moving_to_another_iframe {
358            // If focus is moving to another frame, it will decide what the new status
359            // text is, but if this mouse leave event is leaving the WebView entirely,
360            // then clear it.
361            self.window
362                .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None));
363            self.set_cursor(None);
364        } else {
365            self.current_cursor.set(None);
366        }
367
368        self.current_hover_target.set(None);
369        self.most_recent_mousemove_point.set(None);
370    }
371
372    fn handle_mouse_enter_leave_event(
373        &self,
374        event_target: DomRoot<Node>,
375        related_target: Option<DomRoot<Node>>,
376        event_type: FireMouseEventType,
377        hit_test_result: &HitTestResult,
378        input_event: &ConstellationInputEvent,
379        can_gc: CanGc,
380    ) {
381        assert!(matches!(
382            event_type,
383            FireMouseEventType::Enter | FireMouseEventType::Leave
384        ));
385
386        let common_ancestor = match related_target.as_ref() {
387            Some(related_target) => event_target
388                .common_ancestor(related_target, ShadowIncluding::Yes)
389                .unwrap_or_else(|| DomRoot::from_ref(&*event_target)),
390            None => DomRoot::from_ref(&*event_target),
391        };
392
393        // We need to create a target chain in case the event target shares
394        // its boundaries with its ancestors.
395        let mut targets = vec![];
396        let mut current = Some(event_target);
397        while let Some(node) = current {
398            if node == common_ancestor {
399                break;
400            }
401            current = node.GetParentNode();
402            targets.push(node);
403        }
404
405        // The order for dispatching mouseenter events starts from the topmost
406        // common ancestor of the event target and the related target.
407        if event_type == FireMouseEventType::Enter {
408            targets = targets.into_iter().rev().collect();
409        }
410
411        for target in targets {
412            MouseEvent::new_for_platform_motion_event(
413                &self.window,
414                event_type,
415                hit_test_result,
416                input_event,
417                can_gc,
418            )
419            .upcast::<Event>()
420            .fire(target.upcast(), can_gc);
421        }
422    }
423
424    /// <https://w3c.github.io/uievents/#handle-native-mouse-move>
425    fn handle_native_mouse_move_event(&self, input_event: &ConstellationInputEvent, can_gc: CanGc) {
426        // Ignore all incoming events without a hit test.
427        let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
428            return;
429        };
430
431        let old_mouse_move_point = self
432            .most_recent_mousemove_point
433            .replace(Some(hit_test_result.point_in_frame));
434        if old_mouse_move_point == Some(hit_test_result.point_in_frame) {
435            return;
436        }
437
438        // Update the cursor when the mouse moves, if it has changed.
439        self.set_cursor(Some(hit_test_result.cursor));
440
441        let Some(new_target) = hit_test_result
442            .node
443            .inclusive_ancestors(ShadowIncluding::Yes)
444            .find_map(DomRoot::downcast::<Element>)
445        else {
446            return;
447        };
448
449        let target_has_changed = self
450            .current_hover_target
451            .get()
452            .is_none_or(|old_target| old_target != new_target);
453
454        // Here we know the target has changed, so we must update the state,
455        // dispatch mouseout to the previous one, mouseover to the new one.
456        if target_has_changed {
457            // Dispatch mouseout and mouseleave to previous target.
458            if let Some(old_target) = self.current_hover_target.get() {
459                let old_target_is_ancestor_of_new_target = old_target
460                    .upcast::<Node>()
461                    .is_ancestor_of(new_target.upcast::<Node>());
462
463                // If the old target is an ancestor of the new target, this can be skipped
464                // completely, since the node's hover state will be reset below.
465                if !old_target_is_ancestor_of_new_target {
466                    for element in old_target
467                        .upcast::<Node>()
468                        .inclusive_ancestors(ShadowIncluding::No)
469                        .filter_map(DomRoot::downcast::<Element>)
470                    {
471                        element.set_hover_state(false);
472                        element.set_active_state(false);
473                    }
474                }
475
476                MouseEvent::new_for_platform_motion_event(
477                    &self.window,
478                    FireMouseEventType::Out,
479                    &hit_test_result,
480                    input_event,
481                    can_gc,
482                )
483                .upcast::<Event>()
484                .fire(old_target.upcast(), can_gc);
485
486                if !old_target_is_ancestor_of_new_target {
487                    let event_target = DomRoot::from_ref(old_target.upcast::<Node>());
488                    let moving_into = Some(DomRoot::from_ref(new_target.upcast::<Node>()));
489                    self.handle_mouse_enter_leave_event(
490                        event_target,
491                        moving_into,
492                        FireMouseEventType::Leave,
493                        &hit_test_result,
494                        input_event,
495                        can_gc,
496                    );
497                }
498            }
499
500            // Dispatch mouseover and mouseenter to new target.
501            for element in new_target
502                .upcast::<Node>()
503                .inclusive_ancestors(ShadowIncluding::Yes)
504                .filter_map(DomRoot::downcast::<Element>)
505            {
506                element.set_hover_state(true);
507            }
508
509            MouseEvent::new_for_platform_motion_event(
510                &self.window,
511                FireMouseEventType::Over,
512                &hit_test_result,
513                input_event,
514                can_gc,
515            )
516            .upcast::<Event>()
517            .dispatch(new_target.upcast(), false, can_gc);
518
519            let moving_from = self
520                .current_hover_target
521                .get()
522                .map(|old_target| DomRoot::from_ref(old_target.upcast::<Node>()));
523            let event_target = DomRoot::from_ref(new_target.upcast::<Node>());
524            self.handle_mouse_enter_leave_event(
525                event_target,
526                moving_from,
527                FireMouseEventType::Enter,
528                &hit_test_result,
529                input_event,
530                can_gc,
531            );
532        }
533
534        // Send mousemove event to topmost target, unless it's an iframe, in which case
535        // `Paint` should have also sent an event to the inner document.
536        MouseEvent::new_for_platform_motion_event(
537            &self.window,
538            FireMouseEventType::Move,
539            &hit_test_result,
540            input_event,
541            can_gc,
542        )
543        .upcast::<Event>()
544        .fire(new_target.upcast(), can_gc);
545
546        self.update_current_hover_target_and_status(Some(new_target));
547    }
548
549    fn update_current_hover_target_and_status(&self, new_hover_target: Option<DomRoot<Element>>) {
550        let current_hover_target = self.current_hover_target.get();
551        if current_hover_target == new_hover_target {
552            return;
553        }
554
555        let previous_hover_target = self.current_hover_target.get();
556        self.current_hover_target.set(new_hover_target.as_deref());
557
558        // If the new hover target is an anchor with a status value, inform the embedder
559        // of the new value.
560        if let Some(target) = self.current_hover_target.get() {
561            if let Some(anchor) = target
562                .upcast::<Node>()
563                .inclusive_ancestors(ShadowIncluding::Yes)
564                .find_map(DomRoot::downcast::<HTMLAnchorElement>)
565            {
566                let status = anchor
567                    .full_href_url_for_user_interface()
568                    .map(|url| url.to_string());
569                self.window
570                    .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), status));
571                return;
572            }
573        }
574
575        // No state was set above, which means that the new value of the status in the embedder
576        // should be `None`. Set that now. If `previous_hover_target` is `None` that means this
577        // is the first mouse move event we are seeing after getting the cursor. In that case,
578        // we also clear the status.
579        if previous_hover_target.is_none_or(|previous_hover_target| {
580            previous_hover_target
581                .upcast::<Node>()
582                .inclusive_ancestors(ShadowIncluding::Yes)
583                .any(|node| node.is::<HTMLAnchorElement>())
584        }) {
585            self.window
586                .send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None));
587        }
588    }
589
590    pub(crate) fn handle_refresh_cursor(&self) {
591        let Some(most_recent_mousemove_point) = self.most_recent_mousemove_point.get() else {
592            return;
593        };
594
595        let Some(hit_test_result) = self
596            .window
597            .hit_test_from_point_in_viewport(most_recent_mousemove_point)
598        else {
599            return;
600        };
601
602        self.set_cursor(Some(hit_test_result.cursor));
603    }
604
605    /// <https://w3c.github.io/uievents/#mouseevent-algorithms>
606    /// Handles native mouse down, mouse up, mouse click.
607    fn handle_native_mouse_button_event(
608        &self,
609        event: MouseButtonEvent,
610        input_event: &ConstellationInputEvent,
611        can_gc: CanGc,
612    ) {
613        // Ignore all incoming events without a hit test.
614        let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
615            return;
616        };
617
618        debug!(
619            "{:?}: at {:?}",
620            event.action, hit_test_result.point_in_frame
621        );
622
623        let Some(element) = hit_test_result
624            .node
625            .inclusive_ancestors(ShadowIncluding::Yes)
626            .find_map(DomRoot::downcast::<Element>)
627        else {
628            return;
629        };
630
631        let node = element.upcast::<Node>();
632        debug!("{:?} on {:?}", event.action, node.debug_str());
633
634        // https://w3c.github.io/uievents/#hit-test
635        // Prevent mouse event if element is disabled.
636        // TODO: also inert.
637        if element.is_actually_disabled() {
638            return;
639        }
640
641        let mouse_event_type_string = match event.action {
642            embedder_traits::MouseButtonAction::Up => "mouseup",
643            embedder_traits::MouseButtonAction::Down => "mousedown",
644        };
645
646        // From <https://w3c.github.io/uievents/#event-type-mousedown>
647        // and <https://w3c.github.io/uievents/#event-type-mouseup>:
648        //
649        // UIEvent.detail: indicates the current click count incremented by one. For
650        // example, if no click happened before the mousedown, detail will contain
651        // the value 1
652        if event.action == MouseButtonAction::Down {
653            self.click_counting_info
654                .borrow_mut()
655                .reset_click_count_if_necessary(event.button, hit_test_result.point_in_frame);
656        }
657
658        let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_button_event(
659            mouse_event_type_string,
660            event,
661            input_event.pressed_mouse_buttons,
662            &self.window,
663            &hit_test_result,
664            input_event.active_keyboard_modifiers,
665            self.click_counting_info.borrow().count + 1,
666            can_gc,
667        ));
668
669        let activatable = element.as_maybe_activatable();
670        match event.action {
671            MouseButtonAction::Down => {
672                self.last_mouse_button_down_point
673                    .set(Some(hit_test_result.point_in_frame));
674
675                if let Some(a) = activatable {
676                    a.enter_formal_activation_state();
677                }
678
679                // (TODO) Step 6. Maybe send pointerdown event with `dom_event`.
680
681                // For a node within a text input UA shadow DOM,
682                // delegate the focus target into its shadow host.
683                // TODO: This focus delegation should be done
684                // with shadow DOM delegateFocus attribute.
685                let target_el = element.find_focusable_shadow_host_if_necessary();
686
687                let document = self.window.Document();
688                document.begin_focus_transaction();
689
690                // Try to focus `el`. If it's not focusable, focus the document instead.
691                document.request_focus(None, FocusInitiator::Local, can_gc);
692                document.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
693
694                // Step 7. Let result = dispatch event at target
695                let result = dom_event.dispatch(node.upcast(), false, can_gc);
696
697                // Step 8. If result is true and target is a focusable area
698                // that is click focusable, then Run the focusing steps at target.
699                if result && document.has_focus_transaction() {
700                    document.commit_focus_transaction(FocusInitiator::Local, can_gc);
701                }
702
703                // Step 9. If mbutton is the secondary mouse button, then
704                // Maybe show context menu with native, target.
705                if let MouseButton::Right = event.button {
706                    self.maybe_show_context_menu(
707                        node.upcast(),
708                        &hit_test_result,
709                        input_event,
710                        can_gc,
711                    );
712                }
713            },
714            // https://w3c.github.io/uievents/#handle-native-mouse-up
715            MouseButtonAction::Up => {
716                if let Some(a) = activatable {
717                    a.exit_formal_activation_state();
718                }
719
720                // (TODO) Step 6. Maybe send pointerup event with `dom_event``.
721
722                // Step 7. dispatch event at target.
723                dom_event.dispatch(node.upcast(), false, can_gc);
724
725                // Click counts should still work for other buttons even though they
726                // do not trigger "click" and "dblclick" events, so we increment
727                // even when those events are not fired.
728                self.click_counting_info
729                    .borrow_mut()
730                    .increment_click_count(event.button, hit_test_result.point_in_frame);
731
732                self.maybe_trigger_click_for_mouse_button_down_event(
733                    event,
734                    input_event,
735                    &hit_test_result,
736                    &element,
737                    can_gc,
738                );
739            },
740        }
741    }
742
743    /// <https://w3c.github.io/uievents/#handle-native-mouse-click>
744    /// <https://w3c.github.io/uievents/#event-type-dblclick>
745    fn maybe_trigger_click_for_mouse_button_down_event(
746        &self,
747        event: MouseButtonEvent,
748        input_event: &ConstellationInputEvent,
749        hit_test_result: &HitTestResult,
750        element: &Element,
751        can_gc: CanGc,
752    ) {
753        if event.button != MouseButton::Left {
754            return;
755        }
756
757        let Some(last_mouse_button_down_point) = self.last_mouse_button_down_point.take() else {
758            return;
759        };
760
761        let distance = last_mouse_button_down_point.distance_to(hit_test_result.point_in_frame);
762        let maximum_click_distance = 10.0 * self.window.device_pixel_ratio().get();
763        if distance > maximum_click_distance {
764            return;
765        }
766
767        // From <https://w3c.github.io/uievents/#event-type-click>
768        // > The click event type MUST be dispatched on the topmost event target indicated by the
769        // > pointer, when the user presses down and releases the primary pointer button.
770        // For nodes inside a text input UA shadow DOM, dispatch dblclick at the shadow host.
771        let delegated = element.find_focusable_shadow_host_if_necessary();
772        let element = delegated.as_deref().unwrap_or(element);
773        self.most_recently_clicked_element.set(Some(element));
774
775        let click_count = self.click_counting_info.borrow().count;
776        element.set_click_in_progress(true);
777        MouseEvent::for_platform_button_event(
778            "click",
779            event,
780            input_event.pressed_mouse_buttons,
781            &self.window,
782            hit_test_result,
783            input_event.active_keyboard_modifiers,
784            click_count,
785            can_gc,
786        )
787        .upcast::<Event>()
788        .dispatch(element.upcast(), false, can_gc);
789        element.set_click_in_progress(false);
790
791        // The firing of "dbclick" events is dependent on the platform, so we have
792        // some flexibility here. Some browsers on some platforms only fire a
793        // "dbclick" when the click count is 2 and others essentially fire one for
794        // every 2 clicks in a sequence. In all cases, browsers set the click count
795        // `detail` property to 2.
796        //
797        // We follow the latter approach here, considering that every sequence of
798        // even numbered clicks is a series of double clicks.
799        if click_count % 2 == 0 {
800            MouseEvent::for_platform_button_event(
801                "dblclick",
802                event,
803                input_event.pressed_mouse_buttons,
804                &self.window,
805                hit_test_result,
806                input_event.active_keyboard_modifiers,
807                2,
808                can_gc,
809            )
810            .upcast::<Event>()
811            .dispatch(element.upcast(), false, can_gc);
812        }
813    }
814
815    /// <https://www.w3.org/TR/uievents/#maybe-show-context-menu>
816    fn maybe_show_context_menu(
817        &self,
818        target: &EventTarget,
819        hit_test_result: &HitTestResult,
820        input_event: &ConstellationInputEvent,
821        can_gc: CanGc,
822    ) {
823        // <https://w3c.github.io/uievents/#contextmenu>
824        let menu_event = PointerEvent::new(
825            &self.window,                   // window
826            DOMString::from("contextmenu"), // type
827            EventBubbles::Bubbles,          // can_bubble
828            EventCancelable::Cancelable,    // cancelable
829            Some(&self.window),             // view
830            0,                              // detail
831            hit_test_result.point_in_frame.to_i32(),
832            hit_test_result.point_in_frame.to_i32(),
833            hit_test_result
834                .point_relative_to_initial_containing_block
835                .to_i32(),
836            input_event.active_keyboard_modifiers,
837            2i16, // button, right mouse button
838            input_event.pressed_mouse_buttons,
839            None,                     // related_target
840            None,                     // point_in_target
841            PointerId::Mouse as i32,  // pointer_id
842            1,                        // width
843            1,                        // height
844            0.5,                      // pressure
845            0.0,                      // tangential_pressure
846            0,                        // tilt_x
847            0,                        // tilt_y
848            0,                        // twist
849            PI / 2.0,                 // altitude_angle
850            0.0,                      // azimuth_angle
851            DOMString::from("mouse"), // pointer_type
852            true,                     // is_primary
853            vec![],                   // coalesced_events
854            vec![],                   // predicted_events
855            can_gc,
856        );
857
858        // Step 3. Let result = dispatch menuevent at target.
859        let result = menu_event.upcast::<Event>().fire(target, can_gc);
860
861        // Step 4. If result is true, then show the UA context menu
862        if result {
863            self.window
864                .Document()
865                .embedder_controls()
866                .show_context_menu(hit_test_result);
867        };
868    }
869
870    fn handle_touch_event(
871        &self,
872        event: EmbedderTouchEvent,
873        input_event: &ConstellationInputEvent,
874        can_gc: CanGc,
875    ) -> InputEventResult {
876        // Ignore all incoming events without a hit test.
877        let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
878            self.update_active_touch_points_when_early_return(event);
879            return Default::default();
880        };
881
882        let TouchId(identifier) = event.id;
883        let event_name = match event.event_type {
884            TouchEventType::Down => "touchstart",
885            TouchEventType::Move => "touchmove",
886            TouchEventType::Up => "touchend",
887            TouchEventType::Cancel => "touchcancel",
888        };
889
890        let Some(el) = hit_test_result
891            .node
892            .inclusive_ancestors(ShadowIncluding::Yes)
893            .find_map(DomRoot::downcast::<Element>)
894        else {
895            self.update_active_touch_points_when_early_return(event);
896            return Default::default();
897        };
898
899        let target = DomRoot::upcast::<EventTarget>(el);
900        let window = &*self.window;
901
902        let client_x = Finite::wrap(hit_test_result.point_in_frame.x as f64);
903        let client_y = Finite::wrap(hit_test_result.point_in_frame.y as f64);
904        let page_x =
905            Finite::wrap(hit_test_result.point_in_frame.x as f64 + window.PageXOffset() as f64);
906        let page_y =
907            Finite::wrap(hit_test_result.point_in_frame.y as f64 + window.PageYOffset() as f64);
908
909        let touch = Touch::new(
910            window, identifier, &target, client_x,
911            client_y, // TODO: Get real screen coordinates?
912            client_x, client_y, page_x, page_y, can_gc,
913        );
914
915        match event.event_type {
916            TouchEventType::Down => {
917                // Add a new touch point
918                self.active_touch_points
919                    .borrow_mut()
920                    .push(Dom::from_ref(&*touch));
921            },
922            TouchEventType::Move => {
923                // Replace an existing touch point
924                let mut active_touch_points = self.active_touch_points.borrow_mut();
925                match active_touch_points
926                    .iter_mut()
927                    .find(|t| t.Identifier() == identifier)
928                {
929                    Some(t) => *t = Dom::from_ref(&*touch),
930                    None => warn!("Got a touchmove event for a non-active touch point"),
931                }
932            },
933            TouchEventType::Up | TouchEventType::Cancel => {
934                // Remove an existing touch point
935                let mut active_touch_points = self.active_touch_points.borrow_mut();
936                match active_touch_points
937                    .iter()
938                    .position(|t| t.Identifier() == identifier)
939                {
940                    Some(i) => {
941                        active_touch_points.swap_remove(i);
942                    },
943                    None => warn!("Got a touchend event for a non-active touch point"),
944                }
945            },
946        }
947
948        rooted_vec!(let mut target_touches);
949        let touches = {
950            let touches = self.active_touch_points.borrow();
951            target_touches.extend(touches.iter().filter(|t| t.Target() == target).cloned());
952            TouchList::new(window, touches.r(), can_gc)
953        };
954
955        let touch_event = TouchEvent::new(
956            window,
957            DOMString::from(event_name),
958            EventBubbles::Bubbles,
959            EventCancelable::from(event.is_cancelable()),
960            EventComposed::Composed,
961            Some(window),
962            0i32,
963            &touches,
964            &TouchList::new(window, from_ref(&&*touch), can_gc),
965            &TouchList::new(window, target_touches.r(), can_gc),
966            // FIXME: modifier keys
967            false,
968            false,
969            false,
970            false,
971            can_gc,
972        );
973
974        let event = touch_event.upcast::<Event>();
975        event.fire(&target, can_gc);
976        event.flags().into()
977    }
978
979    // If hittest fails, we still need to update the active point information.
980    fn update_active_touch_points_when_early_return(&self, event: EmbedderTouchEvent) {
981        match event.event_type {
982            TouchEventType::Down => {
983                // If the touchdown fails, we don't need to do anything.
984                // When a touchmove or touchdown occurs at that touch point,
985                // a warning is triggered: Got a touchmove/touchend event for a non-active touch point
986            },
987            TouchEventType::Move => {
988                // The failure of touchmove does not affect the number of active points.
989                // Since there is no position information when it fails, we do not need to update.
990            },
991            TouchEventType::Up | TouchEventType::Cancel => {
992                // Remove an existing touch point
993                let mut active_touch_points = self.active_touch_points.borrow_mut();
994                match active_touch_points
995                    .iter()
996                    .position(|t| t.Identifier() == event.id.0)
997                {
998                    Some(i) => {
999                        active_touch_points.swap_remove(i);
1000                    },
1001                    None => warn!("Got a touchend event for a non-active touch point"),
1002                }
1003            },
1004        }
1005    }
1006
1007    /// The entry point for all key processing for web content
1008    fn handle_keyboard_event(
1009        &self,
1010        keyboard_event: EmbedderKeyboardEvent,
1011        can_gc: CanGc,
1012    ) -> InputEventResult {
1013        let document = self.window.Document();
1014        let focused = document.get_focused_element();
1015        let body = document.GetBody();
1016
1017        let target = match (&focused, &body) {
1018            (Some(focused), _) => focused.upcast(),
1019            (&None, Some(body)) => body.upcast(),
1020            (&None, &None) => self.window.upcast(),
1021        };
1022
1023        let keyevent = KeyboardEvent::new(
1024            &self.window,
1025            DOMString::from(keyboard_event.event.state.event_type()),
1026            true,
1027            true,
1028            Some(&self.window),
1029            0,
1030            keyboard_event.event.key.clone(),
1031            DOMString::from(keyboard_event.event.code.to_string()),
1032            keyboard_event.event.location as u32,
1033            keyboard_event.event.repeat,
1034            keyboard_event.event.is_composing,
1035            keyboard_event.event.modifiers,
1036            0,
1037            keyboard_event.event.key.legacy_keycode(),
1038            can_gc,
1039        );
1040
1041        let event = keyevent.upcast::<Event>();
1042        event.fire(target, can_gc);
1043
1044        let mut flags = event.flags();
1045        if flags.contains(EventFlags::Canceled) {
1046            return flags.into();
1047        }
1048
1049        // https://w3c.github.io/uievents/#keys-cancelable-keys
1050        // it MUST prevent the respective beforeinput and input
1051        // (and keypress if supported) events from being generated
1052        // TODO: keypress should be deprecated and superceded by beforeinput
1053
1054        let is_character_value_key = matches!(
1055            keyboard_event.event.key,
1056            Key::Character(_) | Key::Named(NamedKey::Enter)
1057        );
1058        if keyboard_event.event.state == KeyState::Down &&
1059            is_character_value_key &&
1060            !keyboard_event.event.is_composing
1061        {
1062            // https://w3c.github.io/uievents/#keypress-event-order
1063            let keypress_event = KeyboardEvent::new(
1064                &self.window,
1065                DOMString::from("keypress"),
1066                true,
1067                true,
1068                Some(&self.window),
1069                0,
1070                keyboard_event.event.key.clone(),
1071                DOMString::from(keyboard_event.event.code.to_string()),
1072                keyboard_event.event.location as u32,
1073                keyboard_event.event.repeat,
1074                keyboard_event.event.is_composing,
1075                keyboard_event.event.modifiers,
1076                keyboard_event.event.key.legacy_charcode(),
1077                0,
1078                can_gc,
1079            );
1080            let event = keypress_event.upcast::<Event>();
1081            event.fire(target, can_gc);
1082            flags = event.flags();
1083        }
1084
1085        if flags.contains(EventFlags::Canceled) {
1086            return flags.into();
1087        }
1088
1089        // This behavior is unspecced
1090        // We are supposed to dispatch synthetic click activation for Space and/or Return,
1091        // however *when* we do it is up to us.
1092        // Here, we're dispatching it after the key event so the script has a chance to cancel it
1093        // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27337
1094        if (keyboard_event.event.key == Key::Named(NamedKey::Enter) ||
1095            keyboard_event.event.code == Code::Space) &&
1096            keyboard_event.event.state == KeyState::Up
1097        {
1098            if let Some(elem) = target.downcast::<Element>() {
1099                elem.upcast::<Node>()
1100                    .fire_synthetic_pointer_event_not_trusted(DOMString::from("click"), can_gc);
1101            }
1102        }
1103
1104        flags.into()
1105    }
1106
1107    fn handle_ime_event(&self, event: ImeEvent, can_gc: CanGc) -> InputEventResult {
1108        let document = self.window.Document();
1109        let composition_event = match event {
1110            ImeEvent::Dismissed => {
1111                document.request_focus(
1112                    document.GetBody().as_ref().map(|e| e.upcast()),
1113                    FocusInitiator::Local,
1114                    can_gc,
1115                );
1116                return Default::default();
1117            },
1118            ImeEvent::Composition(composition_event) => composition_event,
1119        };
1120
1121        // spec: https://w3c.github.io/uievents/#compositionstart
1122        // spec: https://w3c.github.io/uievents/#compositionupdate
1123        // spec: https://w3c.github.io/uievents/#compositionend
1124        // > Event.target : focused element processing the composition
1125        let focused = document.get_focused_element();
1126        let target = if let Some(elem) = &focused {
1127            elem.upcast()
1128        } else {
1129            // Event is only dispatched if there is a focused element.
1130            return Default::default();
1131        };
1132
1133        let cancelable = composition_event.state == keyboard_types::CompositionState::Start;
1134        let event = CompositionEvent::new(
1135            &self.window,
1136            DOMString::from(composition_event.state.event_type()),
1137            true,
1138            cancelable,
1139            Some(&self.window),
1140            0,
1141            DOMString::from(composition_event.data),
1142            can_gc,
1143        );
1144
1145        let event = event.upcast::<Event>();
1146        event.fire(target, can_gc);
1147        event.flags().into()
1148    }
1149
1150    fn handle_wheel_event(
1151        &self,
1152        event: EmbedderWheelEvent,
1153        input_event: &ConstellationInputEvent,
1154        can_gc: CanGc,
1155    ) -> InputEventResult {
1156        // Ignore all incoming events without a hit test.
1157        let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
1158            return Default::default();
1159        };
1160
1161        let Some(el) = hit_test_result
1162            .node
1163            .inclusive_ancestors(ShadowIncluding::Yes)
1164            .find_map(DomRoot::downcast::<Element>)
1165        else {
1166            return Default::default();
1167        };
1168
1169        let node = el.upcast::<Node>();
1170        let wheel_event_type_string = "wheel".to_owned();
1171        debug!(
1172            "{}: on {:?} at {:?}",
1173            wheel_event_type_string,
1174            node.debug_str(),
1175            hit_test_result.point_in_frame
1176        );
1177
1178        // https://w3c.github.io/uievents/#event-wheelevents
1179        let dom_event = WheelEvent::new(
1180            &self.window,
1181            DOMString::from(wheel_event_type_string),
1182            EventBubbles::Bubbles,
1183            EventCancelable::Cancelable,
1184            Some(&self.window),
1185            0i32,
1186            hit_test_result.point_in_frame.to_i32(),
1187            hit_test_result.point_in_frame.to_i32(),
1188            hit_test_result
1189                .point_relative_to_initial_containing_block
1190                .to_i32(),
1191            input_event.active_keyboard_modifiers,
1192            0i16,
1193            input_event.pressed_mouse_buttons,
1194            None,
1195            None,
1196            // winit defines positive wheel delta values as revealing more content left/up.
1197            // https://docs.rs/winit-gtk/latest/winit/event/enum.MouseScrollDelta.html
1198            // This is the opposite of wheel delta in uievents
1199            // https://w3c.github.io/uievents/#dom-wheeleventinit-deltaz
1200            Finite::wrap(-event.delta.x),
1201            Finite::wrap(-event.delta.y),
1202            Finite::wrap(-event.delta.z),
1203            event.delta.mode as u32,
1204            can_gc,
1205        );
1206
1207        let dom_event = dom_event.upcast::<Event>();
1208        dom_event.set_trusted(true);
1209        dom_event.fire(node.upcast(), can_gc);
1210
1211        dom_event.flags().into()
1212    }
1213
1214    #[cfg(feature = "gamepad")]
1215    fn handle_gamepad_event(&self, gamepad_event: EmbedderGamepadEvent) {
1216        match gamepad_event {
1217            EmbedderGamepadEvent::Connected(index, name, bounds, supported_haptic_effects) => {
1218                self.handle_gamepad_connect(
1219                    index.0,
1220                    name,
1221                    bounds.axis_bounds,
1222                    bounds.button_bounds,
1223                    supported_haptic_effects,
1224                );
1225            },
1226            EmbedderGamepadEvent::Disconnected(index) => {
1227                self.handle_gamepad_disconnect(index.0);
1228            },
1229            EmbedderGamepadEvent::Updated(index, update_type) => {
1230                self.receive_new_gamepad_button_or_axis(index.0, update_type);
1231            },
1232        };
1233    }
1234
1235    /// <https://www.w3.org/TR/gamepad/#dfn-gamepadconnected>
1236    #[cfg(feature = "gamepad")]
1237    fn handle_gamepad_connect(
1238        &self,
1239        // As the spec actually defines how to set the gamepad index, the GilRs index
1240        // is currently unused, though in practice it will almost always be the same.
1241        // More infra is currently needed to track gamepads across windows.
1242        _index: usize,
1243        name: String,
1244        axis_bounds: (f64, f64),
1245        button_bounds: (f64, f64),
1246        supported_haptic_effects: GamepadSupportedHapticEffects,
1247    ) {
1248        // TODO: 2. If document is not null and is not allowed to use the "gamepad" permission,
1249        //          then abort these steps.
1250        let trusted_window = Trusted::new(&*self.window);
1251        self.window
1252            .upcast::<GlobalScope>()
1253            .task_manager()
1254            .gamepad_task_source()
1255            .queue(task!(gamepad_connected: move || {
1256                let window = trusted_window.root();
1257
1258                let navigator = window.Navigator();
1259                let selected_index = navigator.select_gamepad_index();
1260                let gamepad = Gamepad::new(
1261                    &window,
1262                    selected_index,
1263                    name,
1264                    "standard".into(),
1265                    axis_bounds,
1266                    button_bounds,
1267                    supported_haptic_effects,
1268                    false,
1269                    CanGc::note(),
1270                );
1271                navigator.set_gamepad(selected_index as usize, &gamepad, CanGc::note());
1272            }));
1273    }
1274
1275    /// <https://www.w3.org/TR/gamepad/#dfn-gamepaddisconnected>
1276    #[cfg(feature = "gamepad")]
1277    fn handle_gamepad_disconnect(&self, index: usize) {
1278        let trusted_window = Trusted::new(&*self.window);
1279        self.window
1280            .upcast::<GlobalScope>()
1281            .task_manager()
1282            .gamepad_task_source()
1283            .queue(task!(gamepad_disconnected: move || {
1284                let window = trusted_window.root();
1285                let navigator = window.Navigator();
1286                if let Some(gamepad) = navigator.get_gamepad(index) {
1287                    if window.Document().is_fully_active() {
1288                        gamepad.update_connected(false, gamepad.exposed(), CanGc::note());
1289                        navigator.remove_gamepad(index);
1290                    }
1291                }
1292            }));
1293    }
1294
1295    /// <https://www.w3.org/TR/gamepad/#receiving-inputs>
1296    #[cfg(feature = "gamepad")]
1297    fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) {
1298        let trusted_window = Trusted::new(&*self.window);
1299
1300        // <https://w3c.github.io/gamepad/#dfn-update-gamepad-state>
1301        self.window.upcast::<GlobalScope>().task_manager().gamepad_task_source().queue(
1302                task!(update_gamepad_state: move || {
1303                    let window = trusted_window.root();
1304                    let navigator = window.Navigator();
1305                    if let Some(gamepad) = navigator.get_gamepad(index) {
1306                        let current_time = window.Performance().Now();
1307                        gamepad.update_timestamp(*current_time);
1308                        match update_type {
1309                            GamepadUpdateType::Axis(index, value) => {
1310                                gamepad.map_and_normalize_axes(index, value);
1311                            },
1312                            GamepadUpdateType::Button(index, value) => {
1313                                gamepad.map_and_normalize_buttons(index, value);
1314                            }
1315                        };
1316                        if !navigator.has_gamepad_gesture() && contains_user_gesture(update_type) {
1317                            navigator.set_has_gamepad_gesture(true);
1318                            navigator.GetGamepads()
1319                                .iter()
1320                                .filter_map(|g| g.as_ref())
1321                                .for_each(|gamepad| {
1322                                    gamepad.set_exposed(true);
1323                                    gamepad.update_timestamp(*current_time);
1324                                    let new_gamepad = Trusted::new(&**gamepad);
1325                                    if window.Document().is_fully_active() {
1326                                        window.upcast::<GlobalScope>().task_manager().gamepad_task_source().queue(
1327                                            task!(update_gamepad_connect: move || {
1328                                                let gamepad = new_gamepad.root();
1329                                                gamepad.notify_event(GamepadEventType::Connected, CanGc::note());
1330                                            })
1331                                        );
1332                                    }
1333                                });
1334                        }
1335                    }
1336                })
1337            );
1338    }
1339
1340    /// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions>
1341    pub(crate) fn handle_editing_action(
1342        &self,
1343        element: Option<DomRoot<Element>>,
1344        action: EditingActionEvent,
1345        can_gc: CanGc,
1346    ) -> InputEventResult {
1347        let clipboard_event_type = match action {
1348            EditingActionEvent::Copy => ClipboardEventType::Copy,
1349            EditingActionEvent::Cut => ClipboardEventType::Cut,
1350            EditingActionEvent::Paste => ClipboardEventType::Paste,
1351        };
1352
1353        // The script_triggered flag is set if the action runs because of a script, e.g. document.execCommand()
1354        let script_triggered = false;
1355
1356        // The script_may_access_clipboard flag is set
1357        // if action is paste and the script thread is allowed to read from clipboard or
1358        // if action is copy or cut and the script thread is allowed to modify the clipboard
1359        let script_may_access_clipboard = false;
1360
1361        // Step 1 If the script-triggered flag is set and the script-may-access-clipboard flag is unset
1362        if script_triggered && !script_may_access_clipboard {
1363            return InputEventResult::empty();
1364        }
1365
1366        // Step 2 Fire a clipboard event
1367        let clipboard_event =
1368            self.fire_clipboard_event(element.clone(), clipboard_event_type, can_gc);
1369
1370        // Step 3 If a script doesn't call preventDefault()
1371        // the event will be handled inside target's VirtualMethods::handle_event
1372        let event = clipboard_event.upcast::<Event>();
1373        if !event.IsTrusted() {
1374            return event.flags().into();
1375        }
1376
1377        // Step 4 If the event was canceled, then
1378        if event.DefaultPrevented() {
1379            let event_type = event.Type();
1380            match_domstring_ascii!(event_type,
1381
1382                "copy" => {
1383                    // Step 4.1 Call the write content to the clipboard algorithm,
1384                    // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list.
1385                    if let Some(clipboard_data) = clipboard_event.get_clipboard_data() {
1386                        let drag_data_store =
1387                            clipboard_data.data_store().expect("This shouldn't fail");
1388                        self.write_content_to_the_clipboard(&drag_data_store);
1389                    }
1390                },
1391                "cut" => {
1392                    // Step 4.1 Call the write content to the clipboard algorithm,
1393                    // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list.
1394                    if let Some(clipboard_data) = clipboard_event.get_clipboard_data() {
1395                        let drag_data_store =
1396                            clipboard_data.data_store().expect("This shouldn't fail");
1397                        self.write_content_to_the_clipboard(&drag_data_store);
1398                    }
1399
1400                    // Step 4.2 Fire a clipboard event named clipboardchange
1401                    self.fire_clipboard_event(element, ClipboardEventType::Change, can_gc);
1402                },
1403                // Step 4.1 Return false.
1404                // Note: This function deviates from the specification a bit by returning
1405                // the `InputEventResult` below.
1406                "paste" => (),
1407                _ => (),
1408            )
1409        }
1410
1411        // Step 5: Return true from the action.
1412        // In this case we are returning the `InputEventResult` instead of true or false.
1413        event.flags().into()
1414    }
1415
1416    /// <https://www.w3.org/TR/clipboard-apis/#fire-a-clipboard-event>
1417    pub(crate) fn fire_clipboard_event(
1418        &self,
1419        target: Option<DomRoot<Element>>,
1420        clipboard_event_type: ClipboardEventType,
1421        can_gc: CanGc,
1422    ) -> DomRoot<ClipboardEvent> {
1423        let clipboard_event = ClipboardEvent::new(
1424            &self.window,
1425            None,
1426            DOMString::from(clipboard_event_type.as_str()),
1427            EventBubbles::Bubbles,
1428            EventCancelable::Cancelable,
1429            None,
1430            can_gc,
1431        );
1432
1433        // Step 1 Let clear_was_called be false
1434        // Step 2 Let types_to_clear an empty list
1435        let mut drag_data_store = DragDataStore::new();
1436
1437        // Step 4 let clipboard-entry be the sequence number of clipboard content, null if the OS doesn't support it.
1438
1439        // Step 5 let trusted be true if the event is generated by the user agent, false otherwise
1440        let trusted = true;
1441
1442        // Step 6 if the context is editable:
1443        let document = self.window.Document();
1444        let target = target.or(document.get_focused_element());
1445        let target = target
1446            .map(|target| DomRoot::from_ref(target.upcast()))
1447            .or_else(|| {
1448                document
1449                    .GetBody()
1450                    .map(|body| DomRoot::from_ref(body.upcast()))
1451            })
1452            .unwrap_or_else(|| DomRoot::from_ref(self.window.upcast()));
1453
1454        // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70
1455        // Step 7
1456        match clipboard_event_type {
1457            ClipboardEventType::Copy | ClipboardEventType::Cut => {
1458                // Step 7.2.1
1459                drag_data_store.set_mode(Mode::ReadWrite);
1460            },
1461            ClipboardEventType::Paste => {
1462                let (callback, receiver) =
1463                    GenericCallback::new_blocking().expect("Could not create callback");
1464                self.window.send_to_embedder(EmbedderMsg::GetClipboardText(
1465                    self.window.webview_id(),
1466                    callback,
1467                ));
1468                let text_contents = receiver
1469                    .recv()
1470                    .map(Result::unwrap_or_default)
1471                    .unwrap_or_default();
1472
1473                // Step 7.1.1
1474                drag_data_store.set_mode(Mode::ReadOnly);
1475                // Step 7.1.2 If trusted or the implementation gives script-generated events access to the clipboard
1476                if trusted {
1477                    // Step 7.1.2.1 For each clipboard-part on the OS clipboard:
1478
1479                    // Step 7.1.2.1.1 If clipboard-part contains plain text, then
1480                    let data = DOMString::from(text_contents.to_string());
1481                    let type_ = DOMString::from("text/plain");
1482                    let _ = drag_data_store.add(Kind::Text { data, type_ });
1483
1484                    // Step 7.1.2.1.2 TODO If clipboard-part represents file references, then for each file reference
1485                    // Step 7.1.2.1.3 TODO If clipboard-part contains HTML- or XHTML-formatted text then
1486
1487                    // Step 7.1.3 Update clipboard-event-data’s files to match clipboard-event-data’s items
1488                    // Step 7.1.4 Update clipboard-event-data’s types to match clipboard-event-data’s items
1489                }
1490            },
1491            ClipboardEventType::Change => (),
1492        }
1493
1494        // Step 3
1495        let clipboard_event_data = DataTransfer::new(
1496            &self.window,
1497            Rc::new(RefCell::new(Some(drag_data_store))),
1498            can_gc,
1499        );
1500
1501        // Step 8
1502        clipboard_event.set_clipboard_data(Some(&clipboard_event_data));
1503
1504        // Step 9
1505        let event = clipboard_event.upcast::<Event>();
1506        event.set_trusted(trusted);
1507
1508        // Step 10 Set event’s composed to true.
1509        event.set_composed(true);
1510
1511        // Step 11
1512        event.dispatch(&target, false, can_gc);
1513
1514        DomRoot::from(clipboard_event)
1515    }
1516
1517    /// <https://www.w3.org/TR/clipboard-apis/#write-content-to-the-clipboard>
1518    fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) {
1519        // Step 1
1520        if drag_data_store.list_len() > 0 {
1521            // Step 1.1 Clear the clipboard.
1522            self.window
1523                .send_to_embedder(EmbedderMsg::ClearClipboard(self.window.webview_id()));
1524            // Step 1.2
1525            for item in drag_data_store.iter_item_list() {
1526                match item {
1527                    Kind::Text { data, .. } => {
1528                        // Step 1.2.1.1 Ensure encoding is correct per OS and locale conventions
1529                        // Step 1.2.1.2 Normalize line endings according to platform conventions
1530                        // Step 1.2.1.3
1531                        self.window.send_to_embedder(EmbedderMsg::SetClipboardText(
1532                            self.window.webview_id(),
1533                            data.to_string(),
1534                        ));
1535                    },
1536                    Kind::File { .. } => {
1537                        // Step 1.2.2 If data is of a type listed in the mandatory data types list, then
1538                        // Step 1.2.2.1 Place part on clipboard with the appropriate OS clipboard format description
1539                        // Step 1.2.3 Else this is left to the implementation
1540                    },
1541                }
1542            }
1543        } else {
1544            // Step 2.1
1545            if drag_data_store.clear_was_called {
1546                // Step 2.1.1 If types-to-clear list is empty, clear the clipboard
1547                self.window
1548                    .send_to_embedder(EmbedderMsg::ClearClipboard(self.window.webview_id()));
1549                // Step 2.1.2 Else remove the types in the list from the clipboard
1550                // As of now this can't be done with Arboard, and it's possible that will be removed from the spec
1551            }
1552        }
1553    }
1554
1555    /// Handle scroll event triggered by user interactions from embedder side.
1556    /// <https://drafts.csswg.org/cssom-view/#scrolling-events>
1557    #[expect(unsafe_code)]
1558    fn handle_embedder_scroll_event(&self, event: ScrollEvent) {
1559        // If it is a viewport scroll.
1560        let document = self.window.Document();
1561        if event.external_id.is_root() {
1562            document.handle_viewport_scroll_event();
1563        } else {
1564            // Otherwise, check whether it is for a relevant element within the document.
1565            let Some(node_id) = node_id_from_scroll_id(event.external_id.0 as usize) else {
1566                return;
1567            };
1568            let node = unsafe {
1569                node::from_untrusted_node_address(UntrustedNodeAddress::from_id(node_id))
1570            };
1571            let Some(element) = node
1572                .inclusive_ancestors(ShadowIncluding::Yes)
1573                .find_map(DomRoot::downcast::<Element>)
1574            else {
1575                return;
1576            };
1577
1578            document.handle_element_scroll_event(&element);
1579        }
1580    }
1581
1582    pub(crate) fn run_default_keyboard_event_handler(&self, event: &KeyboardEvent) {
1583        if event.upcast::<Event>().type_() != atom!("keydown") {
1584            return;
1585        }
1586        if !event.modifiers().is_empty() {
1587            return;
1588        }
1589        let scroll = match event.key() {
1590            Key::Named(NamedKey::ArrowDown) => KeyboardScroll::Down,
1591            Key::Named(NamedKey::ArrowLeft) => KeyboardScroll::Left,
1592            Key::Named(NamedKey::ArrowRight) => KeyboardScroll::Right,
1593            Key::Named(NamedKey::ArrowUp) => KeyboardScroll::Up,
1594            Key::Named(NamedKey::End) => KeyboardScroll::End,
1595            Key::Named(NamedKey::Home) => KeyboardScroll::Home,
1596            Key::Named(NamedKey::PageDown) => KeyboardScroll::PageDown,
1597            Key::Named(NamedKey::PageUp) => KeyboardScroll::PageUp,
1598            _ => return,
1599        };
1600        self.do_keyboard_scroll(scroll);
1601    }
1602
1603    pub(crate) fn do_keyboard_scroll(&self, scroll: KeyboardScroll) {
1604        let scroll_axis = match scroll {
1605            KeyboardScroll::Left | KeyboardScroll::Right => ScrollingBoxAxis::X,
1606            _ => ScrollingBoxAxis::Y,
1607        };
1608
1609        let document = self.window.Document();
1610        let mut scrolling_box = document
1611            .get_focused_element()
1612            .or(self.most_recently_clicked_element.get())
1613            .and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive))
1614            .unwrap_or_else(|| {
1615                document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive)
1616            });
1617
1618        while !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
1619            // Always fall back to trying to scroll the entire document.
1620            if scrolling_box.is_viewport() {
1621                break;
1622            }
1623            let parent = scrolling_box.parent().unwrap_or_else(|| {
1624                document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive)
1625            });
1626            scrolling_box = parent;
1627        }
1628
1629        let calculate_current_scroll_offset_and_delta = || {
1630            const LINE_HEIGHT: f32 = 76.0;
1631            const LINE_WIDTH: f32 = 76.0;
1632
1633            let current_scroll_offset = scrolling_box.scroll_position();
1634            (
1635                current_scroll_offset,
1636                match scroll {
1637                    KeyboardScroll::Home => Vector2D::new(0.0, -current_scroll_offset.y),
1638                    KeyboardScroll::End => Vector2D::new(
1639                        0.0,
1640                        -current_scroll_offset.y + scrolling_box.content_size().height -
1641                            scrolling_box.size().height,
1642                    ),
1643                    KeyboardScroll::PageDown => {
1644                        Vector2D::new(0.0, scrolling_box.size().height - 2.0 * LINE_HEIGHT)
1645                    },
1646                    KeyboardScroll::PageUp => {
1647                        Vector2D::new(0.0, 2.0 * LINE_HEIGHT - scrolling_box.size().height)
1648                    },
1649                    KeyboardScroll::Up => Vector2D::new(0.0, -LINE_HEIGHT),
1650                    KeyboardScroll::Down => Vector2D::new(0.0, LINE_HEIGHT),
1651                    KeyboardScroll::Left => Vector2D::new(-LINE_WIDTH, 0.0),
1652                    KeyboardScroll::Right => Vector2D::new(LINE_WIDTH, 0.0),
1653                },
1654            )
1655        };
1656
1657        // If trying to scroll the viewport of this `Window` and this is the root `Document`
1658        // of the `WebView`, then send the srolling operation to the renderer, so that it
1659        // can properly pan any pinch zoom viewport.
1660        let parent_pipeline = self.window.parent_info();
1661        if scrolling_box.is_viewport() && parent_pipeline.is_none() {
1662            let (_, delta) = calculate_current_scroll_offset_and_delta();
1663            self.window
1664                .paint_api()
1665                .scroll_viewport_by_delta(self.window.webview_id(), delta);
1666        }
1667
1668        // If this is the viewport and we cannot scroll, try to ask a parent viewport to scroll,
1669        // if we are inside an `<iframe>`.
1670        if !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
1671            assert!(scrolling_box.is_viewport());
1672
1673            let window_proxy = document.window().window_proxy();
1674            if let Some(iframe) = window_proxy.frame_element() {
1675                // When the `<iframe>` is local (in this ScriptThread), we can
1676                // synchronously chain up the keyboard scrolling event.
1677                let cx = GlobalScope::get_cx();
1678                let iframe_window = iframe.owner_window();
1679                let _ac = JSAutoRealm::new(*cx, iframe_window.reflector().get_jsobject().get());
1680                iframe_window
1681                    .Document()
1682                    .event_handler()
1683                    .do_keyboard_scroll(scroll);
1684            } else if let Some(parent_pipeline) = parent_pipeline {
1685                // Otherwise, if we have a parent (presumably from a different origin)
1686                // asynchronously ask the Constellation to forward the event to the parent
1687                // pipeline, if we have one.
1688                document.window().send_to_constellation(
1689                    ScriptToConstellationMessage::ForwardKeyboardScroll(parent_pipeline, scroll),
1690                );
1691            };
1692            return;
1693        }
1694
1695        let (current_scroll_offset, delta) = calculate_current_scroll_offset_and_delta();
1696        scrolling_box.scroll_to(delta + current_scroll_offset, ScrollBehavior::Auto);
1697    }
1698}