API to start a drag

This commit is contained in:
Kovid Goyal 2026-02-07 13:50:16 +05:30
parent 5ea35cbbfc
commit 07a9f2bcaa
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
11 changed files with 158 additions and 54 deletions

2
glfw/glfw3.h vendored
View file

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

4
glfw/wl_init.c vendored
View file

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

4
glfw/wl_platform.h vendored
View file

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

86
glfw/wl_window.c vendored
View file

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

2
glfw/x11_window.c vendored
View file

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

View file

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

View file

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

2
kitty/glfw-wrapper.h generated
View file

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

View file

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

View file

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

View file

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