mirror of
https://github.com/servo/servo
synced 2026-05-09 08:32:31 +02:00
script: pointer events: pointerenter, pointerout, pointerleave, pointerover (#42736)
Adds support for more pointer events: pointerenter, pointerout, pointerleave, pointerover Also add global event handlers that were missing. Testing: WPT expectations are updated. cc @yezhizhen Signed-off-by: webbeef <me@webbeef.org>
This commit is contained in:
@@ -341,15 +341,24 @@ impl DocumentEventHandler {
|
||||
.get()
|
||||
.and_then(|point| self.window.hit_test_from_point_in_viewport(point))
|
||||
{
|
||||
MouseEvent::new_for_platform_motion_event(
|
||||
let mouse_out_event = MouseEvent::new_for_platform_motion_event(
|
||||
&self.window,
|
||||
FireMouseEventType::Out,
|
||||
&hit_test_result,
|
||||
input_event,
|
||||
can_gc,
|
||||
)
|
||||
.upcast::<Event>()
|
||||
.fire(current_hover_target.upcast(), can_gc);
|
||||
);
|
||||
|
||||
// Fire pointerout before mouseout
|
||||
mouse_out_event
|
||||
.to_pointer_hover_event("pointerout", can_gc)
|
||||
.upcast::<Event>()
|
||||
.fire(current_hover_target.upcast(), can_gc);
|
||||
|
||||
mouse_out_event
|
||||
.upcast::<Event>()
|
||||
.fire(current_hover_target.upcast(), can_gc);
|
||||
|
||||
self.handle_mouse_enter_leave_event(
|
||||
DomRoot::from_ref(current_hover_target),
|
||||
None,
|
||||
@@ -413,22 +422,35 @@ impl DocumentEventHandler {
|
||||
targets.push(node);
|
||||
}
|
||||
|
||||
// The order for dispatching mouseenter events starts from the topmost
|
||||
// The order for dispatching mouseenter/pointerenter events starts from the topmost
|
||||
// common ancestor of the event target and the related target.
|
||||
if event_type == FireMouseEventType::Enter {
|
||||
targets = targets.into_iter().rev().collect();
|
||||
}
|
||||
|
||||
let pointer_event_name = match event_type {
|
||||
FireMouseEventType::Enter => "pointerenter",
|
||||
FireMouseEventType::Leave => "pointerleave",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
for target in targets {
|
||||
MouseEvent::new_for_platform_motion_event(
|
||||
let mouse_event = MouseEvent::new_for_platform_motion_event(
|
||||
&self.window,
|
||||
event_type,
|
||||
hit_test_result,
|
||||
input_event,
|
||||
can_gc,
|
||||
)
|
||||
.upcast::<Event>()
|
||||
.fire(target.upcast(), can_gc);
|
||||
);
|
||||
|
||||
// Fire pointer event before mouse event
|
||||
mouse_event
|
||||
.to_pointer_hover_event(pointer_event_name, can_gc)
|
||||
.upcast::<Event>()
|
||||
.fire(target.upcast(), can_gc);
|
||||
|
||||
// Fire mouse event
|
||||
mouse_event.upcast::<Event>().fire(target.upcast(), can_gc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +487,7 @@ impl DocumentEventHandler {
|
||||
// Here we know the target has changed, so we must update the state,
|
||||
// dispatch mouseout to the previous one, mouseover to the new one.
|
||||
if target_has_changed {
|
||||
// Dispatch mouseout and mouseleave to previous target.
|
||||
// Dispatch pointerout/mouseout and pointerleave/mouseleave to previous target.
|
||||
if let Some(old_target) = self.current_hover_target.get() {
|
||||
let old_target_is_ancestor_of_new_target = old_target
|
||||
.upcast::<Node>()
|
||||
@@ -484,15 +506,23 @@ impl DocumentEventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
MouseEvent::new_for_platform_motion_event(
|
||||
let mouse_out_event = MouseEvent::new_for_platform_motion_event(
|
||||
&self.window,
|
||||
FireMouseEventType::Out,
|
||||
&hit_test_result,
|
||||
input_event,
|
||||
can_gc,
|
||||
)
|
||||
.upcast::<Event>()
|
||||
.fire(old_target.upcast(), can_gc);
|
||||
);
|
||||
|
||||
// Fire pointerout before mouseout
|
||||
mouse_out_event
|
||||
.to_pointer_hover_event("pointerout", can_gc)
|
||||
.upcast::<Event>()
|
||||
.fire(old_target.upcast(), can_gc);
|
||||
|
||||
mouse_out_event
|
||||
.upcast::<Event>()
|
||||
.fire(old_target.upcast(), can_gc);
|
||||
|
||||
if !old_target_is_ancestor_of_new_target {
|
||||
let event_target = DomRoot::from_ref(old_target.upcast::<Node>());
|
||||
@@ -508,7 +538,7 @@ impl DocumentEventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch mouseover and mouseenter to new target.
|
||||
// Dispatch pointerover/mouseover and pointerenter/mouseenter to new target.
|
||||
for element in new_target
|
||||
.upcast::<Node>()
|
||||
.inclusive_ancestors(ShadowIncluding::Yes)
|
||||
@@ -517,15 +547,23 @@ impl DocumentEventHandler {
|
||||
element.set_hover_state(true);
|
||||
}
|
||||
|
||||
MouseEvent::new_for_platform_motion_event(
|
||||
let mouse_over_event = MouseEvent::new_for_platform_motion_event(
|
||||
&self.window,
|
||||
FireMouseEventType::Over,
|
||||
&hit_test_result,
|
||||
input_event,
|
||||
can_gc,
|
||||
)
|
||||
.upcast::<Event>()
|
||||
.dispatch(new_target.upcast(), false, can_gc);
|
||||
);
|
||||
|
||||
// Fire pointerover before mouseover
|
||||
mouse_over_event
|
||||
.to_pointer_hover_event("pointerover", can_gc)
|
||||
.upcast::<Event>()
|
||||
.dispatch(new_target.upcast(), false, can_gc);
|
||||
|
||||
mouse_over_event
|
||||
.upcast::<Event>()
|
||||
.dispatch(new_target.upcast(), false, can_gc);
|
||||
|
||||
let moving_from = self
|
||||
.current_hover_target
|
||||
@@ -986,7 +1024,7 @@ impl DocumentEventHandler {
|
||||
);
|
||||
|
||||
// Dispatch pointer event before updating active touch points and before touch event.
|
||||
let pointer_event_type = match event.event_type {
|
||||
let pointer_event_name = match event.event_type {
|
||||
TouchEventType::Down => "pointerdown",
|
||||
TouchEventType::Move => "pointermove",
|
||||
TouchEventType::Up => "pointerup",
|
||||
@@ -997,9 +1035,38 @@ impl DocumentEventHandler {
|
||||
let pointer_id = self.get_or_create_pointer_id_for_touch(identifier);
|
||||
let is_primary = self.is_primary_pointer(pointer_id);
|
||||
|
||||
// For touch devices (which don't support hover), fire pointerover/pointerenter
|
||||
// <https://w3c.github.io/pointerevents/#mapping-for-devices-that-do-not-support-hover>
|
||||
if matches!(event.event_type, TouchEventType::Down) {
|
||||
// Fire pointerover
|
||||
let pointer_over = pointer_touch.to_pointer_event(
|
||||
window,
|
||||
"pointerover",
|
||||
pointer_id,
|
||||
is_primary,
|
||||
input_event.active_keyboard_modifiers,
|
||||
true, // cancelable
|
||||
Some(hit_test_result.point_in_node),
|
||||
can_gc,
|
||||
);
|
||||
pointer_over.upcast::<Event>().fire(¤t_target, can_gc);
|
||||
|
||||
// Fire pointerenter hierarchically (from topmost ancestor to target)
|
||||
self.fire_pointer_event_for_touch(
|
||||
&element,
|
||||
&pointer_touch,
|
||||
pointer_id,
|
||||
"pointerenter",
|
||||
is_primary,
|
||||
input_event,
|
||||
&hit_test_result,
|
||||
can_gc,
|
||||
);
|
||||
}
|
||||
|
||||
let pointer_event = pointer_touch.to_pointer_event(
|
||||
window,
|
||||
pointer_event_type,
|
||||
pointer_event_name,
|
||||
pointer_id,
|
||||
is_primary,
|
||||
input_event.active_keyboard_modifiers,
|
||||
@@ -1011,6 +1078,38 @@ impl DocumentEventHandler {
|
||||
.upcast::<Event>()
|
||||
.fire(¤t_target, can_gc);
|
||||
|
||||
// For touch devices, fire pointerout/pointerleave after pointerup/pointercancel
|
||||
// <https://w3c.github.io/pointerevents/#mapping-for-devices-that-do-not-support-hover>
|
||||
if matches!(
|
||||
event.event_type,
|
||||
TouchEventType::Up | TouchEventType::Cancel
|
||||
) {
|
||||
// Fire pointerout
|
||||
let pointer_out = pointer_touch.to_pointer_event(
|
||||
window,
|
||||
"pointerout",
|
||||
pointer_id,
|
||||
is_primary,
|
||||
input_event.active_keyboard_modifiers,
|
||||
true, // cancelable
|
||||
Some(hit_test_result.point_in_node),
|
||||
can_gc,
|
||||
);
|
||||
pointer_out.upcast::<Event>().fire(¤t_target, can_gc);
|
||||
|
||||
// Fire pointerleave hierarchically (from target to topmost ancestor)
|
||||
self.fire_pointer_event_for_touch(
|
||||
&element,
|
||||
&pointer_touch,
|
||||
pointer_id,
|
||||
"pointerleave",
|
||||
is_primary,
|
||||
input_event,
|
||||
&hit_test_result,
|
||||
can_gc,
|
||||
);
|
||||
}
|
||||
|
||||
let (touch_dispatch_target, changed_touch) = match event.event_type {
|
||||
TouchEventType::Down => {
|
||||
// Add a new touch point
|
||||
@@ -2045,6 +2144,51 @@ impl DocumentEventHandler {
|
||||
.min()
|
||||
.is_some_and(|primary_pointer| *primary_pointer == pointer_id)
|
||||
}
|
||||
|
||||
/// Fire pointerenter events hierarchically from topmost ancestor to target element.
|
||||
/// Fire pointerleave events hierarchically from target element to topmost ancestor.
|
||||
/// Used for touch devices that don't support hover.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn fire_pointer_event_for_touch(
|
||||
&self,
|
||||
target_element: &Element,
|
||||
touch: &Touch,
|
||||
pointer_id: i32,
|
||||
event_name: &str,
|
||||
is_primary: bool,
|
||||
input_event: &ConstellationInputEvent,
|
||||
hit_test_result: &HitTestResult,
|
||||
can_gc: CanGc,
|
||||
) {
|
||||
// Collect ancestors from target to root
|
||||
let mut targets: Vec<DomRoot<Node>> = vec![];
|
||||
let mut current: Option<DomRoot<Node>> = Some(DomRoot::from_ref(target_element.upcast()));
|
||||
while let Some(node) = current {
|
||||
targets.push(DomRoot::from_ref(&*node));
|
||||
current = node.GetParentNode();
|
||||
}
|
||||
|
||||
// Reverse to dispatch from topmost ancestor to target
|
||||
if event_name == "pointerenter" {
|
||||
targets.reverse();
|
||||
}
|
||||
|
||||
for target in targets {
|
||||
let pointer_event = touch.to_pointer_event(
|
||||
&self.window,
|
||||
event_name,
|
||||
pointer_id,
|
||||
is_primary,
|
||||
input_event.active_keyboard_modifiers,
|
||||
false,
|
||||
Some(hit_test_result.point_in_node),
|
||||
can_gc,
|
||||
);
|
||||
pointer_event
|
||||
.upcast::<Event>()
|
||||
.fire(target.upcast(), can_gc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
||||
@@ -75,7 +75,7 @@ use crate::script_runtime::CanGc;
|
||||
/// <https://html.spec.whatwg.org/multipage/#globaleventhandlers> and
|
||||
/// <https://html.spec.whatwg.org/multipage/#windoweventhandlers> as well as
|
||||
/// specific attributes for elements
|
||||
static CONTENT_EVENT_HANDLER_NAMES: [&str; 108] = [
|
||||
static CONTENT_EVENT_HANDLER_NAMES: [&str; 118] = [
|
||||
"onabort",
|
||||
"onauxclick",
|
||||
"onbeforeinput",
|
||||
@@ -164,6 +164,17 @@ static CONTENT_EVENT_HANDLER_NAMES: [&str; 108] = [
|
||||
// https://w3c.github.io/selection-api/#extensions-to-globaleventhandlers-interface
|
||||
"onselectstart",
|
||||
"onselectionchange",
|
||||
// https://w3c.github.io/pointerevents/#extensions-to-the-globaleventhandlers-interface
|
||||
"onpointercancel",
|
||||
"onpointerdown",
|
||||
"onpointerup",
|
||||
"onpointermove",
|
||||
"onpointerout",
|
||||
"onpointerover",
|
||||
"onpointerenter",
|
||||
"onpointerleave",
|
||||
"ongotpointercapture",
|
||||
"onlostpointercapture",
|
||||
// https://html.spec.whatwg.org/multipage/#windoweventhandlers
|
||||
"onafterprint",
|
||||
"onbeforeprint",
|
||||
|
||||
@@ -650,6 +650,14 @@ macro_rules! global_event_handlers(
|
||||
event_handler!(pause, GetOnpause, SetOnpause);
|
||||
event_handler!(play, GetOnplay, SetOnplay);
|
||||
event_handler!(playing, GetOnplaying, SetOnplaying);
|
||||
event_handler!(pointercancel, GetOnpointercancel, SetOnpointercancel);
|
||||
event_handler!(pointerdown, GetOnpointerdown, SetOnpointerdown);
|
||||
event_handler!(pointerenter, GetOnpointerenter, SetOnpointerenter);
|
||||
event_handler!(pointerleave, GetOnpointerleave, SetOnpointerleave);
|
||||
event_handler!(pointermove, GetOnpointermove, SetOnpointermove);
|
||||
event_handler!(pointerout, GetOnpointerout, SetOnpointerout);
|
||||
event_handler!(pointerover, GetOnpointerover, SetOnpointerover);
|
||||
event_handler!(pointerup, GetOnpointerup, SetOnpointerup);
|
||||
event_handler!(progress, GetOnprogress, SetOnprogress);
|
||||
event_handler!(ratechange, GetOnratechange, SetOnratechange);
|
||||
event_handler!(reset, GetOnreset, SetOnreset);
|
||||
|
||||
@@ -372,6 +372,68 @@ impl MouseEvent {
|
||||
can_gc,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
event_type: &str,
|
||||
can_gc: CanGc,
|
||||
) -> 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(
|
||||
window,
|
||||
DOMString::from(event_type),
|
||||
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
|
||||
can_gc,
|
||||
);
|
||||
|
||||
// Set trusted to match the source mouse event
|
||||
pointer_event
|
||||
.upcast::<Event>()
|
||||
.set_trusted(self.IsTrusted());
|
||||
|
||||
pointer_event
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseEventMethods<crate::DomTypeHolder> for MouseEvent {
|
||||
|
||||
@@ -92,15 +92,35 @@ impl Touch {
|
||||
point_in_node: Option<euclid::Point2D<f32, style_traits::CSSPixel>>,
|
||||
can_gc: CanGc,
|
||||
) -> DomRoot<PointerEvent> {
|
||||
// Pressure is 0.5 for active touches, 0.0 for up/cancel
|
||||
let pressure = if event_type == "pointerup" || event_type == "pointercancel" {
|
||||
// Pressure is 0.5 for active touches, 0.0 for up/cancel/out/leave
|
||||
// <https://w3c.github.io/pointerevents/#dom-pointerevent-pressure>
|
||||
// TODO: add proper force support.
|
||||
let pressure = if event_type == "pointerup" ||
|
||||
event_type == "pointercancel" ||
|
||||
event_type == "pointerout" ||
|
||||
event_type == "pointerleave"
|
||||
{
|
||||
0.0
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
|
||||
let button = if event_type == "pointermove" { -1 } else { 0 };
|
||||
// <https://w3c.github.io/pointerevents/#the-button-property>
|
||||
// For pointermove, pointerover, pointerenter, pointerout, pointerleave: button is -1
|
||||
// For pointerdown, pointerup, pointercancel: button is 0 (primary button)
|
||||
let button = if event_type == "pointermove" ||
|
||||
event_type == "pointerover" ||
|
||||
event_type == "pointerenter" ||
|
||||
event_type == "pointerout" ||
|
||||
event_type == "pointerleave"
|
||||
{
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Buttons: 1 if a button is pressed during the event, 0 otherwise
|
||||
// For touch: button is pressed during over/enter/down/move, not during up/cancel/out/leave
|
||||
let buttons = if event_type == "pointermove" ||
|
||||
event_type == "pointerover" ||
|
||||
event_type == "pointerenter" ||
|
||||
@@ -111,11 +131,19 @@ impl Touch {
|
||||
0
|
||||
};
|
||||
|
||||
// For enter/leave events, they don't bubble and are not cancelable
|
||||
let (bubbles, cancelable) = match event_type {
|
||||
"pointerenter" | "pointerleave" => {
|
||||
(EventBubbles::DoesNotBubble, EventCancelable::NotCancelable)
|
||||
},
|
||||
_ => (EventBubbles::Bubbles, EventCancelable::from(is_cancelable)),
|
||||
};
|
||||
|
||||
PointerEvent::new(
|
||||
window,
|
||||
DOMString::from(event_type),
|
||||
EventBubbles::Bubbles,
|
||||
EventCancelable::from(is_cancelable),
|
||||
bubbles,
|
||||
cancelable,
|
||||
Some(window),
|
||||
0, // detail
|
||||
Point2D::new(*self.ScreenX() as i32, *self.ScreenY() as i32),
|
||||
|
||||
Reference in New Issue
Block a user