LibWeb: Dispatch pointer boundary events when hovered node changes

This commit is contained in:
Tim Ledbetter 2026-01-23 23:46:00 +00:00 committed by Tim Ledbetter
parent e4f37293fc
commit 8d0afda9f7
6 changed files with 166 additions and 1 deletions

View file

@ -1023,6 +1023,7 @@ set(SOURCES
UIEvents/KeyboardEvent.cpp
UIEvents/MouseEvent.cpp
UIEvents/PointerEvent.cpp
UIEvents/PointerTypes.cpp
UIEvents/TextEvent.cpp
UIEvents/UIEvent.cpp
UIEvents/WheelEvent.cpp

View file

@ -197,6 +197,8 @@
#include <LibWeb/UIEvents/FocusEvent.h>
#include <LibWeb/UIEvents/KeyboardEvent.h>
#include <LibWeb/UIEvents/MouseEvent.h>
#include <LibWeb/UIEvents/PointerEvent.h>
#include <LibWeb/UIEvents/PointerTypes.h>
#include <LibWeb/UIEvents/TextEvent.h>
#include <LibWeb/ViewTransition/ViewTransition.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
@ -2104,6 +2106,21 @@ void Document::set_hovered_node(GC::Ptr<Node> node)
m_hovered_node = node;
// https://w3c.github.io/pointerevents/#the-pointerout-event
if (old_hovered_node && old_hovered_node != m_hovered_node) {
UIEvents::PointerEventInit pointer_event_init {};
pointer_event_init.bubbles = true;
pointer_event_init.cancelable = true;
pointer_event_init.composed = true;
pointer_event_init.related_target = m_hovered_node;
pointer_event_init.is_primary = true;
pointer_event_init.pointer_type = UIEvents::PointerTypes::Mouse;
if (auto navigable = this->navigable())
pointer_event_init.view = navigable->active_window_proxy();
auto event = UIEvents::PointerEvent::create(realm(), UIEvents::EventNames::pointerout, pointer_event_init);
old_hovered_node->dispatch_event(event);
}
// https://w3c.github.io/uievents/#mouseout
if (old_hovered_node && old_hovered_node != m_hovered_node) {
UIEvents::MouseEventInit mouse_event_init {};
@ -2117,6 +2134,20 @@ void Document::set_hovered_node(GC::Ptr<Node> node)
old_hovered_node->dispatch_event(event);
}
// https://w3c.github.io/pointerevents/#the-pointerleave-event
if (old_hovered_node && (!m_hovered_node || !m_hovered_node->is_descendant_of(*old_hovered_node))) {
for (auto target = old_hovered_node; target && target.ptr() != common_ancestor; target = target->parent()) {
// FIXME: Populate the event with mouse coordinates, etc.
UIEvents::PointerEventInit pointer_event_init {};
pointer_event_init.related_target = m_hovered_node;
pointer_event_init.is_primary = true;
pointer_event_init.pointer_type = UIEvents::PointerTypes::Mouse;
if (auto navigable = this->navigable())
pointer_event_init.view = navigable->active_window_proxy();
target->dispatch_event(UIEvents::PointerEvent::create(realm(), UIEvents::EventNames::pointerleave, pointer_event_init));
}
}
// https://w3c.github.io/uievents/#mouseleave
if (old_hovered_node && (!m_hovered_node || !m_hovered_node->is_descendant_of(*old_hovered_node))) {
for (auto target = old_hovered_node; target && target.ptr() != common_ancestor; target = target->parent()) {
@ -2127,6 +2158,21 @@ void Document::set_hovered_node(GC::Ptr<Node> node)
}
}
// https://w3c.github.io/pointerevents/#the-pointerover-event
if (m_hovered_node && m_hovered_node != old_hovered_node) {
UIEvents::PointerEventInit pointer_event_init {};
pointer_event_init.bubbles = true;
pointer_event_init.cancelable = true;
pointer_event_init.composed = true;
pointer_event_init.related_target = old_hovered_node;
pointer_event_init.is_primary = true;
pointer_event_init.pointer_type = UIEvents::PointerTypes::Mouse;
if (auto navigable = this->navigable())
pointer_event_init.view = navigable->active_window_proxy();
auto event = UIEvents::PointerEvent::create(realm(), UIEvents::EventNames::pointerover, pointer_event_init);
m_hovered_node->dispatch_event(event);
}
// https://w3c.github.io/uievents/#mouseover
if (m_hovered_node && m_hovered_node != old_hovered_node) {
UIEvents::MouseEventInit mouse_event_init {};
@ -2140,6 +2186,7 @@ void Document::set_hovered_node(GC::Ptr<Node> node)
m_hovered_node->dispatch_event(event);
}
// https://w3c.github.io/pointerevents/#the-pointerenter-event
// https://w3c.github.io/uievents/#mouseenter
// Enter events are dispatched from ancestor to descendant.
// Leave events are dispatched in the opposite order.
@ -2149,7 +2196,14 @@ void Document::set_hovered_node(GC::Ptr<Node> node)
entered_ancestors.append(*target);
for (auto target : entered_ancestors.in_reverse()) {
// FIXME: Populate the event with mouse coordinates, etc.
// FIXME: Populate the events with mouse coordinates, etc.
UIEvents::PointerEventInit pointer_event_init {};
pointer_event_init.related_target = old_hovered_node;
pointer_event_init.is_primary = true;
pointer_event_init.pointer_type = UIEvents::PointerTypes::Mouse;
if (auto navigable = this->navigable())
pointer_event_init.view = navigable->active_window_proxy();
target->dispatch_event(UIEvents::PointerEvent::create(realm(), UIEvents::EventNames::pointerenter, pointer_event_init));
UIEvents::MouseEventInit mouse_event_init {};
mouse_event_init.related_target = old_hovered_node;
target->dispatch_event(UIEvents::MouseEvent::create(realm(), UIEvents::EventNames::mouseenter, mouse_event_init));

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2026, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/UIEvents/PointerTypes.h>
namespace Web::UIEvents::PointerTypes {
#define __ENUMERATE_POINTER_TYPE(name, value) \
String name = value##_string;
ENUMERATE_POINTER_TYPES
#undef __ENUMERATE_POINTER_TYPE
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2026, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <LibWeb/Export.h>
namespace Web::UIEvents::PointerTypes {
// https://w3c.github.io/pointerevents/#dom-pointerevent-pointertype
#define ENUMERATE_POINTER_TYPES \
__ENUMERATE_POINTER_TYPE(Mouse, "mouse") \
__ENUMERATE_POINTER_TYPE(Pen, "pen") \
__ENUMERATE_POINTER_TYPE(Touch, "touch")
#define __ENUMERATE_POINTER_TYPE(name, value) extern WEB_API String name;
ENUMERATE_POINTER_TYPES
#undef __ENUMERATE_POINTER_TYPE
}

View file

@ -0,0 +1,16 @@
> move pointer over #inner
pointerover target.id=(inner) relatedTarget.id=(body)
pointerover target.id=(inner) relatedTarget.id=(body)
pointerenter target.id=(outer) relatedTarget.id=(body)
pointerenter target.id=(inner) relatedTarget.id=(body)
pointermove target.id=(inner) relatedTarget.id=(null)
pointermove target.id=(inner) relatedTarget.id=(null)
> move pointer over #outer (leaving #inner)
pointerout target.id=(inner) relatedTarget.id=(outer)
pointerout target.id=(inner) relatedTarget.id=(outer)
pointerleave target.id=(inner) relatedTarget.id=(outer)
pointerover target.id=(outer) relatedTarget.id=(inner)
pointermove target.id=(outer) relatedTarget.id=(null)
> move pointer outside (leaving #outer)
pointerout target.id=(outer) relatedTarget.id=(out)
pointerleave target.id=(outer) relatedTarget.id=(out)

View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<style>
body {
margin: 0;
padding: 5px;
width: 200px;
height: 200px;
}
#outer {
width: 100px;
height: 100px;
background-color: yellowgreen;
}
#inner {
width: 50px;
height: 50px;
background-color: magenta;
}
</style>
<body id="body"><div id="outer"><div id="inner"></div></div></body>
<script src="../include.js"></script>
<script>
function logEvent(e) {
println(`${e.type} target.id=(${e.target.id}) relatedTarget.id=(${e.relatedTarget?.id ?? 'null'})`);
}
const events = [
'pointerover',
'pointerout',
'pointerenter',
'pointerleave',
'pointermove',
];
events.forEach(eventType => {
outer.addEventListener(eventType, logEvent);
inner.addEventListener(eventType, logEvent);
});
test(() => {
internals.movePointerTo(150, 150);
println("> move pointer over #inner");
internals.movePointerTo(10, 10);
println("> move pointer over #outer (leaving #inner)");
internals.movePointerTo(60, 60);
println("> move pointer outside (leaving #outer)");
internals.movePointerTo(150, 150);
});
</script>