LibWeb: Dispatch pointer boundary events when hovered node changes
This commit is contained in:
parent
e4f37293fc
commit
8d0afda9f7
6 changed files with 166 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
16
Libraries/LibWeb/UIEvents/PointerTypes.cpp
Normal file
16
Libraries/LibWeb/UIEvents/PointerTypes.cpp
Normal 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
|
||||
|
||||
}
|
||||
24
Libraries/LibWeb/UIEvents/PointerTypes.h
Normal file
24
Libraries/LibWeb/UIEvents/PointerTypes.h
Normal 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
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue