Merge branch 'fix-tabs-mouse-handling-stuck' of https://github.com/ttys3/kitty

This commit is contained in:
Kovid Goyal
2026-06-12 09:46:23 +05:30
6 changed files with 58 additions and 6 deletions

8
glfw/wl_init.c vendored
View File

@@ -102,6 +102,11 @@ pointerHandleEnter(
static void
pointerHandleLeave(void* data UNUSED, struct wl_pointer* pointer UNUSED, uint32_t serial, struct wl_surface* surface) {
// The pointer never leaves the surface during an implicit grab, so a
// leave event means any implicit grab is over (e.g. the compositor took
// over the pointer for drag-and-drop). The matching button releases will
// never be delivered to us.
_glfw.wl.pointer_button_count = 0;
_GLFWwindow* window = _glfw.wl.pointerFocus;
if (!window) return;
_glfw.wl.serial = serial;
@@ -138,6 +143,9 @@ static void pointerHandleButton(void* data UNUSED,
{
glfw_cancel_momentum_scroll();
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial; _glfw.wl.pointer_serial = serial;
if (state == WL_POINTER_BUTTON_STATE_PRESSED) {
if (_glfw.wl.pointer_button_count++ == 0) _glfw.wl.pointer_grab_serial = serial;
} else if (_glfw.wl.pointer_button_count > 0) _glfw.wl.pointer_button_count--;
_GLFWwindow* window = _glfw.wl.pointerFocus;
if (!window) return;

6
glfw/wl_platform.h vendored
View File

@@ -374,6 +374,12 @@ typedef struct _GLFWlibraryWayland
struct wl_surface* cursorSurface;
GLFWCursorShape cursorPreviousShape;
uint32_t serial, input_serial, pointer_serial, pointer_enter_serial, keyboard_enter_serial;
// serial of the button press that started the current pointer implicit
// grab, and the number of currently pressed pointer buttons. Requests
// such as wl_data_device.start_drag are silently ignored by compositors
// unless made with the serial of an active implicit grab.
uint32_t pointer_grab_serial;
unsigned pointer_button_count;
int32_t keyboardRepeatRate;
monotonic_t keyboardRepeatDelay;

13
glfw/wl_window.c vendored
View File

@@ -3496,6 +3496,17 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {
return ENOTSUP;
}
if (_glfw.wl.pointer_button_count == 0) {
// start_drag requires the serial of an active pointer implicit grab,
// without one the compositor silently ignores the request and the
// data source never receives any events, so fail early instead.
// This can happen as drags are started asynchronously and the button
// may have been released by the time we get here. EPERM matches what
// start_window_drag() in kitty/glfw.c reports for this situation.
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Refusing to start drag without an active pointer implicit grab");
return EPERM;
}
// Create the data source
_glfw.wl.drag.source = wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager);
if (!_glfw.wl.drag.source) {
@@ -3568,7 +3579,7 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {
wl_data_device_start_drag(
_glfw.wl.dataDevice, _glfw.wl.drag.source, window->wl.surface,
_glfw.wl.drag.toplevel_drag ? NULL : _glfw.wl.drag.drag_icon,
_glfw.wl.pointer_serial);
_glfw.wl.pointer_grab_serial);
if (_glfw.wl.drag.toplevel_drag) {
// Attach the toplevel AFTER start_drag, otherwise doesnt work on mutter

View File

@@ -2046,8 +2046,9 @@ class Boss:
self._move_window_to(window, target_os_window_id='new')
return
if (tab_id := int((data or {}).get(f'application/net.kovidgoyal.kitty-tab-{os.getpid()}', b'0').decode())
) and get_tab_being_dragged()[0] == tab_id and (tab := self.tab_for_id(tab_id)):
if needs_toplevel_on_wayland:
) and get_tab_being_dragged()[0] == tab_id:
tab = self.tab_for_id(tab_id)
if tab is not None and needs_toplevel_on_wayland:
for tm in self.all_tab_managers:
if tm.tab_being_dropped:
tm.on_tab_drop(0, 0, bypass_move=True)
@@ -2055,7 +2056,7 @@ class Boss:
set_tab_being_dragged()
for tm in self.all_tab_managers:
tm.on_tab_drop_move()
if was_dropped: # detach tab into new OS Window
if was_dropped and tab is not None: # detach tab into new OS Window
self._move_tab_to(tab)
@ac('win', '''

View File

@@ -957,6 +957,19 @@ static void
handle_tab_bar_mouse(int button, int modifiers, int action) {
set_currently_hovered_window(0, modifiers, false);
OSWindow *w = global_state.callback_os_window;
if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_RELEASE && global_state.tab_being_dragged.id
&& global_state.tab_being_dragged.drag_started && !global_state.drag_source.is_active) {
// Once a system drag and drop is active the release is consumed by it
// and never delivered to us, so getting one here means the drag never
// became a system DND: either glfwStartDrag failed/was not called yet
// or the compositor silently ignored it (Wayland with a stale serial).
// Clear the drag state so mouse handling is not redirected to the tab
// bar forever, and swallow the release as it ended an aborted drag.
zero_at_ptr(&global_state.tab_being_dragged);
// re-render the tab bar in case it was drawn without the dragged tab
if (w) w->tab_bar_data_updated = false;
return;
}
// dont report motion events, as they are expensive and useless
if (w && (button > -1 || global_state.tab_being_dragged.id)) {
call_boss(handle_tab_bar_mouse, "Kddiii", w->id, w->mouse_x, w->mouse_y, button, modifiers, action);

View File

@@ -1732,6 +1732,9 @@ class TabManager: # {{{
if (td := self.tab_being_dropped) is None:
return
if (tab := get_boss().tab_for_id(td.data.tab_id)) is None:
self.tab_being_dropped = None
set_tab_being_dragged()
self.layout_tab_bar()
return
if not bypass_move:
self.on_tab_drop_move(td.data.tab_id, True, x, y)
@@ -1779,7 +1782,12 @@ class TabManager: # {{{
drag_data = {
f'application/net.kovidgoyal.kitty-tab-{os.getpid()}': str(tab.id).encode(),
}
start_drag_with_data(self.os_window_id, drag_data, thumbnails)
try:
start_drag_with_data(self.os_window_id, drag_data, thumbnails)
except OSError as e:
log_error(f'Failed to start tab drag: {e}')
set_tab_being_dragged()
self.mark_tab_bar_dirty() # re-render the tab bar in case it was drawn without the dragged tab
break
else:
set_tab_being_dragged()
@@ -1797,16 +1805,21 @@ class TabManager: # {{{
tab_id_at_x = self.tab_bar.tab_id_at(int(x))
self.recent_tab_bar_mouse_events.add(button, modifiers, action, x, y, tab_id_at_x)
drag_started = get_tab_being_dragged()[1]
is_left_release = button == GLFW_MOUSE_BUTTON_LEFT and action == GLFW_RELEASE
if tab_id_at_x < 0: # synthetic tab (e.g. "+" new-tab button)
if is_left_release and not drag_started:
set_tab_being_dragged() # clear potential drag from a press on a tab
if self.recent_tab_bar_mouse_events.click_count(GLFW_MOUSE_BUTTON_LEFT) == 1:
self.new_tab()
self.recent_tab_bar_mouse_events.clear()
return
drag_started = get_tab_being_dragged()[1]
if drag_started:
return
tab = self.tab_for_id(tab_id_at_x)
if tab is None:
if is_left_release:
set_tab_being_dragged() # clear potential drag from a press on a tab
if self.recent_tab_bar_mouse_events.click_count(GLFW_MOUSE_BUTTON_LEFT) == 2:
self.new_tab()
self.recent_tab_bar_mouse_events.clear()