From 0584cbfadc2972aa2ad45a77753fbab2a1f72ba3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 3 Apr 2026 09:42:57 +0530 Subject: [PATCH] Implement sending drag start message to client --- docs/dnd-protocol.rst | 2 +- kitty/dnd.c | 5 +++++ kitty/dnd.h | 3 +++ kitty/glfw.c | 8 ++++++++ kitty/mouse.c | 25 ++++++++++++++++++++----- kitty/options/definition.py | 2 +- kitty/screen.c | 23 +++++++++++++++-------- kitty/state.h | 7 +++++++ 8 files changed, 60 insertions(+), 15 deletions(-) diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 4397000ce..7fed4dda2 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -257,7 +257,7 @@ image by sending:: Where ``idx`` is now a zero based index with zero being the first image and so on. -Once the terminal program has present all data and images for the drag +Once the terminal program has sent all data and images for the drag operation, it indicates the drag should be started by sending ``t=P:x=-1``. At this time if the user has already cancelled the drag or the terminal determines the drag operation is not allowed, it must respond with ``t=R ; EPERM``. If any diff --git a/kitty/dnd.c b/kitty/dnd.c index ea3e438ca..7168de829 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -854,3 +854,8 @@ drop_left_child(Window *w) { queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, NULL, 0, false); } } + +void +drag_free_offer(Window *w) { + (void)w; +} diff --git a/kitty/dnd.h b/kitty/dnd.h index 156c348bf..66ab4284b 100644 --- a/kitty/dnd.h +++ b/kitty/dnd.h @@ -21,3 +21,6 @@ size_t drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_m void drop_dispatch_data(Window *w, const char *mime_type, const char *data, ssize_t sz); void drop_finish(Window *w); void dnd_set_test_write_func(PyObject *func); + + +void drag_free_offer(Window *w); diff --git a/kitty/glfw.c b/kitty/glfw.c index ce9b35f9f..717f302f7 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1056,6 +1056,12 @@ free_drag_source(void) { zero_at_ptr(&ds); } +void +cancel_current_drag_source(void) { + if (!ds.from_os_window) return; OSWindow *w = os_window_for_id(ds.from_os_window); if (!w || !w->handle) return; + glfwStartDrag(w->handle, NULL, 0, NULL, -3, false); +} + static void drag_source_callback(GLFWwindow *window UNUSED, GLFWDragEvent *ev) { #define finish \ @@ -3115,6 +3121,8 @@ change_drag_thumbnail(PyObject *self UNUSED, PyObject *args) { if (!get_thumbnail(global_state.drag_source.thumbnails, &thumbnail, idx)) return NULL; global_state.drag_source.thumbnail_idx = idx; } else global_state.drag_source.thumbnail_idx = -1; + global_state.drag_source.from_os_window = w->id; + global_state.drag_source.from_window = 0; errno = glfwStartDrag(w->handle, NULL, 0, thumbnail.pixels ? &thumbnail : NULL, -2, false); if (errno != 0) { PyErr_SetFromErrno(PyExc_OSError); diff --git a/kitty/mouse.c b/kitty/mouse.c index 95db938a4..7805f9dfd 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -643,6 +643,10 @@ handle_scrollbar_mouse(Window *w, int button, MouseAction action, int modifiers } // }}} +static double +distance(double x1, double y1, double x2, double y2) { + return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); +} HANDLER(handle_move_event) { modifiers &= ~GLFW_LOCK_MASK; @@ -672,11 +676,13 @@ HANDLER(handle_move_event) { int sz = encode_mouse_button(w, button, button >=0 ? DRAG : MOVE, modifiers); if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf); } } -} - -static double -distance(double x1, double y1, double x2, double y2) { - return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + if (w->drag_source.can_offer && w->drag_source.initial_left_press.at && distance(w->mouse_pos.global_x, w->mouse_pos.global_y, w->drag_source.initial_left_press.x, w->drag_source.initial_left_press.y) > OPT(drag_threshold)) { + zero_at_ptr(&w->drag_source.initial_left_press); + snprintf(mouse_event_buf, sizeof(mouse_event_buf), "%d;t=o:x=%d:y=%d:X=%d:Y=%d", + DND_CODE, w->mouse_pos.cell_x, w->mouse_pos.cell_y, (int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y); + write_escape_code_to_child(screen, ESC_OSC, mouse_event_buf); + debug("Sent drag start event to child\n"); + } } static void @@ -870,6 +876,15 @@ HANDLER(handle_button_event) { bool a, b; if (!set_mouse_position(w, &a, &b)) return; + if (button == GLFW_MOUSE_BUTTON_LEFT) { + if (is_release) { + zero_at_ptr(&w->drag_source.initial_left_press); + } else if (w->drag_source.can_offer) { + w->drag_source.initial_left_press.x = w->mouse_pos.global_x; + w->drag_source.initial_left_press.y = w->mouse_pos.global_x; + w->drag_source.initial_left_press.at = monotonic(); + } + } id_type wid = w->id; if (!dispatch_mouse_event(w, button, is_release ? -1 : 1, modifiers, screen->modes.mouse_tracking_mode != 0)) { if (!suppress_child_forwarding && screen->modes.mouse_tracking_mode != 0) { diff --git a/kitty/options/definition.py b/kitty/options/definition.py index c9270b919..deef73fae 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -884,7 +884,7 @@ sets the shape when dragging in rectangular selection mode. ''' ) -opt('drag_threshold', '5', option_type='positive_int', long_text=''' +opt('drag_threshold', '5', option_type='positive_int', ctype='int', long_text=''' The threshold distance the mouse must move to start a drag and drop. Dragging works for tabs and windows. You can drag tabs to re-order them, detach them into new OS Windows or move them to another OS Window. Similarly, diff --git a/kitty/screen.c b/kitty/screen.c index a92beca6d..952d0b1db 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1530,16 +1530,23 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa } } break; case 's': { - if (cmd->payload_sz) - drop_request_uri_data(w, (const char*)payload, cmd->payload_sz); - else - drop_send_einval(w); + if (cmd->payload_sz) drop_request_uri_data(w, (const char*)payload, cmd->payload_sz); + else drop_send_einval(w); } break; case 'd': { - if (cmd->cell_x > 0) - drop_handle_dir_request(w, (uint32_t)cmd->cell_x, cmd->cell_y); - else - drop_send_einval(w); + if (cmd->cell_x > 0) drop_handle_dir_request(w, (uint32_t)cmd->cell_x, cmd->cell_y); + else drop_send_einval(w); + } break; + case 'o': { + if (cmd->payload_sz > 0) ; + else w->drag_source.can_offer = true; + } break; + case 'O': { + drag_free_offer(w); + w->drag_source.can_offer = false; + if (global_state.drag_source.is_active && global_state.drag_source.from_window == w->id) { + cancel_current_drag_source(); + } } break; } } diff --git a/kitty/state.h b/kitty/state.h index 8abd5f427..741125e48 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -153,6 +153,7 @@ typedef struct Options { double momentum_scroll; double window_drag_tolerance; bool generate_256_palette; + int drag_threshold; } Options; typedef struct WindowLogoRenderData { @@ -284,6 +285,10 @@ typedef struct Window { monotonic_t last_file_send_at; /* time of last successful file chunk write */ id_type file_send_timer; /* pending file-send retry timer, 0 = none */ } drop; + struct { + bool can_offer; + struct { double x, y; monotonic_t at; } initial_left_press; + } drag_source; } Window; typedef struct BorderRect { @@ -441,6 +446,7 @@ typedef struct GlobalState { struct { bool is_active, was_dropped, was_canceled, needs_toplevel_on_wayland; + id_type from_window, from_os_window; char *accepted_mime_type; int action, thumbnail_idx; PyObject *drag_data, *thumbnails; @@ -571,3 +577,4 @@ bool current_framebuffer_is_ok(void); void request_drop_status_update(OSWindow *osw); void register_mimes_for_drop(OSWindow *w, const char **mimes, size_t sz); void request_drop_data(OSWindow *w, id_type wid, const char* mime); +void cancel_current_drag_source(void);