script/dom/
mouseevent.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::cell::Cell;
6use std::default::Default;
7use std::f64::consts::PI;
8
9use dom_struct::dom_struct;
10use euclid::Point2D;
11use js::rust::HandleObject;
12use keyboard_types::Modifiers;
13use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
14use script_bindings::match_domstring_ascii;
15use script_traits::ConstellationInputEvent;
16use style_traits::CSSPixel;
17
18use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
19use crate::dom::bindings::codegen::Bindings::MouseEventBinding;
20use crate::dom::bindings::codegen::Bindings::MouseEventBinding::MouseEventMethods;
21use crate::dom::bindings::codegen::Bindings::UIEventBinding::UIEventMethods;
22use crate::dom::bindings::error::Fallible;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto};
25use crate::dom::bindings::root::DomRoot;
26use crate::dom::bindings::str::DOMString;
27use crate::dom::document::FireMouseEventType;
28use crate::dom::event::{Event, EventBubbles, EventCancelable};
29use crate::dom::eventtarget::EventTarget;
30use crate::dom::inputevent::HitTestResult;
31use crate::dom::node::Node;
32use crate::dom::pointerevent::{PointerEvent, PointerId};
33use crate::dom::uievent::UIEvent;
34use crate::dom::window::Window;
35use crate::script_runtime::CanGc;
36
37/// <https://w3c.github.io/uievents/#interface-mouseevent>
38#[dom_struct]
39pub(crate) struct MouseEvent {
40    uievent: UIEvent,
41
42    /// The point on the screen of where this [`MouseEvent`] was originally triggered,
43    /// to use during the dispatch phase.
44    ///
45    /// See:
46    /// <https://w3c.github.io/uievents/#dom-mouseevent-screenx>
47    /// <https://w3c.github.io/uievents/#dom-mouseevent-screeny>
48    #[no_trace]
49    screen_point: Cell<Point2D<i32, CSSPixel>>,
50
51    /// The point in the viewport of where this [`MouseEvent`] was originally triggered,
52    /// to use during the dispatch phase.
53    ///
54    /// See:
55    /// <https://w3c.github.io/uievents/#dom-mouseevent-clientx>
56    /// <https://w3c.github.io/uievents/#dom-mouseevent-clienty>
57    #[no_trace]
58    client_point: Cell<Point2D<i32, CSSPixel>>,
59
60    /// The point in the initial containing block of where this [`MouseEvent`] was
61    /// originally triggered to use during the dispatch phase.
62    ///
63    /// See:
64    /// <https://w3c.github.io/uievents/#dom-mouseevent-pagex>
65    /// <https://w3c.github.io/uievents/#dom-mouseevent-pagey>
66    #[no_trace]
67    page_point: Cell<Point2D<i32, CSSPixel>>,
68
69    /// The keyboard modifiers that were active when this mouse event was triggered.
70    #[no_trace]
71    modifiers: Cell<Modifiers>,
72
73    /// <https://w3c.github.io/uievents/#dom-mouseevent-button>
74    button: Cell<i16>,
75
76    /// <https://w3c.github.io/uievents/#dom-mouseevent-buttons>
77    buttons: Cell<u16>,
78
79    #[no_trace]
80    point_in_target: Cell<Option<Point2D<f32, CSSPixel>>>,
81}
82
83impl MouseEvent {
84    pub(crate) fn new_inherited() -> MouseEvent {
85        MouseEvent {
86            uievent: UIEvent::new_inherited(),
87            screen_point: Cell::new(Default::default()),
88            client_point: Cell::new(Default::default()),
89            page_point: Cell::new(Default::default()),
90            modifiers: Cell::new(Modifiers::empty()),
91            button: Cell::new(0),
92            buttons: Cell::new(0),
93            point_in_target: Cell::new(None),
94        }
95    }
96
97    pub(crate) fn new_uninitialized(window: &Window, can_gc: CanGc) -> DomRoot<MouseEvent> {
98        Self::new_uninitialized_with_proto(window, None, can_gc)
99    }
100
101    fn new_uninitialized_with_proto(
102        window: &Window,
103        proto: Option<HandleObject>,
104        can_gc: CanGc,
105    ) -> DomRoot<MouseEvent> {
106        reflect_dom_object_with_proto(Box::new(MouseEvent::new_inherited()), window, proto, can_gc)
107    }
108
109    #[allow(clippy::too_many_arguments)]
110    pub(crate) fn new(
111        window: &Window,
112        type_: DOMString,
113        can_bubble: EventBubbles,
114        cancelable: EventCancelable,
115        view: Option<&Window>,
116        detail: i32,
117        screen_point: Point2D<i32, CSSPixel>,
118        client_point: Point2D<i32, CSSPixel>,
119        page_point: Point2D<i32, CSSPixel>,
120        modifiers: Modifiers,
121        button: i16,
122        buttons: u16,
123        related_target: Option<&EventTarget>,
124        point_in_target: Option<Point2D<f32, CSSPixel>>,
125        can_gc: CanGc,
126    ) -> DomRoot<MouseEvent> {
127        Self::new_with_proto(
128            window,
129            None,
130            type_,
131            can_bubble,
132            cancelable,
133            view,
134            detail,
135            screen_point,
136            client_point,
137            page_point,
138            modifiers,
139            button,
140            buttons,
141            related_target,
142            point_in_target,
143            can_gc,
144        )
145    }
146
147    #[allow(clippy::too_many_arguments)]
148    fn new_with_proto(
149        window: &Window,
150        proto: Option<HandleObject>,
151        type_: DOMString,
152        can_bubble: EventBubbles,
153        cancelable: EventCancelable,
154        view: Option<&Window>,
155        detail: i32,
156        screen_point: Point2D<i32, CSSPixel>,
157        client_point: Point2D<i32, CSSPixel>,
158        page_point: Point2D<i32, CSSPixel>,
159        modifiers: Modifiers,
160        button: i16,
161        buttons: u16,
162        related_target: Option<&EventTarget>,
163        point_in_target: Option<Point2D<f32, CSSPixel>>,
164        can_gc: CanGc,
165    ) -> DomRoot<MouseEvent> {
166        let ev = MouseEvent::new_uninitialized_with_proto(window, proto, can_gc);
167        ev.initialize_mouse_event(
168            type_,
169            can_bubble,
170            cancelable,
171            view,
172            detail,
173            screen_point,
174            client_point,
175            page_point,
176            modifiers,
177            button,
178            buttons,
179            related_target,
180            point_in_target,
181        );
182        ev
183    }
184
185    /// <https://w3c.github.io/uievents/#initialize-a-mouseevent>
186    #[expect(clippy::too_many_arguments)]
187    pub(crate) fn initialize_mouse_event(
188        &self,
189        type_: DOMString,
190        can_bubble: EventBubbles,
191        cancelable: EventCancelable,
192        view: Option<&Window>,
193        detail: i32,
194        screen_point: Point2D<i32, CSSPixel>,
195        client_point: Point2D<i32, CSSPixel>,
196        page_point: Point2D<i32, CSSPixel>,
197        modifiers: Modifiers,
198        button: i16,
199        buttons: u16,
200        related_target: Option<&EventTarget>,
201        point_in_target: Option<Point2D<f32, CSSPixel>>,
202    ) {
203        self.uievent.initialize_ui_event(
204            type_,
205            view.map(|window| window.upcast::<EventTarget>()),
206            can_bubble,
207            cancelable,
208        );
209
210        self.uievent.set_detail(detail);
211        self.screen_point.set(screen_point);
212        self.client_point.set(client_point);
213        self.page_point.set(page_point);
214        self.modifiers.set(modifiers);
215        self.button.set(button);
216        self.buttons.set(buttons);
217        self.upcast::<Event>().set_related_target(related_target);
218        self.point_in_target.set(point_in_target);
219        // Legacy mapping per spec: left/middle/right => 1/2/3 (button + 1), else 0.
220        let w = if button >= 0 { (button as u32) + 1 } else { 0 };
221        self.uievent.set_which(w);
222    }
223
224    pub(crate) fn new_for_platform_motion_event(
225        window: &Window,
226        event_name: FireMouseEventType,
227        hit_test_result: &HitTestResult,
228        input_event: &ConstellationInputEvent,
229        can_gc: CanGc,
230    ) -> DomRoot<Self> {
231        // These values come from the event tables in
232        // <https://w3c.github.io/uievents/#events-mouse-types>.
233        let (bubbles, cancelable, composed) = match event_name {
234            FireMouseEventType::Move | FireMouseEventType::Over | FireMouseEventType::Out => {
235                (EventBubbles::Bubbles, EventCancelable::Cancelable, true)
236            },
237            FireMouseEventType::Enter | FireMouseEventType::Leave => (
238                EventBubbles::DoesNotBubble,
239                EventCancelable::NotCancelable,
240                false,
241            ),
242        };
243
244        let mouse_event = Self::new(
245            window,
246            DOMString::from(event_name.as_str()),
247            bubbles,
248            cancelable,
249            Some(window),
250            0i32,
251            hit_test_result.point_in_frame.to_i32(),
252            hit_test_result.point_in_frame.to_i32(),
253            hit_test_result
254                .point_relative_to_initial_containing_block
255                .to_i32(),
256            input_event.active_keyboard_modifiers,
257            0i16,
258            input_event.pressed_mouse_buttons,
259            None,
260            None,
261            can_gc,
262        );
263
264        let event = mouse_event.upcast::<Event>();
265        event.set_composed(composed);
266        event.set_trusted(true);
267
268        mouse_event
269    }
270
271    /// Create a [MouseEvent] triggered by the embedder
272    /// <https://w3c.github.io/uievents/#create-a-cancelable-mouseevent-id>
273    #[expect(clippy::too_many_arguments)]
274    pub(crate) fn for_platform_button_event(
275        event_type_string: &'static str,
276        event: embedder_traits::MouseButtonEvent,
277        pressed_mouse_buttons: u16,
278        window: &Window,
279        hit_test_result: &HitTestResult,
280        modifiers: Modifiers,
281        click_count: usize,
282        can_gc: CanGc,
283    ) -> DomRoot<Self> {
284        let client_point = hit_test_result.point_in_frame.to_i32();
285        let page_point = hit_test_result
286            .point_relative_to_initial_containing_block
287            .to_i32();
288
289        let mouse_event = Self::new(
290            window,
291            event_type_string.into(),
292            EventBubbles::Bubbles,
293            EventCancelable::Cancelable,
294            Some(window),
295            click_count as i32,
296            client_point, // TODO: Get real screen coordinates?
297            client_point,
298            page_point,
299            modifiers,
300            event.button.into(),
301            pressed_mouse_buttons,
302            None,
303            Some(hit_test_result.point_in_node),
304            can_gc,
305        );
306
307        mouse_event.upcast::<Event>().set_trusted(true);
308        mouse_event.upcast::<Event>().set_composed(true);
309
310        mouse_event
311    }
312
313    pub(crate) fn point_in_viewport(&self) -> Option<Point2D<f32, CSSPixel>> {
314        Some(self.client_point.get().to_f32())
315    }
316
317    /// Create a PointerEvent from this MouseEvent.
318    /// <https://w3c.github.io/pointerevents/#the-primary-pointer>
319    /// For mouse, the pointer ID is always -1, and is_primary is always true.
320    pub(crate) fn to_pointer_event(
321        &self,
322        event_type: &str,
323        can_gc: CanGc,
324    ) -> DomRoot<crate::dom::pointerevent::PointerEvent> {
325        // Pressure is 0.5 when button is down, 0.0 when up
326        let pressure = if event_type == "pointerdown" ||
327            (event_type == "pointermove" && self.Buttons() != 0)
328        {
329            0.5
330        } else {
331            0.0
332        };
333
334        let button = if event_type == "pointerdown" || event_type == "pointerup" {
335            self.Button()
336        } else {
337            -1
338        };
339
340        let window = self.global();
341        let window = window.as_window();
342
343        PointerEvent::new(
344            window,
345            DOMString::from(event_type),
346            EventBubbles::from(self.upcast::<Event>().Bubbles()),
347            EventCancelable::from(self.upcast::<Event>().Cancelable()),
348            self.uievent.GetView().as_deref(),
349            self.uievent.Detail(),
350            Point2D::new(self.ScreenX(), self.ScreenY()),
351            Point2D::new(self.ClientX(), self.ClientY()),
352            Point2D::new(self.PageX(), self.PageY()),
353            self.modifiers.get(),
354            button,
355            self.Buttons(),
356            self.GetRelatedTarget().as_deref(),
357            self.point_in_target.get(),
358            PointerId::Mouse as i32, // Mouse pointer ID is always -1
359            1,                       // width
360            1,                       // height
361            pressure,
362            0.0,      // tangential_pressure
363            0,        // tilt_x
364            0,        // tilt_y
365            0,        // twist
366            PI / 2.0, // altitude_angle (perpendicular to surface)
367            0.0,      // azimuth_angle
368            DOMString::from("mouse"),
369            true,   // is_primary (mouse is always primary)
370            vec![], // coalesced_events
371            vec![], // predicted_events
372            can_gc,
373        )
374    }
375}
376
377impl MouseEventMethods<crate::DomTypeHolder> for MouseEvent {
378    /// <https://w3c.github.io/uievents/#dom-mouseevent-mouseevent>
379    fn Constructor(
380        window: &Window,
381        proto: Option<HandleObject>,
382        can_gc: CanGc,
383        type_: DOMString,
384        init: &MouseEventBinding::MouseEventInit,
385    ) -> Fallible<DomRoot<MouseEvent>> {
386        let bubbles = EventBubbles::from(init.parent.parent.parent.bubbles);
387        let cancelable = EventCancelable::from(init.parent.parent.parent.cancelable);
388        let scroll_offset = window.scroll_offset();
389        let page_point = Point2D::new(
390            scroll_offset.x as i32 + init.clientX,
391            scroll_offset.y as i32 + init.clientY,
392        );
393        let event = MouseEvent::new_with_proto(
394            window,
395            proto,
396            type_,
397            bubbles,
398            cancelable,
399            init.parent.parent.view.as_deref(),
400            init.parent.parent.detail,
401            Point2D::new(init.screenX, init.screenY),
402            Point2D::new(init.clientX, init.clientY),
403            page_point,
404            init.parent.modifiers(),
405            init.button,
406            init.buttons,
407            init.relatedTarget.as_deref(),
408            None,
409            can_gc,
410        );
411        event
412            .upcast::<Event>()
413            .set_composed(init.parent.parent.parent.composed);
414        Ok(event)
415    }
416
417    /// <https://w3c.github.io/uievents/#widl-MouseEvent-screenX>
418    fn ScreenX(&self) -> i32 {
419        self.screen_point.get().x
420    }
421
422    /// <https://w3c.github.io/uievents/#widl-MouseEvent-screenY>
423    fn ScreenY(&self) -> i32 {
424        self.screen_point.get().y
425    }
426
427    /// <https://w3c.github.io/uievents/#widl-MouseEvent-clientX>
428    fn ClientX(&self) -> i32 {
429        self.client_point.get().x
430    }
431
432    /// <https://w3c.github.io/uievents/#widl-MouseEvent-clientY>
433    fn ClientY(&self) -> i32 {
434        self.client_point.get().y
435    }
436
437    /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-pagex>
438    fn PageX(&self) -> i32 {
439        // The pageX attribute must follow these steps:
440        // > 1. If the event’s dispatch flag is set, return the horizontal coordinate of the
441        // > position where the event occurred relative to the origin of the initial containing
442        // > block and terminate these steps.
443        if self.upcast::<Event>().dispatching() {
444            return self.page_point.get().x;
445        }
446
447        // > 2. Let offset be the value of the scrollX attribute of the event’s associated
448        // > Window object, if there is one, or zero otherwise.
449        // > 3. Return the sum of offset and the value of the event’s clientX attribute.
450        self.global().as_window().ScrollX() + self.ClientX()
451    }
452
453    /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-pagey>
454    fn PageY(&self) -> i32 {
455        // The pageY attribute must follow these steps:
456        // > 1. If the event’s dispatch flag is set, return the vertical coordinate of the
457        // > position where the event occurred relative to the origin of the initial
458        // > containing block and terminate these steps.
459        if self.upcast::<Event>().dispatching() {
460            return self.page_point.get().y;
461        }
462
463        // > 2. Let offset be the value of the scrollY attribute of the event’s associated
464        // > Window object, if there is one, or zero otherwise.
465        // > 3. Return the sum of offset and the value of the event’s clientY attribute.
466        self.global().as_window().ScrollY() + self.ClientY()
467    }
468
469    /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-x>
470    fn X(&self) -> i32 {
471        self.ClientX()
472    }
473
474    /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-y>
475    fn Y(&self) -> i32 {
476        self.ClientY()
477    }
478
479    /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-offsetx>
480    fn OffsetX(&self) -> i32 {
481        // > The offsetX attribute must follow these steps:
482        // > 1. If the event’s dispatch flag is set, return the x-coordinate of the position
483        // >    where the event occurred relative to the origin of the padding edge of the
484        // >    target node, ignoring the transforms that apply to the element and its
485        // >    ancestors, and terminate these steps.
486        let event = self.upcast::<Event>();
487        if event.dispatching() {
488            let Some(target) = event.GetTarget() else {
489                return 0;
490            };
491            let Some(node) = target.downcast::<Node>() else {
492                return 0;
493            };
494            return self.ClientX() - node.client_rect().origin.x;
495        }
496
497        // > 2. Return the value of the event’s pageX attribute.
498        self.PageX()
499    }
500
501    /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-offsety>
502    fn OffsetY(&self) -> i32 {
503        // > The offsetY attribute must follow these steps:
504        // > 1. If the event’s dispatch flag is set, return the y-coordinate of the
505        // >    position where the event occurred relative to the origin of the padding edge of
506        // >    the target node, ignoring the transforms that apply to the element and its
507        // >    ancestors, and terminate these steps.
508        let event = self.upcast::<Event>();
509        if event.dispatching() {
510            let Some(target) = event.GetTarget() else {
511                return 0;
512            };
513            let Some(node) = target.downcast::<Node>() else {
514                return 0;
515            };
516            return self.ClientY() - node.client_rect().origin.y;
517        }
518
519        // 2. Return the value of the event’s pageY attribute.
520        self.PageY()
521    }
522
523    /// <https://w3c.github.io/uievents/#dom-mouseevent-ctrlkey>
524    fn CtrlKey(&self) -> bool {
525        self.modifiers.get().contains(Modifiers::CONTROL)
526    }
527
528    /// <https://w3c.github.io/uievents/#dom-mouseevent-shiftkey>
529    fn ShiftKey(&self) -> bool {
530        self.modifiers.get().contains(Modifiers::SHIFT)
531    }
532
533    /// <https://w3c.github.io/uievents/#dom-mouseevent-altkey>
534    fn AltKey(&self) -> bool {
535        self.modifiers.get().contains(Modifiers::ALT)
536    }
537
538    /// <https://w3c.github.io/uievents/#dom-mouseevent-metakey>
539    fn MetaKey(&self) -> bool {
540        self.modifiers.get().contains(Modifiers::META)
541    }
542
543    /// <https://w3c.github.io/uievents/#dom-mouseevent-button>
544    fn Button(&self) -> i16 {
545        self.button.get()
546    }
547
548    /// <https://w3c.github.io/uievents/#dom-mouseevent-buttons>
549    fn Buttons(&self) -> u16 {
550        self.buttons.get()
551    }
552
553    /// <https://w3c.github.io/uievents/#widl-MouseEvent-relatedTarget>
554    fn GetRelatedTarget(&self) -> Option<DomRoot<EventTarget>> {
555        self.upcast::<Event>().related_target()
556    }
557
558    /// <https://w3c.github.io/uievents/#widl-MouseEvent-initMouseEvent>
559    fn InitMouseEvent(
560        &self,
561        type_arg: DOMString,
562        can_bubble_arg: bool,
563        cancelable_arg: bool,
564        view_arg: Option<&Window>,
565        detail_arg: i32,
566        screen_x_arg: i32,
567        screen_y_arg: i32,
568        client_x_arg: i32,
569        client_y_arg: i32,
570        ctrl_key_arg: bool,
571        alt_key_arg: bool,
572        shift_key_arg: bool,
573        meta_key_arg: bool,
574        button_arg: i16,
575        related_target_arg: Option<&EventTarget>,
576    ) {
577        if self.upcast::<Event>().dispatching() {
578            return;
579        }
580
581        self.upcast::<UIEvent>().InitUIEvent(
582            type_arg,
583            can_bubble_arg,
584            cancelable_arg,
585            view_arg,
586            detail_arg,
587        );
588        self.screen_point
589            .set(Point2D::new(screen_x_arg, screen_y_arg));
590        self.client_point
591            .set(Point2D::new(client_x_arg, client_y_arg));
592
593        let global = self.global();
594        let scroll_offset = global.as_window().scroll_offset();
595        self.page_point.set(Point2D::new(
596            scroll_offset.x as i32 + client_x_arg,
597            scroll_offset.y as i32 + client_y_arg,
598        ));
599
600        let mut modifiers = Modifiers::empty();
601        if ctrl_key_arg {
602            modifiers.insert(Modifiers::CONTROL);
603        }
604        if alt_key_arg {
605            modifiers.insert(Modifiers::ALT);
606        }
607        if shift_key_arg {
608            modifiers.insert(Modifiers::SHIFT);
609        }
610        if meta_key_arg {
611            modifiers.insert(Modifiers::META);
612        }
613        self.modifiers.set(modifiers);
614
615        self.button.set(button_arg);
616        self.upcast::<Event>()
617            .set_related_target(related_target_arg);
618
619        // Keep UIEvent.which in sync for legacy init path too.
620        let w = if button_arg >= 0 {
621            (button_arg as u32) + 1
622        } else {
623            0
624        };
625        self.uievent.set_which(w);
626    }
627
628    /// <https://dom.spec.whatwg.org/#dom-event-istrusted>
629    fn IsTrusted(&self) -> bool {
630        self.uievent.IsTrusted()
631    }
632
633    /// <https://w3c.github.io/uievents/#dom-mouseevent-getmodifierstate>
634    fn GetModifierState(&self, key_arg: DOMString) -> bool {
635        self.modifiers
636            .get()
637            .contains(match_domstring_ascii!(key_arg,
638                "Alt" => Modifiers::ALT,
639                "AltGraph" => Modifiers::ALT_GRAPH,
640                "CapsLock" => Modifiers::CAPS_LOCK,
641                "Control" => Modifiers::CONTROL,
642                "Fn" => Modifiers::FN,
643                "FnLock" => Modifiers::FN_LOCK,
644                "Meta" => Modifiers::META,
645                "NumLock" => Modifiers::NUM_LOCK,
646                "ScrollLock" => Modifiers::SCROLL_LOCK,
647                "Shift" => Modifiers::SHIFT,
648                "Symbol" => Modifiers::SYMBOL,
649                "SymbolLock" => Modifiers::SYMBOL_LOCK,
650                    _ => { return false; },
651            ))
652    }
653}