From 3883ba2a34f03eef28f4c290520da7eb8acaf45d Mon Sep 17 00:00:00 2001 From: Dery Almas Date: Tue, 14 Apr 2026 00:18:06 +0200 Subject: [PATCH] Wayland: Handle complex pointer event frames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- platform/linuxbsd/wayland/wayland_thread.cpp | 50 ++++++++++++++------ platform/linuxbsd/wayland/wayland_thread.h | 4 ++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index dc27d4becb..32af8d8ef5 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -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 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) { diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h index a22ece5e29..3bc33f341e 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -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;