Change drag thumbnail when in detach/move modes

This commit is contained in:
Kovid Goyal
2026-02-22 13:09:56 +05:30
parent a4514fcf66
commit 531fb3e1b1
11 changed files with 66 additions and 24 deletions

View File

@@ -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]];

2
glfw/input.c vendored
View File

@@ -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;

2
glfw/internal.h vendored
View File

@@ -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);

2
glfw/null_window.c vendored
View File

@@ -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)
{

5
glfw/wl_window.c vendored
View File

@@ -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) {

3
glfw/x11_window.c vendored
View File

@@ -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;

View File

@@ -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

View File

@@ -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]: ...

View File

@@ -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),

View File

@@ -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;

View File

@@ -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()