From 07a9f2bcaac88f03b3fc04c9baabb2097d74d6b2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 7 Feb 2026 13:50:16 +0530 Subject: [PATCH] API to start a drag --- glfw/glfw3.h | 2 +- glfw/wl_init.c | 4 +- glfw/wl_platform.h | 4 +- glfw/wl_window.c | 86 ++++++++++++++++++--------------------- glfw/x11_window.c | 2 +- kitty/boss.py | 10 +++++ kitty/fast_data_types.pyi | 9 ++++ kitty/glfw-wrapper.h | 2 +- kitty/glfw.c | 79 ++++++++++++++++++++++++++++++++++- kitty/state.c | 3 ++ kitty/state.h | 11 +++++ 11 files changed, 158 insertions(+), 54 deletions(-) diff --git a/glfw/glfw3.h b/glfw/glfw3.h index 58b908ee5..01bc953a4 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -2068,7 +2068,7 @@ typedef struct GLFWimage int height; /*! The pixel data of this image, arranged left-to-right, top-to-bottom. */ - unsigned char* pixels; + const unsigned char* pixels; } GLFWimage; /*! @brief Gamepad input state diff --git a/glfw/wl_init.c b/glfw/wl_init.c index edd2401a5..76c19bac9 100644 --- a/glfw/wl_init.c +++ b/glfw/wl_init.c @@ -608,9 +608,7 @@ static void registryHandleGlobal(void* data UNUSED, else if (is(wl_data_device_manager)) { _glfw.wl.dataDeviceManager = - wl_registry_bind(registry, name, - &wl_data_device_manager_interface, - 1); + wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3); if (_glfw.wl.seat && _glfw.wl.dataDeviceManager && !_glfw.wl.dataDevice) { _glfwSetupWaylandDataDevice(); } diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index 64bc7378f..87a5b37df 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -414,8 +414,10 @@ typedef struct _GLFWlibraryWayland struct wl_data_source* source; char** mimes; // Array of MIME type strings int mime_count; // Number of MIME types - _GLFWwindow* window; // Window that initiated the drag + GLFWid window_id; // Window that initiated the drag GLFWDragSourceData* current_request; // Current data request being processed + struct wl_surface *drag_icon; + struct wp_viewport *drag_viewport; } drag; } _GLFWlibraryWayland; diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 5008ce1b2..4afae0ae6 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -3006,7 +3006,8 @@ GLFWAPI bool glfwWaylandBeep(GLFWwindow *handle) { // Drag operation implementation -static void cleanup_drag_source_data(GLFWDragSourceData* data) { +static void +cleanup_drag_source_data(GLFWDragSourceData* data) { if (!data) return; if (data->write_fd >= 0) { close(data->write_fd); @@ -3016,11 +3017,11 @@ static void cleanup_drag_source_data(GLFWDragSourceData* data) { free(data); } -static void cleanup_drag(void) { +static void +cleanup_drag(struct wl_data_source *source) { // Notify the application that the drag source is closed - if (_glfw.wl.drag.window && _glfw.wl.drag.window->callbacks.dragSource) { - _glfwInputDragSourceRequest(_glfw.wl.drag.window, NULL, NULL); - } + _GLFWwindow *window = _glfwWindowForId(_glfw.wl.drag.window_id); + if (window && window->callbacks.dragSource) _glfwInputDragSourceRequest(window, NULL, NULL); // Clean up any pending data request if (_glfw.wl.drag.current_request) { @@ -3029,18 +3030,23 @@ static void cleanup_drag(void) { } // Clean up MIME type strings - for (int i = 0; i < _glfw.wl.drag.mime_count; i++) { - free(_glfw.wl.drag.mimes[i]); - } + for (int i = 0; i < _glfw.wl.drag.mime_count; i++) free(_glfw.wl.drag.mimes[i]); free(_glfw.wl.drag.mimes); _glfw.wl.drag.mimes = NULL; _glfw.wl.drag.mime_count = 0; - _glfw.wl.drag.window = NULL; + _glfw.wl.drag.window_id = 0; + if (_glfw.wl.drag.drag_viewport) wp_viewport_destroy(_glfw.wl.drag.drag_viewport); + if (_glfw.wl.drag.drag_icon) wl_surface_destroy(_glfw.wl.drag.drag_icon); + _glfw.wl.drag.drag_icon = NULL; _glfw.wl.drag.drag_viewport = NULL; + if (_glfw.wl.drag.source && _glfw.wl.drag.source != source) wl_data_source_destroy(_glfw.wl.drag.source); + _glfw.wl.drag.source = NULL; + if (source) wl_data_source_destroy(source); } static void drag_source_send(void *data UNUSED, struct wl_data_source *source UNUSED, const char *mime_type, int fd) { - if (!_glfw.wl.drag.window) { + _GLFWwindow *window = _glfwWindowForId(_glfw.wl.drag.window_id); + if (!window) { close(fd); return; } @@ -3052,7 +3058,7 @@ drag_source_send(void *data UNUSED, struct wl_data_source *source UNUSED, const return; } - request->window_id = _glfw.wl.drag.window ? _glfw.wl.drag.window->id : 0; + request->window_id = _glfw.wl.drag.window_id; request->mime_type = _glfw_strdup(mime_type); request->write_fd = fd; request->finished = false; @@ -3064,25 +3070,16 @@ drag_source_send(void *data UNUSED, struct wl_data_source *source UNUSED, const } // Store as current request - if (_glfw.wl.drag.current_request) { - cleanup_drag_source_data(_glfw.wl.drag.current_request); - } + if (_glfw.wl.drag.current_request) cleanup_drag_source_data(_glfw.wl.drag.current_request); _glfw.wl.drag.current_request = request; // Notify the application via callback - _glfwInputDragSourceRequest(_glfw.wl.drag.window, mime_type, request); + _glfwInputDragSourceRequest(window, mime_type, request); } static void drag_source_cancelled(void *data UNUSED, struct wl_data_source *source) { - // Clean up drag data - if (_glfw.wl.drag.source == source) { - cleanup_drag(); - _glfw.wl.drag.source = NULL; - } - if (source) { - wl_data_source_destroy(source); - } + cleanup_drag(source); } static void @@ -3113,10 +3110,7 @@ static const struct wl_data_source_listener drag_source_listener = { void _glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) { - if (_glfw.wl.drag.source) { - drag_source_cancelled(NULL, _glfw.wl.drag.source); - _glfw.wl.drag.source = NULL; - } + cleanup_drag(_glfw.wl.drag.source); } int @@ -3154,7 +3148,7 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const char* const* mime_types, int m // Allocate storage for MIME types _glfw.wl.drag.mimes = calloc(mime_count, sizeof(char*)); _glfw.wl.drag.mime_count = mime_count; - _glfw.wl.drag.window = window; + _glfw.wl.drag.window_id = window->id; if (!_glfw.wl.drag.mimes) { _glfwPlatformCancelDrag(window); @@ -3176,33 +3170,33 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const char* const* mime_types, int m wl_data_source_add_listener(_glfw.wl.drag.source, &drag_source_listener, NULL); // Set up the drag icon surface if thumbnail is provided - struct wl_surface* icon_surface = NULL; - struct wl_buffer* icon_buffer = NULL; - if (thumbnail && thumbnail->pixels) { - icon_surface = wl_compositor_create_surface(_glfw.wl.compositor); - if (icon_surface) { + struct wl_buffer* icon_buffer = NULL; + _glfw.wl.drag.drag_icon = wl_compositor_create_surface(_glfw.wl.compositor); + if (_glfw.wl.drag.drag_icon) { icon_buffer = createShmBuffer(thumbnail, false, true); if (icon_buffer) { - wl_surface_attach(icon_surface, icon_buffer, 0, 0); - wl_surface_commit(icon_surface); + if (_glfw.wl.wp_viewporter) { + double f_scale = _glfwWaylandWindowScale(window); + int logical_width = (int)(thumbnail->width / f_scale); + int logical_height = (int)(thumbnail->height / f_scale); + _glfw.wl.drag.drag_viewport = wp_viewporter_get_viewport( + _glfw.wl.wp_viewporter, _glfw.wl.drag.drag_icon); + wp_viewport_set_destination(_glfw.wl.drag.drag_viewport, logical_width, logical_height); + } else { + int scale = _glfwWaylandIntegerWindowScale(window); + wl_surface_set_buffer_scale(_glfw.wl.drag.drag_icon, scale); + } + wl_surface_attach(_glfw.wl.drag.drag_icon, icon_buffer, 0, 0); + wl_surface_commit(_glfw.wl.drag.drag_icon); + wl_buffer_destroy(icon_buffer); } } } - // Start the drag operation - wl_data_device_start_drag(_glfw.wl.dataDevice, _glfw.wl.drag.source, - window->wl.surface, icon_surface, + wl_data_device_start_drag(_glfw.wl.dataDevice, _glfw.wl.drag.source, window->wl.surface, _glfw.wl.drag.drag_icon, _glfw.wl.pointer_serial); - // Clean up icon resources (the compositor takes ownership) - if (icon_buffer) { - wl_buffer_destroy(icon_buffer); - } - if (icon_surface) { - wl_surface_destroy(icon_surface); - } - return 0; } diff --git a/glfw/x11_window.c b/glfw/x11_window.c index 3a37c35c5..91c765e6c 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -2438,7 +2438,7 @@ void _glfwPlatformSetWindowIcon(_GLFWwindow* window, for (j = 0; j < images[i].width * images[i].height; j++) { - unsigned char *p = images->pixels + j * 4; + const unsigned char *p = images->pixels + j * 4; const unsigned char r = *p++, g = *p++, b = *p++, a = *p++; *target++ = a << 24 | (r << 16) | (g << 8) | b; } diff --git a/kitty/boss.py b/kitty/boss.py index 0c69bab1c..6e3fd0745 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -91,6 +91,7 @@ grab_keyboard, is_layer_shell_supported, last_focused_os_window_id, + load_png_data, macos_cycle_through_os_windows, mark_os_window_for_close, monitor_pid, @@ -109,6 +110,7 @@ set_os_window_chrome, set_os_window_size, set_os_window_title, + start_drag_with_data, thread_write, toggle_fullscreen, toggle_maximized, @@ -3301,6 +3303,14 @@ def simulate_color_scheme_preference_change(self, which: str) -> None: case _: self.show_error(_('Unknown color scheme type'), _('{} is not a valid color scheme type').format(which)) + @ac('debug', ''' Start a test drag operation, for use with mouse_map ''') + def test_dragging(self) -> None: + if wid := current_os_window(): + with open(logo_png_file, 'rb') as f: + rgba, width, height = load_png_data(f.read()) + drag_data = {'text/plain': b'This is a test drag of some basic text with the kitty logo as the drag icon.'} + start_drag_with_data(wid, drag_data, rgba, width, height) + def launch_urls(self, *urls: str, no_replace_window: bool = False) -> None: from .launch import force_window_launch from .open_actions import actions_for_launch diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index a38693fab..f9829651f 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -20,6 +20,9 @@ COLOR_IS_SPECIAL: int COLOR_NOT_SET: int COLOR_IS_RGB: int COLOR_IS_INDEX: int +GLFW_DRAG_OPERATION_MOVE: int +GLFW_DRAG_OPERATION_COPY: int +GLFW_DRAG_OPERATION_GENERIC: int GLFW_LAYER_SHELL_NONE: int GLFW_LAYER_SHELL_PANEL: int GLFW_LAYER_SHELL_TOP: int @@ -1808,3 +1811,9 @@ class StreamingBase64Encodeer: def reset(self) -> bytes: ... # encode the specified data, return number of bytes written dest should be at least 4/3 *src + 2 bytes in size def encode_into(self, dest: WriteableBuffer, src: ReadableBuffer) -> int: ... + + +def start_drag_with_data( + os_window_id: int, data_map: dict[str, bytes], thumbnail: bytes = b'', width: int = 0, height: int = 0, + operations: int = GLFW_DRAG_OPERATION_MOVE +) -> None: ... diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index b3df6b33b..a48ff16dc 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1796,7 +1796,7 @@ typedef struct GLFWimage int height; /*! The pixel data of this image, arranged left-to-right, top-to-bottom. */ - unsigned char* pixels; + const unsigned char* pixels; } GLFWimage; /*! @brief Gamepad input state diff --git a/kitty/glfw.c b/kitty/glfw.c index 54ecaf04d..b607ff8d3 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -771,6 +771,52 @@ application_close_requested_callback(int flags) { } } +#define ds (global_state.drag_source) +static void +try_sending_drag_source_data(id_type timer_id UNUSED, void *callback_data UNUSED) { + bool incomplete = false; + for (size_t i = 0; i < ds.num_ongoing_transfers; i++) { +#define t ds.ongoing_transfers[i] + size_t sz = PyBytes_GET_SIZE(t.weakref_to_data_object); + ssize_t ret; + if (sz > t.offset) { + const char *data = PyBytes_AS_STRING(t.weakref_to_data_object); + ret = glfwSendDragData(t.platform_data, data + t.offset, sz - t.offset); + } else ret = glfwSendDragData(t.platform_data, NULL, 0); + if (ret >= 0) { + t.offset += ret; + if (t.offset < sz) incomplete = true; + else glfwSendDragData(t.platform_data, NULL, 0); // tell glfw transfer is complete + } else { + log_error("Failed to send data from drag source with error: %s", strerror(-ret)); + t.offset = sz; + } +#undef t + } + if (incomplete) add_main_loop_timer(ms_double_to_monotonic_t(2), false, try_sending_drag_source_data, NULL, NULL); +} + +static void +drag_source_callback(GLFWwindow *window UNUSED, const char* mime_type, GLFWDragSourceData* source_data) { + PyObject *data; + if (mime_type == NULL) { + ds.is_active = false; + Py_CLEAR(ds.drag_data); + return; + } + if (!ds.is_active || !ds.drag_data || !(data = PyDict_GetItemString(ds.drag_data, mime_type))) { + glfwSendDragData(source_data, NULL, EINVAL); + return; + } + ensure_space_for(&ds, ongoing_transfers, ds.ongoing_transfers[0], ds.num_ongoing_transfers + 1, ongoing_transfers_capacity, 8, true); + ds.ongoing_transfers[ds.num_ongoing_transfers].platform_data = source_data; + ds.ongoing_transfers[ds.num_ongoing_transfers].weakref_to_data_object = data; + ds.ongoing_transfers[ds.num_ongoing_transfers].offset = 0; + ds.num_ongoing_transfers++; + try_sending_drag_source_data(0, NULL); +} +#undef ds + static char* get_current_selection(void) { if (!global_state.boss) return NULL; @@ -1629,6 +1675,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { glfwSetKeyboardCallback(glfw_window, key_callback); glfwSetDropCallback(glfw_window, drop_callback); glfwSetDragCallback(glfw_window, drag_callback); + glfwSetDragSourceCallback(glfw_window, drag_source_callback); monotonic_t now = monotonic(); w->is_focused = true; w->cursor_blink_zero_time = now; @@ -2704,6 +2751,32 @@ grab_keyboard(PyObject *self UNUSED, PyObject *action) { return Py_NewRef(glfwGrabKeyboard(action == Py_None ? 2 : PyObject_IsTrue(action)) ? Py_True : Py_False); } +static PyObject* +start_drag_with_data(PyObject *self UNUSED, PyObject *args, PyObject *kw) { + static const char* kwlist[] = {"os_window_id", "data_map", "thumbnail", "width", "height", "operations", NULL}; + unsigned long long os_window_id; PyObject *data_map; + const unsigned char *thumbnail_data = NULL; Py_ssize_t thumbnail_sz = 0; int height = 0, width = 0; + int operations = GLFW_DRAG_OPERATION_MOVE; + if (!PyArg_ParseTupleAndKeywords(args, kw, "KO!|y#iii", (char**)kwlist, + &os_window_id, &PyDict_Type, &data_map, &thumbnail_data, &thumbnail_sz, &width, &height, &operations)) return NULL; + OSWindow *w = os_window_for_id(os_window_id); + if (!w || !w->handle) { PyErr_SetString(PyExc_KeyError, "OS Window with specified id does not exist"); return NULL; } + RAII_ALLOC(const char*, mime_types, calloc(PyDict_Size(data_map), sizeof(const char*))); + if (!mime_types) { PyErr_NoMemory(); return NULL; } + PyObject *key, *value; Py_ssize_t pos = 0; int num = 0; + while (PyDict_Next(data_map, &pos, &key, &value)) { + if (!PyUnicode_Check(key)) { PyErr_SetString(PyExc_TypeError, "data_map must have string keys"); return NULL; } + if (!PyBytes_Check(value)) { PyErr_SetString(PyExc_TypeError, "data_map must have bytes values"); return NULL; } + mime_types[num++] = PyUnicode_AsUTF8(key); + } + GLFWimage thumbnail = {.pixels=thumbnail_data, .width=width, .height=height}; + global_state.drag_source.is_active = true; + Py_CLEAR(global_state.drag_source.drag_data); global_state.drag_source.drag_data = Py_NewRef(data_map); + global_state.drag_source.num_ongoing_transfers = 0; + glfwStartDrag(w->handle, mime_types, num, thumbnail_data ? &thumbnail : NULL, operations); + Py_RETURN_NONE; +} + // Boilerplate {{{ static PyMethodDef module_methods[] = { @@ -2715,6 +2788,7 @@ static PyMethodDef module_methods[] = { METHODB(grab_keyboard, METH_O), METHODB(pointer_name_to_css_name, METH_O), {"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL}, + {"start_drag_with_data", (PyCFunction)(void (*) (void))(start_drag_with_data), METH_VARARGS | METH_KEYWORDS, NULL}, METHODB(set_default_window_icon, METH_VARARGS), METHODB(set_os_window_icon, METH_VARARGS), METHODB(set_clipboard_data_types, METH_VARARGS), @@ -2758,7 +2832,7 @@ static PyMethodDef module_methods[] = { }; void cleanup_glfw(void) { - if (logo.pixels) free(logo.pixels); + if (logo.pixels) free((void*)logo.pixels); logo.pixels = NULL; Py_CLEAR(edge_spacing_func); #ifndef __APPLE__ @@ -2775,6 +2849,9 @@ init_glfw(PyObject *m) { // constants {{{ #define ADDC(n) if(PyModule_AddIntConstant(m, #n, n) != 0) return false; + ADDC(GLFW_DRAG_OPERATION_MOVE); + ADDC(GLFW_DRAG_OPERATION_COPY); + ADDC(GLFW_DRAG_OPERATION_GENERIC); ADDC(GLFW_RELEASE); ADDC(GLFW_PRESS); ADDC(GLFW_REPEAT); diff --git a/kitty/state.c b/kitty/state.c index 3b9923bf3..448c1607c 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -1581,6 +1581,9 @@ finalize(void) { free_bgimage(&global_state.bgimage, false); free_window_logo_table(&global_state.all_window_logos); global_state.bgimage = NULL; + free(global_state.drag_source.ongoing_transfers); + Py_CLEAR(global_state.drag_source.drag_data); + zero_at_ptr(&global_state.drag_source); free_allocs_in_options(&global_state.opts); } diff --git a/kitty/state.h b/kitty/state.h index 3f8aa9295..7db06b635 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -374,6 +374,17 @@ typedef struct GlobalState { int gl_version; bool supports_framebuffer_srgb; PyObject *options_object; + + struct { + bool is_active; + PyObject *drag_data; + struct { + void *platform_data; + size_t offset; + PyObject *weakref_to_data_object; + } *ongoing_transfers; + size_t num_ongoing_transfers, ongoing_transfers_capacity; + } drag_source; } GlobalState; extern GlobalState global_state;