Wayland: Handle complex pointer event frames

From the spec:

> The wl_pointer.enter and wl_pointer.leave events are logical events
> generated by the compositor and not the hardware. These events are
> also grouped by a wl_pointer.frame. When a pointer moves from one
> surface to another, a compositor should group the wl_pointer.leave
> event within the same wl_pointer.frame. However, a client must not
> rely on wl_pointer.leave and wl_pointer.enter being in the same
> wl_pointer.frame. Compositor-specific policies may require the
> wl_pointer.leave and wl_pointer.enter event being split across
> multiple wl_pointer.frame groups.

From my understanding™ this means that a compositor SHOULD group
leave/enter events together. Is this common? From my testing... Not
really. Notably, (only?) KDE does this.

Our pointer frame event assumed that we would be working with the
currently pointed window but since all events must be logically grouped
together I think it can really only group "normal" events related to the
*leave* event.

Now, whenever there's a pointer focus change, we send everything to the
old window, if it exists, otherwise the currently pointed one.

This approach seems to handle complex event frames with both leave and
enter events properly now, with good results on all compositors.

This patch also and makes it harder to get to a null check when the
window simply does not exists (the error was meant only for existing but
invalid windows), along with an helper method to aid in this.
This commit is contained in:
Dery Almas
2026-04-14 00:18:06 +02:00
parent ee713ccb7c
commit 3883ba2a34
2 changed files with 39 additions and 15 deletions
+35 -15
View File
@@ -1916,7 +1916,6 @@ void WaylandThread::_wl_pointer_on_leave(void *data, struct wl_pointer *wl_point
DisplayServerEnums::WindowID id = pd.pointed_id;
pd.pointed_id = DisplayServerEnums::INVALID_WINDOW_ID;
pd.pressed_button_mask.clear();
DEBUG_LOG_WAYLAND_THREAD(vformat("Pointer left window %d.", id));
@@ -2025,14 +2024,19 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point
PointerData &old_pd = ss->pointer_data;
PointerData &pd = ss->pointer_data_buffer;
WindowState *ws = nullptr;
if (pd.pointed_id != old_pd.pointed_id) {
if (old_pd.pointed_id != DisplayServerEnums::INVALID_WINDOW_ID) {
pd.pressed_button_mask.clear();
Ref<WindowEventMessage> msg;
msg.instantiate();
msg->id = old_pd.pointed_id;
msg->event = DisplayServerEnums::WINDOW_EVENT_MOUSE_EXIT;
wayland_thread->push_message(msg);
DEBUG_LOG_WAYLAND_THREAD(vformat("Notifying mouse exit to window %d", msg->id));
}
if (pd.pointed_id != DisplayServerEnums::INVALID_WINDOW_ID) {
@@ -2040,24 +2044,32 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point
msg.instantiate();
msg->id = pd.pointed_id;
msg->event = DisplayServerEnums::WINDOW_EVENT_MOUSE_ENTER;
DEBUG_LOG_WAYLAND_THREAD(vformat("Notifying mouse enter to window %d", msg->id));
wayland_thread->push_message(msg);
}
// According to the spec, compositors SHOULD emit both leave and enter events
// in one frame. Given that the frame event groups logically related events,
// it makes sense that all other events outside `enter` are related to the OLD
// surface. Additionally, some compositors (e.g. sway) emit other events
// alongside a leave event in one single frame, further confirming this
// behavior.
if (wayland_thread->window_exists(old_pd.pointed_id)) {
ws = wayland_thread->window_get_state(old_pd.pointed_id);
if (ws == nullptr) {
// Not ERR_FAIL_* as we still want to fall through.
ERR_PRINT("Invalid window userdata.");
}
}
}
WindowState *ws = nullptr;
// NOTE: At least on sway, with wl_pointer version 5 or greater,
// wl_pointer::leave might be emitted with other events (like
// wl_pointer::button) within the same wl_pointer::frame. Because of this, we
// need to account for when the currently pointed window might be invalid
// (third-party or even none) and fall back to the old one.
if (pd.pointed_id != DisplayServerEnums::INVALID_WINDOW_ID) {
ws = ss->wayland_thread->window_get_state(pd.pointed_id);
ERR_FAIL_NULL(ws);
} else if (old_pd.pointed_id != DisplayServerEnums::INVALID_WINDOW_ID) {
ws = ss->wayland_thread->window_get_state(old_pd.pointed_id);
ERR_FAIL_NULL(ws);
if (ws == nullptr && wayland_thread->window_exists(pd.pointed_id)) {
ws = wayland_thread->window_get_state(pd.pointed_id);
if (ws == nullptr) {
// Not ERR_FAIL_* as we still want to fall through.
ERR_PRINT("Invalid window userdata.");
}
}
if (ws == nullptr) {
@@ -4313,6 +4325,14 @@ void WaylandThread::window_destroy(DisplayServerEnums::WindowID p_window_id) {
windows.erase(p_window_id);
}
bool WaylandThread::window_exists(DisplayServerEnums::WindowID p_window_id) {
if (p_window_id == DisplayServerEnums::INVALID_WINDOW_ID) {
return false;
}
return windows.has(p_window_id);
}
struct wl_surface *WaylandThread::window_get_wl_surface(DisplayServerEnums::WindowID p_window_id) const {
const WindowState *ws = windows.getptr(p_window_id);
if (ws) {
@@ -1231,6 +1231,10 @@ public:
void window_create_popup(DisplayServerEnums::WindowID p_window_id, DisplayServerEnums::WindowID p_parent_id, Rect2i p_rect);
void window_destroy(DisplayServerEnums::WindowID p_window_Id);
// Checks if a window exists for this ID (NOT if its data is valid). Useful to
// detect deleted windows.
bool window_exists(DisplayServerEnums::WindowID p_window_id);
void window_set_parent(DisplayServerEnums::WindowID p_window_id, DisplayServerEnums::WindowID p_parent_id);
struct wl_surface *window_get_wl_surface(DisplayServerEnums::WindowID p_window_id) const;