/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/event/mouseevent.rs
761 строка
27 KB
Simon Sapin
Resolve DOM/glyph positions during hit testing (#47048)
07 авг 2026, 10:07
Не верифицирован
07 авг 2026, 10:07
bffc115
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::Cell; use std::default::Default; use std::f64::consts::PI; use dom_struct::dom_struct; use euclid::Point2D; use js::context::JSContext; use js::rust::HandleObject; use keyboard_types::Modifiers; use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods; use script_bindings::dom::MutNullableDom; use script_bindings::match_domstring_ascii; use script_bindings::reflector::reflect_dom_object_with_proto; use script_traits::ConstellationInputEvent; use servo_base::text::Utf32CodeUnits; use style::Atom; use style_traits::CSSPixel; use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods; use crate::dom::bindings::codegen::Bindings::MouseEventBinding; use crate::dom::bindings::codegen::Bindings::MouseEventBinding::MouseEventMethods; use crate::dom::bindings::codegen::Bindings::UIEventBinding::UIEventMethods; use crate::dom::bindings::error::Fallible; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::str::DOMString; use crate::dom::document::FireMouseEventType; use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; use crate::dom::inputevent::HitTestResult; use crate::dom::node::Node; use crate::dom::pointerevent::{PointerEvent, PointerId}; use crate::dom::uievent::UIEvent; use crate::dom::window::Window; /// <https://w3c.github.io/uievents/#interface-mouseevent> #[dom_struct] pub(crate) struct MouseEvent { uievent: UIEvent, /// The point on the screen of where this [`MouseEvent`] was originally triggered, /// to use during the dispatch phase. /// /// See: /// <https://w3c.github.io/uievents/#dom-mouseevent-screenx> /// <https://w3c.github.io/uievents/#dom-mouseevent-screeny> #[no_trace] screen_point: Cell<Point2D<i32, CSSPixel>>, /// The point in the viewport of where this [`MouseEvent`] was originally triggered, /// to use during the dispatch phase. /// /// See: /// <https://w3c.github.io/uievents/#dom-mouseevent-clientx> /// <https://w3c.github.io/uievents/#dom-mouseevent-clienty> #[no_trace] client_point: Cell<Point2D<i32, CSSPixel>>, /// The point in the initial containing block of where this [`MouseEvent`] was /// originally triggered to use during the dispatch phase. /// /// See: /// <https://w3c.github.io/uievents/#dom-mouseevent-pagex> /// <https://w3c.github.io/uievents/#dom-mouseevent-pagey> #[no_trace] page_point: Cell<Point2D<i32, CSSPixel>>, /// The keyboard modifiers that were active when this mouse event was triggered. #[no_trace] modifiers: Cell<Modifiers>, /// <https://w3c.github.io/uievents/#dom-mouseevent-button> button: Cell<i16>, /// <https://w3c.github.io/uievents/#dom-mouseevent-buttons> buttons: Cell<u16>, #[no_trace] point_in_target: Cell<Option<Point2D<f32, CSSPixel>>>, /// Together with `dom_position_offset`, if both are present, a position in the DOM tree /// near the cursor at time of this event. dom_position_container: MutNullableDom<Node>, /// Together with `dom_position_container`, if both are present, a position in the DOM tree /// near the cursor at time of this event. #[no_trace] dom_position_offset: Cell<Option<Utf32CodeUnits>>, } impl MouseEvent { pub(crate) fn new_inherited() -> MouseEvent { MouseEvent { uievent: UIEvent::new_inherited(), screen_point: Cell::new(Default::default()), client_point: Cell::new(Default::default()), page_point: Cell::new(Default::default()), modifiers: Cell::new(Modifiers::empty()), button: Cell::new(0), buttons: Cell::new(0), point_in_target: Cell::new(None), dom_position_container: MutNullableDom::default(), dom_position_offset: Cell::new(None), } } pub(crate) fn new_uninitialized(cx: &mut JSContext, window: &Window) -> DomRoot<MouseEvent> { Self::new_uninitialized_with_proto(cx, window, None) } fn new_uninitialized_with_proto( cx: &mut JSContext, window: &Window, proto: Option<HandleObject>, ) -> DomRoot<MouseEvent> { reflect_dom_object_with_proto(cx, Box::new(MouseEvent::new_inherited()), window, proto) } #[allow(clippy::too_many_arguments)] pub(crate) fn new( cx: &mut JSContext, window: &Window, event_type: Atom, can_bubble: EventBubbles, cancelable: EventCancelable, view: Option<&Window>, detail: i32, screen_point: Point2D<i32, CSSPixel>, client_point: Point2D<i32, CSSPixel>, page_point: Point2D<i32, CSSPixel>, modifiers: Modifiers, button: i16, buttons: u16, related_target: Option<&EventTarget>, point_in_target: Option<Point2D<f32, CSSPixel>>, dom_position_for_selection: Option<&(DomRoot<Node>, Utf32CodeUnits)>, ) -> DomRoot<MouseEvent> { Self::new_with_proto( cx, window, None, event_type, can_bubble, cancelable, view, detail, screen_point, client_point, page_point, modifiers, button, buttons, related_target, point_in_target, dom_position_for_selection, ) } #[allow(clippy::too_many_arguments)] fn new_with_proto( cx: &mut JSContext, window: &Window, proto: Option<HandleObject>, event_type: Atom, can_bubble: EventBubbles, cancelable: EventCancelable, view: Option<&Window>, detail: i32, screen_point: Point2D<i32, CSSPixel>, client_point: Point2D<i32, CSSPixel>, page_point: Point2D<i32, CSSPixel>, modifiers: Modifiers, button: i16, buttons: u16, related_target: Option<&EventTarget>, point_in_target: Option<Point2D<f32, CSSPixel>>, dom_position_for_selection: Option<&(DomRoot<Node>, Utf32CodeUnits)>, ) -> DomRoot<MouseEvent> { let ev = MouseEvent::new_uninitialized_with_proto(cx, window, proto); ev.initialize_mouse_event( event_type, can_bubble, cancelable, view, detail, screen_point, client_point, page_point, modifiers, button, buttons, related_target, point_in_target, dom_position_for_selection, ); ev } /// <https://w3c.github.io/pointerevents/#initialize-a-mouseevent> #[expect(clippy::too_many_arguments)] pub(crate) fn initialize_mouse_event( &self, event_type: Atom, can_bubble: EventBubbles, cancelable: EventCancelable, view: Option<&Window>, detail: i32, screen_point: Point2D<i32, CSSPixel>, client_point: Point2D<i32, CSSPixel>, page_point: Point2D<i32, CSSPixel>, modifiers: Modifiers, button: i16, buttons: u16, related_target: Option<&EventTarget>, point_in_target: Option<Point2D<f32, CSSPixel>>, dom_position_for_selection: Option<&(DomRoot<Node>, Utf32CodeUnits)>, ) { self.uievent.initialize_ui_event( event_type, view.map(|window| window.upcast::<EventTarget>()), can_bubble, cancelable, ); self.uievent.set_detail(detail); self.screen_point.set(screen_point); self.client_point.set(client_point); self.page_point.set(page_point); self.modifiers.set(modifiers); self.button.set(button); self.buttons.set(buttons); self.upcast::<Event>().set_related_target(related_target); self.point_in_target.set(point_in_target); // Legacy mapping per spec: left/middle/right => 1/2/3 (button + 1), else 0. let w = if button >= 0 { (button as u32) + 1 } else { 0 }; self.uievent.set_which(w); if let Some((node, offset)) = dom_position_for_selection { self.dom_position_container.set(Some(node)); self.dom_position_offset.set(Some(*offset)); } } pub(crate) fn new_for_platform_motion_event( cx: &mut JSContext, window: &Window, event_name: FireMouseEventType, hit_test_result: &HitTestResult, input_event: &ConstellationInputEvent, ) -> DomRoot<Self> { // These values come from the event tables in // <https://w3c.github.io/pointerevents/#mouse-event-types>. let (bubbles, cancelable, composed) = match event_name { FireMouseEventType::Move | FireMouseEventType::Over | FireMouseEventType::Out => { (EventBubbles::Bubbles, EventCancelable::Cancelable, true) }, FireMouseEventType::Enter | FireMouseEventType::Leave => ( EventBubbles::DoesNotBubble, EventCancelable::NotCancelable, false, ), }; let mouse_event = Self::new( cx, window, Atom::from(event_name.as_str()), bubbles, cancelable, Some(window), 0i32, hit_test_result.point_in_frame.to_i32(), hit_test_result.point_in_frame.to_i32(), hit_test_result .point_relative_to_initial_containing_block .to_i32(), input_event.active_keyboard_modifiers, 0i16, input_event.pressed_mouse_buttons, None, None, hit_test_result.dom_position_for_selection.as_ref(), ); let event = mouse_event.upcast::<Event>(); event.set_composed(composed); event.set_trusted(true); mouse_event } pub(crate) fn dom_offset_for_selection(&self) -> Option<Utf32CodeUnits> { self.dom_position_offset.get() } /// Create a [MouseEvent] triggered by the embedder. /// /// <https://w3c.github.io/pointerevents/#create-a-cancelable-mouseevent> #[expect(clippy::too_many_arguments)] pub(crate) fn for_platform_button_event( cx: &mut JSContext, event_type: Atom, event: embedder_traits::MouseButtonEvent, pressed_mouse_buttons: u16, window: &Window, hit_test_result: &HitTestResult, modifiers: Modifiers, click_count: usize, ) -> DomRoot<Self> { let client_point = hit_test_result.point_in_frame.to_i32(); let page_point = hit_test_result .point_relative_to_initial_containing_block .to_i32(); let mouse_event = Self::new( cx, window, event_type, EventBubbles::Bubbles, EventCancelable::Cancelable, Some(window), click_count as i32, client_point, // TODO: Get real screen coordinates? client_point, page_point, modifiers, event.button.into(), pressed_mouse_buttons, None, Some(hit_test_result.point_in_node), hit_test_result.dom_position_for_selection.as_ref(), ); mouse_event.upcast::<Event>().set_trusted(true); mouse_event.upcast::<Event>().set_composed(true); mouse_event } /// Create a PointerEvent from this MouseEvent. /// <https://w3c.github.io/pointerevents/#the-primary-pointer> /// For mouse, the pointer ID is always -1, and is_primary is always true. pub(crate) fn to_pointer_event( &self, cx: &mut JSContext, event_type: Atom, ) -> DomRoot<crate::dom::pointerevent::PointerEvent> { // TODO: This function should almost certainly accept an enumn for the event type. let is_pointer_down = &*event_type == "pointerdown"; let is_pointer_move = &*event_type == "pointermove"; let is_pointer_up = &*event_type == "pointerup"; // Pressure is 0.5 when button is down, 0.0 when up let pressure = if is_pointer_down || (is_pointer_move && self.Buttons() != 0) { 0.5 } else { 0.0 }; let button = if is_pointer_down || is_pointer_up { self.Button() } else { -1 }; // https://w3c.github.io/pointerevents/#dfn-attributes-and-default-actions // For pointerenter and pointerleave events, the composed [DOM] attribute SHOULD be false; // for all other pointer events in the table above, the attribute SHOULD be true. let composed = !matches!(&*event_type, "pointerenter" | "pointerleave"); let window = self.global(); let window = window.as_window(); let pointer_event = PointerEvent::new( cx, window, event_type, EventBubbles::from(self.upcast::<Event>().Bubbles()), EventCancelable::from(self.upcast::<Event>().Cancelable()), self.uievent.GetView().as_deref(), self.uievent.Detail(), Point2D::new(self.ScreenX(), self.ScreenY()), Point2D::new(self.ClientX(), self.ClientY()), Point2D::new(self.PageX(), self.PageY()), self.modifiers.get(), button, self.Buttons(), self.GetRelatedTarget().as_deref(), self.point_in_target.get(), PointerId::Mouse as i32, // Mouse pointer ID is always -1 1, // width 1, // height pressure, 0.0, // tangential_pressure 0, // tilt_x 0, // tilt_y 0, // twist PI / 2.0, // altitude_angle (perpendicular to surface) 0.0, // azimuth_angle DOMString::from("mouse"), true, // is_primary (mouse is always primary) vec![], // coalesced_events vec![], // predicted_events ); pointer_event.upcast::<Event>().set_composed(composed); pointer_event } /// Create a PointerEvent for hover events (pointerover, pointerenter, pointerout, pointerleave). /// <https://w3c.github.io/pointerevents/#the-primary-pointer> /// For mouse, the pointer ID is always -1, and is_primary is always true. pub(crate) fn to_pointer_hover_event( &self, cx: &mut JSContext, event_type: &str, ) -> DomRoot<crate::dom::pointerevent::PointerEvent> { // Determine bubbles and cancelable based on event type // pointerover/pointerout bubble and are cancelable // pointerenter/pointerleave do not bubble and are not cancelable let (bubbles, cancelable) = match event_type { "pointerover" | "pointerout" => (EventBubbles::Bubbles, EventCancelable::Cancelable), "pointerenter" | "pointerleave" => { (EventBubbles::DoesNotBubble, EventCancelable::NotCancelable) }, _ => (EventBubbles::Bubbles, EventCancelable::Cancelable), }; let window = self.global(); let window = window.as_window(); let pointer_event = PointerEvent::new( cx, window, event_type.into(), bubbles, cancelable, self.uievent.GetView().as_deref(), self.uievent.Detail(), Point2D::new(self.ScreenX(), self.ScreenY()), Point2D::new(self.ClientX(), self.ClientY()), Point2D::new(self.PageX(), self.PageY()), self.modifiers.get(), -1, // button: -1 for hover events (no button pressed) self.Buttons(), self.GetRelatedTarget().as_deref(), self.point_in_target.get(), PointerId::Mouse as i32, // Mouse pointer ID is always -1 1, // width 1, // height 0.0, // pressure: 0.0 for hover events 0.0, // tangential_pressure 0, // tilt_x 0, // tilt_y 0, // twist PI / 2.0, // altitude_angle (perpendicular to surface) 0.0, // azimuth_angle DOMString::from("mouse"), true, // is_primary (mouse is always primary) vec![], // coalesced_events vec![], // predicted_events ); // https://w3c.github.io/pointerevents/#dfn-attributes-and-default-actions // For pointerenter and pointerleave events, the composed [DOM] attribute SHOULD be false; // for all other pointer events in the table above, the attribute SHOULD be true. let composed = !matches!(event_type, "pointerenter" | "pointerleave"); pointer_event.upcast::<Event>().set_composed(composed); // Set trusted to match the source mouse event pointer_event .upcast::<Event>() .set_trusted(self.IsTrusted()); pointer_event } } impl MouseEventMethods<crate::DomTypeHolder> for MouseEvent { /// <https://w3c.github.io/pointerevents/#dom-mouseevent-constructor> fn Constructor( cx: &mut JSContext, window: &Window, proto: Option<HandleObject>, event_type: DOMString, init: &MouseEventBinding::MouseEventInit, ) -> Fallible<DomRoot<MouseEvent>> { let bubbles = EventBubbles::from(init.parent.parent.parent.bubbles); let cancelable = EventCancelable::from(init.parent.parent.parent.cancelable); let scroll_offset = window.scroll_offset(); let page_point = Point2D::new( scroll_offset.x as i32 + init.clientX, scroll_offset.y as i32 + init.clientY, ); let event = MouseEvent::new_with_proto( cx, window, proto, event_type.into(), bubbles, cancelable, init.parent.parent.view.as_deref(), init.parent.parent.detail, Point2D::new(init.screenX, init.screenY), Point2D::new(init.clientX, init.clientY), page_point, init.parent.modifiers(), init.button, init.buttons, init.relatedTarget.as_deref(), None, None, ); event .upcast::<Event>() .set_composed(init.parent.parent.parent.composed); Ok(event) } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-screenx> fn ScreenX(&self) -> i32 { self.screen_point.get().x } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-screeny> fn ScreenY(&self) -> i32 { self.screen_point.get().y } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-clientx> fn ClientX(&self) -> i32 { self.client_point.get().x } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-clienty> fn ClientY(&self) -> i32 { self.client_point.get().y } /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-pagex> fn PageX(&self) -> i32 { // The pageX attribute must follow these steps: // > 1. If the event’s dispatch flag is set, return the horizontal coordinate of the // > position where the event occurred relative to the origin of the initial containing // > block and terminate these steps. if self.upcast::<Event>().dispatching() { return self.page_point.get().x; } // > 2. Let offset be the value of the scrollX attribute of the event’s associated // > Window object, if there is one, or zero otherwise. // > 3. Return the sum of offset and the value of the event’s clientX attribute. self.global().as_window().ScrollX() + self.ClientX() } /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-pagey> fn PageY(&self) -> i32 { // The pageY attribute must follow these steps: // > 1. If the event’s dispatch flag is set, return the vertical coordinate of the // > position where the event occurred relative to the origin of the initial // > containing block and terminate these steps. if self.upcast::<Event>().dispatching() { return self.page_point.get().y; } // > 2. Let offset be the value of the scrollY attribute of the event’s associated // > Window object, if there is one, or zero otherwise. // > 3. Return the sum of offset and the value of the event’s clientY attribute. self.global().as_window().ScrollY() + self.ClientY() } /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-x> fn X(&self) -> i32 { self.ClientX() } /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-y> fn Y(&self) -> i32 { self.ClientY() } /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-offsetx> fn OffsetX(&self) -> i32 { // > The offsetX attribute must follow these steps: // > 1. If the event’s dispatch flag is set, return the x-coordinate of the position // > where the event occurred relative to the origin of the padding edge of the // > target node, ignoring the transforms that apply to the element and its // > ancestors, and terminate these steps. let event = self.upcast::<Event>(); if event.dispatching() { let Some(target) = event.GetTarget() else { return 0; }; let Some(node) = target.downcast::<Node>() else { return 0; }; return self.ClientX() - node.client_rect().origin.x; } // > 2. Return the value of the event’s pageX attribute. self.PageX() } /// <https://drafts.csswg.org/cssom-view/#dom-mouseevent-offsety> fn OffsetY(&self) -> i32 { // > The offsetY attribute must follow these steps: // > 1. If the event’s dispatch flag is set, return the y-coordinate of the // > position where the event occurred relative to the origin of the padding edge of // > the target node, ignoring the transforms that apply to the element and its // > ancestors, and terminate these steps. let event = self.upcast::<Event>(); if event.dispatching() { let Some(target) = event.GetTarget() else { return 0; }; let Some(node) = target.downcast::<Node>() else { return 0; }; return self.ClientY() - node.client_rect().origin.y; } // 2. Return the value of the event’s pageY attribute. self.PageY() } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-ctrlkey> fn CtrlKey(&self) -> bool { self.modifiers.get().contains(Modifiers::CONTROL) } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-shiftkey> fn ShiftKey(&self) -> bool { self.modifiers.get().contains(Modifiers::SHIFT) } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-altkey> fn AltKey(&self) -> bool { self.modifiers.get().contains(Modifiers::ALT) } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-metakey> fn MetaKey(&self) -> bool { self.modifiers.get().contains(Modifiers::META) } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-button> fn Button(&self) -> i16 { self.button.get() } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-buttons> fn Buttons(&self) -> u16 { self.buttons.get() } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-relatedtarget> fn GetRelatedTarget(&self) -> Option<DomRoot<EventTarget>> { self.upcast::<Event>().related_target() } /// <https://w3c.github.io/pointerevents/#dom-mouseevent-initmouseevent> fn InitMouseEvent( &self, type_arg: DOMString, can_bubble_arg: bool, cancelable_arg: bool, view_arg: Option<&Window>, detail_arg: i32, screen_x_arg: i32, screen_y_arg: i32, client_x_arg: i32, client_y_arg: i32, ctrl_key_arg: bool, alt_key_arg: bool, shift_key_arg: bool, meta_key_arg: bool, button_arg: i16, related_target_arg: Option<&EventTarget>, ) { if self.upcast::<Event>().dispatching() { return; } self.upcast::<UIEvent>().InitUIEvent( type_arg, can_bubble_arg, cancelable_arg, view_arg, detail_arg, ); self.screen_point .set(Point2D::new(screen_x_arg, screen_y_arg)); self.client_point .set(Point2D::new(client_x_arg, client_y_arg)); let global = self.global(); let scroll_offset = global.as_window().scroll_offset(); self.page_point.set(Point2D::new( scroll_offset.x as i32 + client_x_arg, scroll_offset.y as i32 + client_y_arg, )); let mut modifiers = Modifiers::empty(); if ctrl_key_arg { modifiers.insert(Modifiers::CONTROL); } if alt_key_arg { modifiers.insert(Modifiers::ALT); } if shift_key_arg { modifiers.insert(Modifiers::SHIFT); } if meta_key_arg { modifiers.insert(Modifiers::META); } self.modifiers.set(modifiers); self.button.set(button_arg); self.upcast::<Event>() .set_related_target(related_target_arg); // Keep UIEvent.which in sync for legacy init path too. let w = if button_arg >= 0 { (button_arg as u32) + 1 } else { 0 }; self.uievent.set_which(w); } /// <https://dom.spec.whatwg.org/#dom-event-istrusted> fn IsTrusted(&self) -> bool { self.uievent.IsTrusted() } /// <https://w3c.github.io/pointerevents/#dfn-getmodifierstate-keyarg> fn GetModifierState(&self, key_arg: DOMString) -> bool { self.modifiers .get() .contains(match_domstring_ascii!(key_arg, "Alt" => Modifiers::ALT, "AltGraph" => Modifiers::ALT_GRAPH, "CapsLock" => Modifiers::CAPS_LOCK, "Control" => Modifiers::CONTROL, "Fn" => Modifiers::FN, "FnLock" => Modifiers::FN_LOCK, "Meta" => Modifiers::META, "NumLock" => Modifiers::NUM_LOCK, "ScrollLock" => Modifiers::SCROLL_LOCK, "Shift" => Modifiers::SHIFT, "Symbol" => Modifiers::SYMBOL, "SymbolLock" => Modifiers::SYMBOL_LOCK, _ => { return false; }, )) } }