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:
webbeef
2026-03-03 00:27:53 -08:00
committed by GitHub
parent b68628ad82
commit 295e019d00
21 changed files with 425 additions and 282 deletions

View File

@@ -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(&current_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(&current_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(&current_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)]

View File

@@ -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",

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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),