diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index e8fc090e7..8fe492af5 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -4215,7 +4215,8 @@ _glfwPlatformFreeDragSourceData(void) { } int -_glfwPlatformChangeDragImage(const GLFWimage *thumbnail) {@autoreleasepool{ +_glfwPlatformChangeDragImage(const GLFWimage *thumbnail, int make_toplevel) {@autoreleasepool{ + (void)make_toplevel; if (!_glfw.ns.drag_session || !_glfw.ns.drag_view) return 0; _GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id); NSArray *classes = @[[NSPasteboardItem class], [NSFilePromiseProvider class]]; diff --git a/glfw/input.c b/glfw/input.c index 0518a39dc..3a03e505f 100644 --- a/glfw/input.c +++ b/glfw/input.c @@ -1166,7 +1166,7 @@ glfwStartDrag(GLFWwindow* handle, const GLFWDragSourceItem *items, size_t item_c assert(window != NULL); _GLFW_REQUIRE_INIT_OR_RETURN(EINVAL); if (operations == -1) return _glfwPlatformDragDataReady(items[0].mime_type); - if (operations == -2) return _glfwPlatformChangeDragImage(thumbnail); + if (operations == -2) return _glfwPlatformChangeDragImage(thumbnail, item_count); _glfwFreeDragSourceData(); _glfw.drag.instance_id++; if (!items || !item_count) return 0; diff --git a/glfw/internal.h b/glfw/internal.h index d7e58e50a..b377e3ef7 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -837,7 +837,7 @@ void _glfwFreeDragSourceData(void); void _glfwPlatformFreeDragSourceData(void); void _glfwInputDragSourceRequest(_GLFWwindow* window, GLFWDragEvent *ev); int _glfwPlatformDragDataReady(const char *mime_type); -int _glfwPlatformChangeDragImage(const GLFWimage *thumbnail); +int _glfwPlatformChangeDragImage(const GLFWimage *thumbnail, int make_toplevel); void _glfwInputColorScheme(GLFWColorScheme, bool); diff --git a/glfw/null_window.c b/glfw/null_window.c index b46279c9d..675332f34 100644 --- a/glfw/null_window.c +++ b/glfw/null_window.c @@ -542,7 +542,7 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) { } void _glfwPlatformFreeDragSourceData(void) {} int _glfwPlatformDragDataReady(const char *mime_type) { (void) mime_type; return 0; } -int _glfwPlatformChangeDragImage(const GLFWimage *thumbnail) { (void)thumbnail; return 0; } +int _glfwPlatformChangeDragImage(const GLFWimage *thumbnail, int make_toplevel) { (void)thumbnail; (void)make_toplevel; return 0; } const char** _glfwPlatformGetDropMimeTypes(GLFWDropData* drop UNUSED, int* count) { diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 511b7807d..763ee6ce4 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -3190,7 +3190,8 @@ add_drag_watch(int fd) { } int -_glfwPlatformChangeDragImage(const GLFWimage *thumbnail) { +_glfwPlatformChangeDragImage(const GLFWimage *thumbnail, int make_toplevel) { + (void)make_toplevel; // TODO: Implement me if (!_glfw.wl.drag.drag_icon || !thumbnail || !thumbnail->pixels) return 0; struct wl_buffer* icon_buffer = createShmBuffer(thumbnail, false, true); if (!icon_buffer) return ENOMEM; @@ -3353,9 +3354,9 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) { wl_data_source_add_listener(_glfw.wl.drag.source, &drag_source_listener, NULL); // Set up the drag icon surface if thumbnail is provided + _glfw.wl.drag.drag_icon = wl_compositor_create_surface(_glfw.wl.compositor); if (thumbnail && thumbnail->pixels) { 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) { diff --git a/glfw/x11_window.c b/glfw/x11_window.c index 5d52d5e91..4ced436db 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -4471,7 +4471,8 @@ _glfwPlatformFreeDragSourceData(void) { } int -_glfwPlatformChangeDragImage(const GLFWimage *thumbnail) { +_glfwPlatformChangeDragImage(const GLFWimage *thumbnail, int make_toplevel) { + (void)make_toplevel; if (!_glfw.x11.drag.active) return 0; if (!thumbnail || !thumbnail->pixels || thumbnail->width <= 0 || thumbnail->height <= 0) return 0; diff --git a/kitty/boss.py b/kitty/boss.py index 292e6dec5..aa70b5d9d 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -73,6 +73,7 @@ from .fast_data_types import ( apply_options_update, background_opacity_of, change_background_opacity, + change_drag_thumbnail, cocoa_hide_app, cocoa_hide_other_apps, cocoa_minimize_os_window, @@ -1900,6 +1901,8 @@ class Boss: if tab_id and drag_started and (tab := self.tab_for_id(tab_id)): central, tab_bar = viewport_for_window(os_window_id)[:2] in_tab_bar = tab_bar.left <= x < tab_bar.right and tab_bar.top <= y < tab_bar.bottom + detach = not in_tab_bar or tab.os_window_id != tm.os_window_id + change_drag_thumbnail(tab.os_window_id, 1 if detach else 0, detach) for q in self.all_tab_managers: is_dest = q is tm and (in_tab_bar or os_window_id != tab.os_window_id) q.on_tab_drop_move(tab_id, is_dest, x, y) @@ -3358,7 +3361,7 @@ class Boss: 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) + 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 diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 08a13bbbf..98eb19bfb 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1,5 +1,5 @@ import termios -from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Tuple, TypedDict, Union, overload +from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Sequence, Tuple, TypedDict, Union, overload from kitty.borders import Border from kitty.boss import Boss @@ -1814,10 +1814,10 @@ class StreamingBase64Encodeer: def start_drag_with_data( - os_window_id: int, data_map: dict[str, bytes], thumbnail: bytes = b'', width: int = 0, height: int = 0, + os_window_id: int, data_map: dict[str, bytes], thumbnails: Sequence[tuple[bytes, int, int]], operations: int = GLFW_DRAG_OPERATION_MOVE ) -> None: ... - +def change_drag_thumbnail(os_window_id: int, idx: int = -1, make_toplevel: bool = False) -> None: ... def draw_single_line_of_text(os_window_id: int, text: str, fg: int, bg: int, width: int, padding_y: int = 2) -> bytes: ... def set_tab_being_dragged(tab_id: int = 0, drag_started: bool = False, x: float = 0, y: float = 0) -> None: ... def get_tab_being_dragged() -> tuple[int, bool, float, float]: ... diff --git a/kitty/glfw.c b/kitty/glfw.c index 6e616237f..604b41dcf 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -784,7 +784,7 @@ application_close_requested_callback(int flags) { void free_drag_source(void) { if (ds.accepted_mime_type) free(ds.accepted_mime_type); - Py_CLEAR(ds.drag_data); + Py_CLEAR(ds.drag_data); Py_CLEAR(ds.thumbnails); zero_at_ptr(&ds); } @@ -2787,16 +2787,49 @@ draw_single_line_of_text(PyObject *self UNUSED, PyObject *args) { return Py_NewRef(ans); } +static bool +get_thumbnail(PyObject *thumbnails, GLFWimage *thumbnail, int idx) { + RAII_PyObject(t, PySequence_GetItem(thumbnails, idx)); + if (!PyTuple_Check(t) || PyTuple_GET_SIZE(t) != 3) { PyErr_SetString(PyExc_TypeError, "thumbnail must be a 3-tuple"); return false; } + thumbnail->pixels = (uint8_t*)PyBytes_AS_STRING(PyTuple_GET_ITEM(t, 0)); + thumbnail->width = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(t, 1)); + thumbnail->height = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(t, 2)); + return true; +} + 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; +change_drag_thumbnail(PyObject *self UNUSED, PyObject *args) { + unsigned long long os_window_id; int idx = -1; int make_toplevel = 0; + if (!PyArg_ParseTuple(args, "K|ip", &os_window_id, &idx, &make_toplevel)) return NULL; + if (global_state.drag_source.thumbnail_idx == idx) Py_RETURN_NONE; 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; } + GLFWimage thumbnail = {0}; + if (idx >=0 && global_state.drag_source.thumbnails && idx < PySequence_Size(global_state.drag_source.thumbnails)) { + 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; + errno = glfwStartDrag(w->handle, NULL, make_toplevel, thumbnail.pixels ? &thumbnail : NULL, -2); + if (errno != 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + Py_RETURN_NONE; +} + +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}; + unsigned long long os_window_id; PyObject *data_map; + int operations = GLFW_DRAG_OPERATION_MOVE; + PyObject *thumbnails = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kw, "KO!|Oi", (char**)kwlist, + &os_window_id, &PyDict_Type, &data_map, &thumbnails, &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; } + GLFWimage thumbnail = {0}; + if (!PySequence_Check(thumbnails)) { PyErr_SetString(PyExc_TypeError, "thumbnails must be a sequence"); return NULL; } + if (thumbnails && PySequence_Size(thumbnails) && !get_thumbnail(thumbnails, &thumbnail, 0)) return NULL; RAII_ALLOC(GLFWDragSourceItem, items, calloc(PyDict_Size(data_map), sizeof(GLFWDragSourceItem))); if (!items) { PyErr_NoMemory(); return NULL; } PyObject *key, *value; Py_ssize_t pos = 0; size_t num = 0; @@ -2807,11 +2840,12 @@ start_drag_with_data(PyObject *self UNUSED, PyObject *args, PyObject *kw) { item->mime_type = PyUnicode_AsUTF8(key); item->optional_data = PyBytes_AS_STRING(value); item->data_size = PyBytes_GET_SIZE(value); } - GLFWimage thumbnail = {.pixels=thumbnail_data, .width=width, .height=height}; free_drag_source(); global_state.drag_source.is_active = true; global_state.drag_source.drag_data = Py_NewRef(data_map); - errno = glfwStartDrag(w->handle, items, num, thumbnail_data ? &thumbnail : NULL, operations); + if (thumbnails) global_state.drag_source.thumbnails = Py_NewRef(thumbnails); + global_state.drag_source.thumbnail_idx = thumbnail.pixels ? 0 : -1; + errno = glfwStartDrag(w->handle, items, num, thumbnail.pixels ? &thumbnail : NULL, operations); if (errno != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; @@ -2831,6 +2865,7 @@ static PyMethodDef module_methods[] = { 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(change_drag_thumbnail, METH_VARARGS), METHODB(draw_single_line_of_text, METH_VARARGS), METHODB(set_default_window_icon, METH_VARARGS), METHODB(set_os_window_icon, METH_VARARGS), diff --git a/kitty/state.h b/kitty/state.h index 677b0560c..a2690a5ac 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -386,8 +386,8 @@ typedef struct GlobalState { struct { bool is_active, was_dropped, was_canceled; char *accepted_mime_type; - int action; - PyObject *drag_data; + int action, thumbnail_idx; + PyObject *drag_data, *thumbnails; } drag_source; struct { id_type os_window, window; diff --git a/kitty/tabs.py b/kitty/tabs.py index 82eda3947..42b7ae4b3 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -1654,10 +1654,11 @@ class TabManager: # {{{ bg = color_as_int(opts.inactive_tab_background) title_pixels = draw_single_line_of_text(self.os_window_id, title, 0xff000000 | fg, 0xff000000 | bg, width) title_height = len(title_pixels) // (width * 4) + thumbnails = ((title_pixels, width, title_height), (title_pixels + pixels, width, title_height + height)) drag_data = { f'application/net.kovidgoyal.kitty-tab-{os.getpid()}': str(tab.id).encode(), } - start_drag_with_data(self.os_window_id, drag_data, title_pixels + pixels, width, title_height + height) + start_drag_with_data(self.os_window_id, drag_data, thumbnails) break else: set_tab_being_dragged()