From fe9828a0e4558c10c469639389a377fa8c2ae5dd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Jul 2026 09:04:45 +0530 Subject: [PATCH] Workaround for dragging not starting on niri niri for some reason known only to itself does not send any pointer leave/data device enter/data source events when it accepts a grab. The only event it seems to send is surface enter on the drag icon surface, and that too it sends that event much later. Handle this bonkers behavior by retrying the sync multiple times to detect if niri has accepted the drag or not. Fixes #10271 --- docs/changelog.rst | 2 ++ glfw/wl_platform.h | 5 +++++ glfw/wl_window.c | 55 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fa34d0b3b..33cca1d7b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -184,6 +184,8 @@ Detailed list of changes - Wayland: fix a regression in the previous release that broke window sizing on Hyprland with fractional scales and an initial specified size (:iss:`10268`) +- Wayland: Fix a regression in 0.47.3 that broke dragging on the niri compositor (:iss:`10271`) + 0.48.0 [2026-07-18] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index 163ec103f..a1627f052 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -448,6 +448,11 @@ typedef struct _GLFWlibraryWayland // mapping it was deferred so it cannot end up as a stray regular // window if start_drag was silently ignored. bool toplevel_map_deferred; + // Number of extra sync roundtrips issued waiting for confirmation; + // some compositors (e.g. niri) send the confirmation event (drag icon + // wl_surface.enter) after the first sync roundtrip, so we retry once + // before concluding that start_drag was silently ignored. + uint8_t sync_retries; struct { const char *mime_type; int fd; diff --git a/glfw/wl_window.c b/glfw/wl_window.c index dc351eba9..69c169960 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -3276,6 +3276,8 @@ _glfwWaylandConfirmDragSession(void) { } } +static const struct wl_callback_listener drag_start_confirmation_listener; + static void drag_start_confirmation_handle_done(void *data UNUSED, struct wl_callback *callback, uint32_t cb_data UNUSED) { if (callback != _glfw.wl.drag.start_confirmation) { @@ -3285,24 +3287,53 @@ drag_start_confirmation_handle_done(void *data UNUSED, struct wl_callback *callb } _glfw.wl.drag.start_confirmation = NULL; wl_callback_destroy(callback); - if (!_glfw.wl.drag.session_confirmed) { - // The compositor processed start_drag before this sync callback, and - // an accepted start_drag synchronously produces events (pointer - // leave, data device enter, data source events) that are ordered - // before it. None arrived, so the compositor silently ignored - // start_drag (no active implicit grab matching the serial). Without - // this, the data source would never receive any event, leaking the - // drag state forever and orphaning the drag toplevel as a stray - // window. - _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: start_drag was silently ignored by the compositor, cancelling drag"); - cancel_drag(GLFW_DRAG_CANCELLED); + if (_glfw.wl.drag.session_confirmed) return; + // The compositor processed start_drag before this sync callback. An + // accepted start_drag should produce events that are ordered before this + // sync (pointer leave, data device enter, data source events, or drag icon + // wl_surface.enter). Some compositors (e.g. niri) send the confirmation + // event one roundtrip later than the sync. Use pointer_button_count to + // distinguish: if the button is still held the DND implicit grab (which + // sends pointer leave, resetting the count to 0) hasn't fired yet, so + // retry a few times. If the button has already been released (count == 0) + // there is no active grab and the compositor definitely ignored start_drag. + if (_glfw.wl.pointer_button_count > 0 && _glfw.wl.drag.sync_retries < 3) { + _glfw.wl.drag.sync_retries++; + debug_input("Drag session not yet confirmed, button still held; retrying sync (%u/3)\n", + _glfw.wl.drag.sync_retries); + _glfw.wl.drag.start_confirmation = wl_display_sync(_glfw.wl.display); + if (_glfw.wl.drag.start_confirmation) + wl_callback_add_listener(_glfw.wl.drag.start_confirmation, &drag_start_confirmation_listener, NULL); + return; } + // Without this detection the data source would never receive any event, + // leaking the drag state forever and orphaning the drag toplevel as a + // stray window. + _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: start_drag was silently ignored by the compositor, cancelling drag"); + cancel_drag(GLFW_DRAG_CANCELLED); } static const struct wl_callback_listener drag_start_confirmation_listener = { .done = drag_start_confirmation_handle_done, }; +static void +drag_icon_surface_handle_enter(void *data UNUSED, struct wl_surface *surface UNUSED, struct wl_output *output UNUSED) { + // The compositor maps the drag icon surface to an output only when the + // DND session is live. Some compositors (e.g. niri) don't send a pointer + // leave or data device enter when accepting start_drag; this enter event + // on the drag icon is the only signal they provide. + _glfwWaylandConfirmDragSession(); +} + +static void +drag_icon_surface_handle_leave(void *data UNUSED, struct wl_surface *surface UNUSED, struct wl_output *output UNUSED) {} + +static const struct wl_surface_listener drag_icon_surface_listener = { + .enter = drag_icon_surface_handle_enter, + .leave = drag_icon_surface_handle_leave, +}; + #define dr _glfw.wl.drag.data_requests[i] static void @@ -3627,6 +3658,7 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) { if (thumbnail && thumbnail->pixels) { _glfw.wl.drag.drag_icon = wl_compositor_create_surface(_glfw.wl.compositor); if (!_glfw.wl.drag.drag_icon) return ENOMEM; + wl_surface_add_listener(_glfw.wl.drag.drag_icon, &drag_icon_surface_listener, NULL); icon_buffer = createShmBuffer(thumbnail, false, true); if (!icon_buffer) return ENOMEM; if (_glfw.wl.wp_viewporter) { @@ -3695,6 +3727,7 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) { // event. Detect that with a sync: an accepted start_drag synchronously // produces events ordered before the sync callback. _glfw.wl.drag.session_confirmed = false; + _glfw.wl.drag.sync_retries = 0; _glfw.wl.drag.start_confirmation = wl_display_sync(_glfw.wl.display); if (_glfw.wl.drag.start_confirmation) wl_callback_add_listener(_glfw.wl.drag.start_confirmation, &drag_start_confirmation_listener, NULL);