From a29b9387facfda7dda854e0727bb32d49f3b8c52 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 4 Apr 2026 14:27:03 +0530 Subject: [PATCH] More work on DnD protocol implementation --- docs/dnd-protocol.rst | 27 ++++++++++++++------------- kitty/dnd.c | 22 +++++++++++++++++----- kitty/glfw.c | 39 +++++++++++++++++++++++++++++++++++++++ kitty/state.h | 1 + 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 57c97a2ce..f1ca3e955 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -242,19 +242,20 @@ the ``t=o`` key indicating the offer if the data is available. To associate one or more images with the drag operation, the terminal program must transmit the data for the image with the ``idx`` value above being a -negative number starting with ``-1`` for the first image and so on. When -transmitting images, the image data format is specified using the ``y`` key. -A value of ``y=24`` mean 24bit RGB data and ``y=32`` means 32bit RGBA data. -Colors in the RGB/A data must be in the sRGB color space. -Using ``y=100`` means the data is a PNG image. Additionally, the ``X`` and -``Y`` keys must be used to specify the width and height of the image data in -pixels. If the size of the transmitted data does not match the image dimensions -the terminal must replay with ``t=R ; EINVAL``. Terminals are free to impose a -limit on the amount of image data, too avoid Denial-of-service attacks. If the -image data is too much or the image is too large they must reply with ``t=R ; -EFBIG`` and abort the drag. By default, the drag will be started using the -first image, if any. During the drag, the terminal program can change the -image by sending:: +negative number starting with ``-1`` for the first image and so on. Clients +**must** transmit all images consecutively in order, starting with the frist, +then the second and so on. When transmitting images, the image data format is +specified using the ``y`` key. A value of ``y=24`` mean 24bit RGB data and +``y=32`` means 32bit RGBA data. Colors in the RGB/A data must be in the sRGB +color space. Using ``y=100`` means the data is a PNG image. Additionally, the +``X`` and ``Y`` keys must be used to specify the width and height of the image +data in pixels. If the size of the transmitted data does not match the image +dimensions the terminal must replay with ``t=R ; EINVAL``. Terminals are free +to impose a limit on the amount of image data, too avoid Denial-of-service +attacks. If the image data is too much or the image is too large they must +reply with ``t=R ; EFBIG`` and abort the drag. By default, the drag will be +started using the first image, if any. During the drag, the terminal program +can change the image by sending:: OSC _dnd_code ; t=P:x=idx ST diff --git a/kitty/dnd.c b/kitty/dnd.c index 70aafce1f..c28e5bdf1 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -862,11 +862,8 @@ drop_left_child(Window *w) { #define ds w->drag_source -void -drag_free_offer(Window *w) { - free(ds.mimes_buf); ds.mimes_buf = NULL; - ds.allowed_operations = 0; - ds.state = DRAG_SOURCE_NONE; +static void +drag_free_built_data(Window *w) { if (ds.items) { for (size_t i=0; i < ds.num_mimes; i++) free(ds.items[i].optional_data); free(ds.items); @@ -875,6 +872,14 @@ drag_free_offer(Window *w) { if (ds.images[i].data) free(ds.images[i].data); zero_at_ptr(ds.images + i); } +} + +void +drag_free_offer(Window *w) { + free(ds.mimes_buf); ds.mimes_buf = NULL; + drag_free_built_data(w); + ds.allowed_operations = 0; + ds.state = DRAG_SOURCE_NONE; ds.num_mimes = 0; ds.pre_sent_total_sz = 0; ds.images_sent_total_sz = 0; @@ -1028,6 +1033,13 @@ drag_start(Window *w) { if (img.sz != (size_t)img.width * (size_t)img.height * 4u) abrt(EINVAL); } } + int err = start_window_drag(w); + if (err != 0) { + abrt(err); + } else { + drag_free_built_data(w); + ds.state = DRAG_SOURCE_STARTED; + } } #undef img diff --git a/kitty/glfw.c b/kitty/glfw.c index 9ebb37453..15f35d94e 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -3147,6 +3147,44 @@ change_drag_thumbnail(PyObject *self UNUSED, PyObject *args) { Py_RETURN_NONE; } +int +start_window_drag(Window *w) { + OSWindow *osw = os_window_for_kitty_window(w->id); + if (!osw || !osw->handle) return EINVAL; + RAII_ALLOC(GLFWDragSourceItem, items, calloc(w->drag_source.num_mimes, sizeof(GLFWDragSourceItem))); + if (!items) return ENOMEM; + for (size_t i = 0; i < w->drag_source.num_mimes; i++) { + items[i].mime_type = w->drag_source.items[i].mime_type; + items[i].optional_data = (char*)w->drag_source.items[i].optional_data; + items[i].data_size = w->drag_source.items[i].data_size; + } + size_t num_images = 0; + for (size_t i = 0; i < arraysz(w->drag_source.images); i++) if (w->drag_source.images[i].data) num_images++; + RAII_PyObject(images, PyTuple_New(num_images)); + if (!images) { PyErr_Clear(); return ENOMEM; } + for (size_t i = 0, n = 0; i < arraysz(w->drag_source.images); i++) { + if (w->drag_source.images[i].data) { + PyObject *t = Py_BuildValue( + "y#ii", w->drag_source.images[i].data, w->drag_source.images[i].sz, w->drag_source.images[i].width, w->drag_source.images[i].height); + if (!t) { PyErr_Clear(); return ENOMEM; } + PyTuple_SET_ITEM(images, n, t); n++; + } + } + GLFWimage thumbnail = {0}; + if (w->drag_source.img_idx < num_images && !get_thumbnail(images, &thumbnail, w->drag_source.img_idx)) return ENOMEM; + free_drag_source(); + global_state.drag_source.thumbnails = Py_NewRef(images); + global_state.drag_source.is_active = true; + global_state.drag_source.needs_toplevel_on_wayland = true; + global_state.drag_source.from_window = w->id; + global_state.drag_source.from_os_window = osw->id; + global_state.drag_source.thumbnail_idx = w->drag_source.img_idx < num_images ? (int)w->drag_source.img_idx : -1; + int ret = glfwStartDrag(osw->handle, items, w->drag_source.num_mimes, thumbnail.pixels ? &thumbnail : NULL, w->drag_source.allowed_operations, true); + if (ret != 0) free_drag_source(); + return ret; +} + + static PyObject* start_drag_with_data(PyObject *self UNUSED, PyObject *args, PyObject *kw) { static const char* kwlist[] = {"os_window_id", "data_map", "thumbnails", "operations", NULL}; @@ -3185,6 +3223,7 @@ start_drag_with_data(PyObject *self UNUSED, PyObject *args, PyObject *kw) { errno = glfwStartDrag(w->handle, items, num, thumbnail.pixels ? &thumbnail : NULL, operations, needs_toplevel_on_wayland); if (errno != 0) { PyErr_SetFromErrno(PyExc_OSError); + free_drag_source(); return NULL; } Py_RETURN_NONE; diff --git a/kitty/state.h b/kitty/state.h index c513646ad..f09c47d14 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -594,3 +594,4 @@ 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); bool change_drag_image(int idx); +int start_window_drag(Window *w);