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