mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-06-28 03:41:41 +00:00
fix(tabs): mouse handling stuck after aborted tab drag on Wayland
A quick click-and-flick on a tab could leave all of kitty with mouse input permanently redirected to the tab bar, making every window unclickable and text selection impossible. Starting a tab drag is asynchronous: the drag thumbnail is rendered on the next frame before glfwStartDrag is called. If the button is released in that window, wl_data_device_start_drag is sent with a stale serial that no longer matches an active pointer implicit grab, so the compositor silently ignores it. The wl_data_source then never receives any event, on_drag_source_finished never runs, and the tab_being_dragged state is stuck forever, hijacking all mouse events. Fix in layers: - glfw/Wayland: track the implicit grab (serial of the first button press and pressed-button count), use that serial for start_drag and refuse with EAGAIN when there is no active implicit grab instead of letting the compositor silently drop the request - mouse.c: a left button release arriving while a tab drag is marked started but no system DND is active means the drag never launched (an active DND consumes the release on all platforms), so clear the drag state instead of waiting for DND events that will never come - tabs.py: handle OSError from start_drag_with_data for tab drags the same way window drags already do; clear the potential-drag state when the release lands on the new-tab button or empty tab bar area - tabs.py/boss.py: clear drag state on drag finish/drop even when the dragged tab has already been closed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
11c2ccf00f
commit
dc36e21654
6 changed files with 58 additions and 6 deletions
8
glfw/wl_init.c
vendored
8
glfw/wl_init.c
vendored
|
|
@ -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
6
glfw/wl_platform.h
vendored
|
|
@ -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
13
glfw/wl_window.c
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', '''
|
||||
|
|
|
|||
|
|
@ -957,6 +957,19 @@ static void
|
|||
handle_tab_bar_mouse(int button, int modifiers, int action) {
|
||||
set_currently_hovered_window(0, modifiers);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue