Allow reading dropped data in chunks

Fixes #9470
This commit is contained in:
copilot-swe-agent[bot] 2026-02-04 06:21:29 +00:00 • committed by Kovid Goyal
parent 0ab7aec690
commit ed5eb8f45c
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
7 changed files with 759 additions and 135 deletions

View file

@ -33,6 +33,7 @@
#include <Availability.h>
#import <CoreServices/CoreServices.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <errno.h>
#include <float.h>
#include <string.h>
#include <assert.h>
@ -1496,32 +1497,34 @@ static void freeFilteredDragMimes(_GLFWwindow* window, int old_count, int new_co
_glfwInputCursorPos(window, pos.x, contentRect.size.height - pos.y);
NSPasteboard* pasteboard = [sender draggingPasteboard];
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]]
options:options];
if (!objs) return NO;
const NSUInteger count = [objs count];
NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096]; // auto-released
if (count)
{
for (NSUInteger i = 0; i < count; i++)
{
id obj = objs[i];
if ([obj isKindOfClass:[NSURL class]]) {
NSURL *url = (NSURL*)obj;
if ([uri_list length] > 0) [uri_list appendString:@("\n")];
if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString];
else [uri_list appendString:url.absoluteString];
} else if ([obj isKindOfClass:[NSString class]]) {
const char *text = [obj UTF8String];
_glfwInputDrop(window, "text/plain;charset=utf-8", text, strlen(text));
} else {
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Object is neither a URL nor a string");
}
}
// Heap-allocate drop data structure for chunked reading
// The application is responsible for freeing this via glfwFinishDrop
GLFWDropData* drop_data = calloc(1, sizeof(GLFWDropData));
if (!drop_data) {
_glfwInputError(GLFW_OUT_OF_MEMORY, "Cocoa: Failed to allocate drop data");
return NO;
}
if ([uri_list length] > 0) _glfwInputDrop(window, "text/uri-list", uri_list.UTF8String, strlen(uri_list.UTF8String));
// Transfer ownership of mimes array from window to drop object
drop_data->mime_types = window->ns.dragMimes;
drop_data->mime_count = window->ns.dragMimeCount;
drop_data->mime_array_size = window->ns.dragMimeArraySize;
// Clear window's references since drop object now owns the mimes
window->ns.dragMimes = NULL;
window->ns.dragMimeCount = 0;
window->ns.dragMimeArraySize = 0;
drop_data->current_mime = NULL;
drop_data->read_fd = -1;
drop_data->bytes_read = 0;
drop_data->platform_data = [pasteboard retain]; // Retain the pasteboard
drop_data->eof_reached = false;
drop_data->current_data = NULL;
drop_data->data_offset = 0;
_glfwInputDrop(window, drop_data);
// Note: drop_data is NOT freed here - application must call glfwFinishDrop
return YES;
}
@ -3872,3 +3875,128 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) {
}
}
const char**
_glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count) {
if (!drop || !count) return NULL;
*count = drop->mime_count;
return drop->mime_types;
}
ssize_t
_glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout UNUSED) {
if (!drop || !mime || !buffer || capacity == 0) return -EINVAL;
NSPasteboard* pasteboard = (__bridge NSPasteboard*)drop->platform_data;
if (!pasteboard) return -EINVAL;
// Check if the MIME type is available
bool mime_found = false;
for (int i = 0; i < drop->mime_count; i++) {
if (drop->mime_types[i] && strcmp(drop->mime_types[i], mime) == 0) {
mime_found = true;
break;
}
}
if (!mime_found) return -ENOENT;
// If switching MIME types, release previous data
if (drop->current_mime && strcmp(drop->current_mime, mime) != 0) {
if (drop->current_data) {
[(NSData*)drop->current_data release];
drop->current_data = NULL;
}
drop->data_offset = 0;
}
// If we need to fetch data for this MIME type
if (drop->current_data == NULL || drop->current_mime == NULL ||
strcmp(drop->current_mime, mime) != 0) {
NSData* data = nil;
// Handle special MIME types
if (strcmp(mime, "text/uri-list") == 0) {
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]] options:options];
if (urls && [urls count] > 0) {
NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096];
for (NSURL* url in urls) {
if ([uri_list length] > 0) [uri_list appendString:@"\n"];
if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString];
else [uri_list appendString:url.absoluteString];
}
data = [uri_list dataUsingEncoding:NSUTF8StringEncoding];
}
} else if (strcmp(mime, "text/plain") == 0 || strcmp(mime, "text/plain;charset=utf-8") == 0) {
NSArray* strings = [pasteboard readObjectsForClasses:@[[NSString class]] options:nil];
if (strings && [strings count] > 0) {
NSString* str = strings[0];
data = [str dataUsingEncoding:NSUTF8StringEncoding];
}
} else {
// Try to read data for other MIME types using UTI
NSString* uti = mime_to_uti(mime);
if (uti) {
NSPasteboardType pbType = [pasteboard availableTypeFromArray:@[uti]];
if (pbType) {
data = [pasteboard dataForType:pbType];
}
}
}
if (!data) return -ENOENT;
drop->current_data = [data retain];
drop->current_mime = mime;
drop->data_offset = 0;
}
// Read data from buffer
NSData* data = (NSData*)drop->current_data;
NSUInteger dataLength = [data length];
if (drop->data_offset >= dataLength) {
return 0; // EOF
}
NSUInteger remaining = dataLength - drop->data_offset;
NSUInteger to_read = (remaining < capacity) ? remaining : capacity;
[data getBytes:buffer range:NSMakeRange(drop->data_offset, to_read)];
drop->data_offset += to_read;
return (ssize_t)to_read;
}
void
_glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUSED, bool success UNUSED) {
if (!drop) return;
// Release the retained current data
if (drop->current_data) {
[(NSData*)drop->current_data release];
drop->current_data = NULL;
}
// Release the retained pasteboard
// Note: Cocoa drag operations don't have a way to report back to the source
// like X11's XdndFinished, so we just clean up our resources
if (drop->platform_data) {
[(NSPasteboard*)drop->platform_data release];
drop->platform_data = NULL;
}
// Free the mime types array (owned by drop object)
if (drop->mime_types) {
for (int i = 0; i < drop->mime_array_size; i++) {
if (drop->mime_types[i])
free((char*)drop->mime_types[i]);
}
free(drop->mime_types);
drop->mime_types = NULL;
}
// Free the heap-allocated drop data structure
free(drop);
}

136
glfw/glfw3.h vendored
View file

@ -109,6 +109,16 @@ extern "C" {
#include <stdbool.h>
/* Include for ssize_t on POSIX systems. On Windows we define it if needed. */
#if !defined(_WIN32)
#include <sys/types.h>
#else
#if !defined(SSIZE_T_DEFINED)
typedef intptr_t ssize_t;
#define SSIZE_T_DEFINED
#endif
#endif
#if defined(GLFW_INCLUDE_VULKAN)
#include <vulkan/vulkan.h>
#endif /* Vulkan header */
@ -1267,6 +1277,22 @@ typedef struct GLFWwindow GLFWwindow;
*/
typedef struct GLFWcursor GLFWcursor;
/*! @brief Opaque drop data object.
*
* Opaque drop data object representing data from a drag and drop operation.
* This object is passed to the drop callback and can be used to query
* available MIME types and read the dropped data in chunks.
*
* @see @ref path_drop
* @see @ref glfwGetDropMimeTypes
* @see @ref glfwReadDropData
*
* @since Added in version 4.0.
*
* @ingroup input
*/
typedef struct GLFWDropData GLFWDropData;
typedef enum {
GLFW_RELEASE = 0,
GLFW_PRESS = 1,
@ -1761,25 +1787,30 @@ typedef void (* GLFWkeyboardfun)(GLFWwindow*, GLFWkeyevent*);
* This is the function pointer type for drop callbacks. A drop
* callback function has the following signature:
* @code
* void function_name(GLFWwindow* window, const char* mime, const char* data, size_t sz)
* void function_name(GLFWwindow* window, GLFWDropData* drop)
* @endcode
*
* @param[in] window The window that received the event.
* @param[in] mime The UTF-8 encoded drop mime-type
* @param[in] data The dropped data.
* @param[in] sz The size of the dropped data
* @param[in] drop A heap-allocated opaque pointer representing the dropped data. Use
* @ref glfwGetDropMimeTypes to get available MIME types and
* @ref glfwReadDropData to read the data in chunks.
*
* @pointer_lifetime The data is valid until the
* callback function returns.
* @note The drop object is heap-allocated and remains valid until the
* application calls @ref glfwFinishDrop to free it. The application is
* responsible for calling glfwFinishDrop when it has finished reading
* the dropped data, even if reading fails or is not needed.
*
* @sa @ref path_drop
* @sa @ref glfwSetDropCallback
* @sa @ref glfwGetDropMimeTypes
* @sa @ref glfwReadDropData
* @sa @ref glfwFinishDrop
*
* @since Added in version 3.1.
* @since Changed in version 4.0 to receive opaque drop data pointer.
*
* @ingroup input
*/
typedef void (* GLFWdropfun)(GLFWwindow*, const char *, const char*, size_t);
typedef void (* GLFWdropfun)(GLFWwindow*, GLFWDropData*);
/*! @brief Drag event types.
*
@ -5105,6 +5136,95 @@ GLFWAPI int glfwStartDrag(GLFWwindow* window, const GLFWdragitem* items, int ite
*/
GLFWAPI void glfwUpdateDragState(GLFWwindow* window);
/*! @brief Gets the list of available MIME types from drop data.
*
* This function returns an array of MIME type strings available from
* the dropped data. The array and strings are owned by GLFW and should
* not be freed by the application. They are only valid until the drop
* callback returns.
*
* @param[in] drop The drop data object from the drop callback.
* @param[out] count The number of MIME types in the returned array.
* @return An array of MIME type strings, or `NULL` if the drop data is invalid.
*
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED.
*
* @thread_safety This function must only be called from the main thread.
*
* @sa @ref path_drop
* @sa @ref glfwSetDropCallback
* @sa @ref glfwReadDropData
*
* @since Added in version 4.0.
*
* @ingroup input
*/
GLFWAPI const char** glfwGetDropMimeTypes(GLFWDropData* drop, int* count);
/*! @brief Reads a chunk of dropped data for a specified MIME type.
*
* This function reads a chunk of the dropped data of the specified MIME type
* into the provided buffer. Repeated calls read subsequent chunks of data.
*
* If this function is called for a different MIME type than the previous call,
* resources associated with the previous MIME type are automatically freed.
*
* @param[in] drop The drop data object from the drop callback.
* @param[in] mime The MIME type to read data for.
* @param[out] buffer The buffer to read data into.
* @param[in] capacity The size of the buffer in bytes.
* @param[in] timeout The maximum time to wait for data, as a monotonic time value.
* Use 0 for non-blocking, or a positive value for the timeout duration.
* @return The number of bytes written to the buffer on success. Returns 0 when
* all data has been read (end of data). Returns a negative value on error:
* `-ENOENT` if the MIME type is not available, `-EIO` for an I/O error,
* `-ETIME` if the timeout expires before data is available.
*
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED.
*
* @remark The returned byte count may be less than the buffer capacity even
* when more data is available.
*
* @thread_safety This function must only be called from the main thread.
*
* @sa @ref path_drop
* @sa @ref glfwSetDropCallback
* @sa @ref glfwGetDropMimeTypes
*
* @since Added in version 4.0.
*
* @ingroup input
*/
GLFWAPI ssize_t glfwReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout);
/*! @brief Finishes a drop operation and frees associated resources.
*
* This function finishes the drop operation, informs the drag source of the
* result, and frees the heap-allocated drop data object. After calling this
* function, the drop data object must not be used anymore.
*
* The application MUST call this function when it has finished reading the
* dropped data. Failure to do so will result in a memory leak and may cause
* the drag source to hang waiting for the drop operation to complete.
*
* @param[in] drop The drop data object to finish and free.
* @param[in] operation The type of operation that was performed (copy, move, etc).
* @param[in] success Whether the drop operation was successful.
*
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED.
*
* @thread_safety This function must only be called from the main thread.
*
* @sa @ref path_drop
* @sa @ref glfwSetDropCallback
* @sa @ref glfwReadDropData
*
* @since Added in version 4.0.
*
* @ingroup input
*/
GLFWAPI void glfwFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation, bool success);
/*! @brief Returns whether the specified joystick is present.
*
* This function returns whether the specified joystick is present.

33
glfw/input.c vendored
View file

@ -31,6 +31,7 @@
#include "../kitty/monotonic.h"
#include <assert.h>
#include <errno.h>
#include <float.h>
#include <math.h>
#include <stdlib.h>
@ -403,10 +404,10 @@ void _glfwInputCursorEnter(_GLFWwindow* window, bool entered)
// Notifies shared code of files or directories dropped on a window
//
void _glfwInputDrop(_GLFWwindow* window, const char *mime, const char *text, size_t sz)
void _glfwInputDrop(_GLFWwindow* window, GLFWDropData* drop)
{
if (window->callbacks.drop)
window->callbacks.drop((GLFWwindow*) window, mime, text, sz);
window->callbacks.drop((GLFWwindow*) window, drop);
}
// Notifies shared code of a drag event
@ -1149,6 +1150,34 @@ GLFWAPI void glfwUpdateDragState(GLFWwindow* handle)
_glfwPlatformUpdateDragState(window);
}
GLFWAPI const char** glfwGetDropMimeTypes(GLFWDropData* drop, int* count)
{
assert(drop != NULL);
assert(count != NULL);
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return _glfwPlatformGetDropMimeTypes(drop, count);
}
GLFWAPI ssize_t glfwReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout)
{
assert(drop != NULL);
assert(mime != NULL);
assert(buffer != NULL);
assert(capacity > 0);
_GLFW_REQUIRE_INIT_OR_RETURN(-1);
return _glfwPlatformReadDropData(drop, mime, buffer, capacity, timeout);
}
GLFWAPI void glfwFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation, bool success)
{
assert(drop != NULL);
_GLFW_REQUIRE_INIT();
_glfwPlatformFinishDrop(drop, operation, success);
}
GLFWAPI int glfwJoystickPresent(int jid)
{
_GLFWjoystick* js;

28
glfw/internal.h vendored
View file

@ -85,6 +85,27 @@ typedef struct _GLFWjoystick _GLFWjoystick;
typedef struct _GLFWtls _GLFWtls;
typedef struct _GLFWmutex _GLFWmutex;
// Drop data structure for chunked reading of drag and drop data
struct GLFWDropData {
const char** mime_types; // Array of available MIME types (owned by drop object)
int mime_count; // Number of MIME types
int mime_array_size; // Original array size for proper cleanup
const char* current_mime; // Currently being read MIME type
int read_fd; // File descriptor for reading data (Wayland/X11)
size_t bytes_read; // Total bytes read so far for current mime
void* platform_data; // Platform-specific data (offer for Wayland, pasteboard for Cocoa)
bool eof_reached; // Whether EOF has been reached for current mime
// Platform-specific data fields
void* current_data; // NSData* (Cocoa) or unsigned char* from XGetWindowProperty (X11)
size_t data_offset; // Read offset in current data (Cocoa/X11)
// X11-specific fields
size_t x11_data_size; // Size of current X11 data
unsigned long x11_drop_target; // Window handle where the drop occurred (X11)
unsigned long x11_drop_time; // Time from the drop event (X11)
unsigned long x11_source; // Source window for XdndFinished (X11)
int x11_version; // Xdnd protocol version (X11)
};
typedef void (* _GLFWmakecontextcurrentfun)(_GLFWwindow*);
typedef void (* _GLFWswapbuffersfun)(_GLFWwindow*);
typedef void (* _GLFWswapintervalfun)(int);
@ -822,8 +843,13 @@ void _glfwInputScroll(_GLFWwindow* window, const GLFWScrollEvent *ev);
void _glfwInputMouseClick(_GLFWwindow* window, int button, int action, int mods);
void _glfwInputCursorPos(_GLFWwindow* window, double xpos, double ypos);
void _glfwInputCursorEnter(_GLFWwindow* window, bool entered);
void _glfwInputDrop(_GLFWwindow* window, const char *mime, const char *text, size_t sz);
void _glfwInputDrop(_GLFWwindow* window, GLFWDropData* drop);
int _glfwInputDragEvent(_GLFWwindow* window, int event, double xpos, double ypos, const char** mime_types, int* mime_count);
// Platform functions for drop data reading
const char** _glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count);
ssize_t _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout);
void _glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation, bool success);
void _glfwInputColorScheme(GLFWColorScheme, bool);
void _glfwPlatformInputColorScheme(GLFWColorScheme);
void _glfwInputJoystick(_GLFWjoystick* js, int event);

21
glfw/null_window.c vendored
View file

@ -30,6 +30,7 @@
#include "internal.h"
#include "../kitty/monotonic.h"
#include <errno.h>
#include <stdlib.h>
static void applySizeLimits(_GLFWwindow* window, int* width, int* height)
@ -544,6 +545,26 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window UNUSED)
// No-op for null platform
}
const char** _glfwPlatformGetDropMimeTypes(GLFWDropData* drop UNUSED, int* count)
{
if (count) *count = 0;
return NULL;
}
ssize_t _glfwPlatformReadDropData(GLFWDropData* drop UNUSED, const char* mime UNUSED,
void* buffer UNUSED, size_t capacity UNUSED,
monotonic_t timeout UNUSED)
{
return -ENOENT;
}
void _glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUSED, bool success UNUSED)
{
if (!drop) return;
// Free the heap-allocated drop data structure
free(drop);
}
void _glfwPlatformSetClipboardString(const char* string)
{
char* copy = _glfw_strdup(string);

222
glfw/wl_window.c vendored
View file

@ -39,6 +39,7 @@
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/mman.h>
#include <assert.h>
#include <unistd.h>
@ -2258,35 +2259,6 @@ read_offer(int data_pipe, GLFWclipboardwritedatafun write_data, void *object) {
}
typedef struct chunked_writer {
char *buf; size_t sz, cap;
} chunked_writer;
static bool
write_chunk(void *object, const char *data, size_t sz) {
chunked_writer *cw = object;
if (cw->cap < cw->sz + sz) {
cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz);
cw->buf = realloc(cw->buf, cw->cap * sizeof(cw->buf[0]));
}
memcpy(cw->buf + cw->sz, data, sz);
cw->sz += sz;
return true;
}
static char*
read_offer_string(int data_pipe, size_t *sz) {
chunked_writer cw = {0};
read_offer(data_pipe, write_chunk, &cw);
if (cw.buf) {
*sz = cw.sz;
return cw.buf;
}
*sz = 0;
return NULL;
}
static void
read_clipboard_data_offer(struct wl_data_offer *data_offer, const char *mime, GLFWclipboardwritedatafun write_data, void *object) {
int pipefd[2];
@ -2305,14 +2277,6 @@ read_primary_selection_offer(struct zwp_primary_selection_offer_v1 *primary_sele
read_offer(pipefd[0], write_data, object);
}
static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime, size_t *sz) {
int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL;
wl_data_offer_receive(data_offer, mime, pipefd[1]);
close(pipefd[1]);
return read_offer_string(pipefd[0], sz);
}
static void data_source_canceled(void *data UNUSED, struct wl_data_source *wl_data_source) {
if (_glfw.wl.dataSourceForClipboard == wl_data_source) {
_glfw.wl.dataSourceForClipboard = NULL;
@ -2560,27 +2524,45 @@ static void drag_leave(void *data UNUSED, struct wl_data_device *wl_data_device
static void drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) {
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP && _glfw.wl.dataOffers[i].mime_for_drop) {
size_t sz = 0;
char *d = read_data_offer(_glfw.wl.dataOffers[i].id, _glfw.wl.dataOffers[i].mime_for_drop, &sz);
if (d) {
// We dont do finish as this requires version 3 for wl_data_device_manager
// which then requires more work with calling set_actions for drag and drop to function
// wl_data_offer_finish(_glfw.wl.dataOffers[i].id);
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) {
_GLFWWaylandDataOffer *offer = &_glfw.wl.dataOffers[i];
_GLFWwindow* window = _glfw.windowListHead;
while (window)
{
if (window->wl.surface == _glfw.wl.dataOffers[i].surface) {
_glfwInputDrop(window, _glfw.wl.dataOffers[i].mime_for_drop, d, sz);
_GLFWwindow* window = _glfw.windowListHead;
while (window)
{
if (window->wl.surface == offer->surface) {
// Heap-allocate drop data structure for chunked reading
// The application is responsible for freeing this via glfwFinishDrop
GLFWDropData* drop_data = calloc(1, sizeof(GLFWDropData));
if (!drop_data) {
_glfwInputError(GLFW_OUT_OF_MEMORY, "Wayland: Failed to allocate drop data");
destroy_data_offer(offer); // Clean up the offer on allocation failure
break;
}
window = window->next;
}
// Transfer ownership of mimes array from offer to drop object
drop_data->mime_types = offer->mimes;
drop_data->mime_count = (int)offer->mimes_count;
drop_data->mime_array_size = (int)offer->mimes_count;
// Clear offer's references since drop object now owns the mimes
offer->mimes = NULL;
offer->mimes_count = 0;
free(d);
drop_data->current_mime = NULL;
drop_data->read_fd = -1;
drop_data->bytes_read = 0;
drop_data->platform_data = offer; // Store the offer for later use
drop_data->eof_reached = false;
_glfwInputDrop(window, drop_data);
// Note: drop_data is NOT freed here - application must call glfwFinishDrop
break;
}
window = window->next;
}
destroy_data_offer(&_glfw.wl.dataOffers[i]);
// Note: We no longer destroy the offer here as the drop_data holds a reference
// The offer will be destroyed when glfwFinishDrop is called
break;
}
}
@ -3198,3 +3180,137 @@ _glfwPlatformUpdateDragState(_GLFWwindow* window) {
}
}
const char**
_glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count) {
if (!drop || !count) return NULL;
*count = drop->mime_count;
return drop->mime_types;
}
ssize_t
_glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout) {
if (!drop || !mime || !buffer || capacity == 0) return -EINVAL;
_GLFWWaylandDataOffer *offer = (_GLFWWaylandDataOffer*)drop->platform_data;
if (!offer || !offer->id) return -EINVAL;
// Check if the MIME type is available
bool mime_found = false;
for (int i = 0; i < drop->mime_count; i++) {
if (drop->mime_types[i] && strcmp(drop->mime_types[i], mime) == 0) {
mime_found = true;
break;
}
}
if (!mime_found) return -ENOENT;
// If switching MIME types, close the previous file descriptor
if (drop->current_mime && strcmp(drop->current_mime, mime) != 0) {
if (drop->read_fd >= 0) {
close(drop->read_fd);
drop->read_fd = -1;
}
drop->bytes_read = 0;
drop->eof_reached = false;
}
// If we've reached EOF for this MIME type, return 0
if (drop->eof_reached && drop->current_mime && strcmp(drop->current_mime, mime) == 0) {
return 0;
}
// Open a new pipe if we don't have one for this MIME type
if (drop->read_fd < 0 || drop->current_mime == NULL || strcmp(drop->current_mime, mime) != 0) {
int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC | O_NONBLOCK) != 0) return -EIO;
wl_data_offer_receive(offer->id, mime, pipefd[1]);
close(pipefd[1]);
// Round trip to ensure the compositor processes the receive request
wl_display_roundtrip(_glfw.wl.display);
drop->read_fd = pipefd[0];
// mime points to drop->mime_types entry, valid for duration of drop callback
drop->current_mime = mime;
drop->bytes_read = 0;
drop->eof_reached = false;
}
// Wait for data with timeout using poll
monotonic_t start = monotonic();
while (true) {
struct pollfd pfd = { .fd = drop->read_fd, .events = POLLIN };
monotonic_t remaining = timeout - (monotonic() - start);
if (timeout > 0 && remaining <= 0) return -ETIME;
int poll_timeout_ms = (timeout <= 0) ? 0 : monotonic_t_to_ms(remaining);
if (poll_timeout_ms < 1 && timeout > 0) poll_timeout_ms = 1;
int ret = poll(&pfd, 1, poll_timeout_ms);
if (ret < 0) {
if (errno == EINTR) continue;
return -EIO;
}
if (ret == 0) {
if (timeout <= 0) {
// Non-blocking mode: no data available yet, try reading anyway
break;
}
return -ETIME;
}
break; // Data available
}
// Read data from the pipe
ssize_t bytes_read = read(drop->read_fd, buffer, capacity);
if (bytes_read < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return -ETIME; // No data available yet
}
return -EIO;
}
if (bytes_read == 0) {
// EOF reached
drop->eof_reached = true;
return 0;
}
drop->bytes_read += bytes_read;
return bytes_read;
}
void
_glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUSED, bool success UNUSED) {
if (!drop) return;
// Close any open file descriptor
if (drop->read_fd >= 0) {
close(drop->read_fd);
drop->read_fd = -1;
}
// Destroy the associated data offer
// Note: Wayland doesn't have a way to report the operation type or success back to the source
// in the same way as X11, as the source is notified through other means
_GLFWWaylandDataOffer* offer = (_GLFWWaylandDataOffer*)drop->platform_data;
if (offer) {
destroy_data_offer(offer);
}
// Free the mime types array (owned by drop object)
if (drop->mime_types) {
for (int i = 0; i < drop->mime_array_size; i++) {
if (drop->mime_types[i])
free((char*)drop->mime_types[i]);
}
free(drop->mime_types);
drop->mime_types = NULL;
}
// Free the heap-allocated drop data structure
free(drop);
}

276
glfw/x11_window.c vendored
View file

@ -1923,23 +1923,60 @@ static void processEvent(XEvent *event)
else if (event->xclient.message_type == _glfw.x11.XdndDrop)
{
// The drag operation has finished by dropping on the window
Time time = CurrentTime;
if (_glfw.x11.xdnd.version > _GLFW_XDND_VERSION)
return;
if (_glfw.x11.xdnd.format_priority > 0)
if (_glfw.x11.xdnd.drag_accepted && _glfw.x11.xdnd.mimes_count > 0)
{
if (_glfw.x11.xdnd.version >= 1)
time = event->xclient.data.l[2];
// Heap-allocate drop data structure for chunked reading
// The application is responsible for freeing this via glfwFinishDrop
GLFWDropData* drop_data = calloc(1, sizeof(GLFWDropData));
if (!drop_data) {
_glfwInputError(GLFW_OUT_OF_MEMORY, "X11: Failed to allocate drop data");
// Send XdndFinished to avoid leaving drag source hanging
if (_glfw.x11.xdnd.version >= 2)
{
XEvent reply = { ClientMessage };
reply.xclient.window = _glfw.x11.xdnd.source;
reply.xclient.message_type = _glfw.x11.XdndFinished;
reply.xclient.format = 32;
reply.xclient.data.l[0] = window->x11.handle;
reply.xclient.data.l[1] = 0; // Failure
reply.xclient.data.l[2] = None;
// Request the chosen format from the source window
XConvertSelection(_glfw.x11.display,
_glfw.x11.XdndSelection,
XInternAtom(_glfw.x11.display, _glfw.x11.xdnd.format, 0),
_glfw.x11.XdndSelection,
window->x11.handle,
time);
XSendEvent(_glfw.x11.display, _glfw.x11.xdnd.source,
False, NoEventMask, &reply);
XFlush(_glfw.x11.display);
}
return;
}
// Transfer ownership of mimes array from global to drop object
drop_data->mime_types = (const char**)_glfw.x11.xdnd.mimes;
drop_data->mime_count = _glfw.x11.xdnd.mimes_count;
drop_data->mime_array_size = _glfw.x11.xdnd.mimes_array_size;
// Clear global references since drop object now owns the mimes
_glfw.x11.xdnd.mimes = NULL;
_glfw.x11.xdnd.mimes_count = 0;
_glfw.x11.xdnd.mimes_array_size = 0;
drop_data->current_mime = NULL;
drop_data->read_fd = -1;
drop_data->bytes_read = 0;
drop_data->platform_data = NULL;
drop_data->eof_reached = false;
drop_data->current_data = NULL;
drop_data->data_offset = 0;
drop_data->x11_data_size = 0;
// Store X11-specific drop state in the drop object
drop_data->x11_drop_target = window->x11.handle;
drop_data->x11_drop_time = (_glfw.x11.xdnd.version >= 1) ?
(unsigned long)event->xclient.data.l[2] : CurrentTime;
drop_data->x11_source = _glfw.x11.xdnd.source;
drop_data->x11_version = _glfw.x11.xdnd.version;
_glfwInputDrop(window, drop_data);
// Note: drop_data is NOT freed here - application must call glfwFinishDrop
}
else if (_glfw.x11.xdnd.version >= 2)
{
@ -2046,40 +2083,9 @@ static void processEvent(XEvent *event)
case SelectionNotify:
{
if (event->xselection.property == _glfw.x11.XdndSelection)
{
// The converted data from the drag operation has arrived
char* data;
const unsigned long result =
_glfwGetWindowPropertyX11(event->xselection.requestor,
event->xselection.property,
event->xselection.target,
(unsigned char**) &data);
if (result)
{
_glfwInputDrop(window, _glfw.x11.xdnd.format, data, result);
}
if (data)
XFree(data);
if (_glfw.x11.xdnd.version >= 2)
{
XEvent reply = { ClientMessage };
reply.xclient.window = _glfw.x11.xdnd.source;
reply.xclient.message_type = _glfw.x11.XdndFinished;
reply.xclient.format = 32;
reply.xclient.data.l[0] = window->x11.handle;
reply.xclient.data.l[1] = result;
reply.xclient.data.l[2] = _glfw.x11.XdndActionCopy;
XSendEvent(_glfw.x11.display, _glfw.x11.xdnd.source,
False, NoEventMask, &reply);
XFlush(_glfw.x11.display);
}
}
// SelectionNotify for XdndSelection is now handled synchronously
// in _glfwPlatformReadDropData for chunked reading
// This case is left here for clipboard operations
return;
}
@ -3868,3 +3874,181 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) {
XFlush(_glfw.x11.display);
}
const char**
_glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count) {
if (!drop || !count) return NULL;
*count = drop->mime_count;
return drop->mime_types;
}
ssize_t
_glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout) {
if (!drop || !mime || !buffer || capacity == 0) return -EINVAL;
// Check if the MIME type is available
bool mime_found = false;
for (int i = 0; i < drop->mime_count; i++) {
if (drop->mime_types[i] && strcmp(drop->mime_types[i], mime) == 0) {
mime_found = true;
break;
}
}
if (!mime_found) return -ENOENT;
// If switching MIME types, free the previous data
if (drop->current_mime && strcmp(drop->current_mime, mime) != 0) {
if (drop->current_data) {
XFree(drop->current_data);
drop->current_data = NULL;
}
drop->x11_data_size = 0;
drop->data_offset = 0;
drop->current_mime = NULL;
}
// If we need to fetch data for this MIME type
if (drop->current_data == NULL || drop->current_mime == NULL ||
strcmp(drop->current_mime, mime) != 0) {
Atom target_atom = XInternAtom(_glfw.x11.display, mime, False);
// Request the data via XConvertSelection
XConvertSelection(_glfw.x11.display,
_glfw.x11.XdndSelection,
target_atom,
_glfw.x11.XdndSelection,
(Window)drop->x11_drop_target,
(Time)drop->x11_drop_time);
XFlush(_glfw.x11.display);
// Wait for SelectionNotify event with timeout
monotonic_t start = monotonic();
XEvent event;
bool got_data = false;
while (true) {
monotonic_t elapsed = monotonic() - start;
if (timeout > 0 && elapsed >= timeout) return -ETIME;
int timeout_ms = (timeout > 0) ? monotonic_t_to_ms(timeout - elapsed) : 100;
if (timeout_ms < 1) timeout_ms = 1;
// Use poll to wait for X11 events
struct pollfd pfd = { .fd = ConnectionNumber(_glfw.x11.display), .events = POLLIN };
int ret = poll(&pfd, 1, timeout_ms);
if (ret < 0) {
if (errno == EINTR) continue;
return -EIO;
}
// Process pending events
while (XPending(_glfw.x11.display)) {
XNextEvent(_glfw.x11.display, &event);
if (event.type == SelectionNotify &&
event.xselection.selection == _glfw.x11.XdndSelection &&
event.xselection.property == _glfw.x11.XdndSelection) {
// Get the data
unsigned char* data = NULL;
unsigned long size = _glfwGetWindowPropertyX11(
event.xselection.requestor,
event.xselection.property,
event.xselection.target,
&data);
if (size > 0 && data) {
drop->current_data = data;
drop->x11_data_size = size;
drop->data_offset = 0;
// mime points to drop->mime_types entry, valid for duration of drop callback
drop->current_mime = mime;
got_data = true;
} else {
if (data) XFree(data);
return -EIO;
}
break;
}
}
if (got_data) break;
if (timeout == 0) {
// Non-blocking mode - no data yet
return -ETIME;
}
}
}
// Read data from buffer
if (drop->data_offset >= drop->x11_data_size) {
return 0; // EOF
}
size_t remaining = drop->x11_data_size - drop->data_offset;
size_t to_read = (remaining < capacity) ? remaining : capacity;
memcpy(buffer, (unsigned char*)drop->current_data + drop->data_offset, to_read);
drop->data_offset += to_read;
return (ssize_t)to_read;
}
void
_glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation, bool success) {
if (!drop) return;
// Free current data if any
if (drop->current_data) {
XFree(drop->current_data);
drop->current_data = NULL;
}
// Send XdndFinished
if (drop->x11_version >= 2)
{
Atom action_atom = None;
if (success) {
switch (operation) {
case GLFW_DRAG_OPERATION_MOVE:
action_atom = _glfw.x11.XdndActionMove;
break;
case GLFW_DRAG_OPERATION_COPY:
action_atom = _glfw.x11.XdndActionCopy;
break;
case GLFW_DRAG_OPERATION_GENERIC:
default:
action_atom = _glfw.x11.XdndActionCopy;
break;
}
}
XEvent reply = { ClientMessage };
reply.xclient.window = (Window)drop->x11_source;
reply.xclient.message_type = _glfw.x11.XdndFinished;
reply.xclient.format = 32;
reply.xclient.data.l[0] = (long)drop->x11_drop_target;
reply.xclient.data.l[1] = success ? 1 : 0;
reply.xclient.data.l[2] = action_atom;
XSendEvent(_glfw.x11.display, (Window)drop->x11_source,
False, NoEventMask, &reply);
XFlush(_glfw.x11.display);
}
// Free the mime types array (owned by drop object, allocated by X11 XGetAtomNames)
if (drop->mime_types) {
for (int i = 0; i < drop->mime_array_size; i++) {
if (drop->mime_types[i])
XFree((char*)drop->mime_types[i]);
}
free(drop->mime_types);
drop->mime_types = NULL;
}
// Free the heap-allocated drop data structure
free(drop);
}