diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 57261fb21..976abc6d7 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -33,6 +33,7 @@ #include #import #import +#include #include #include #include @@ -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); +} + diff --git a/glfw/glfw3.h b/glfw/glfw3.h index 8ca6c2a4f..775aa91bd 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -109,6 +109,16 @@ extern "C" { #include +/* Include for ssize_t on POSIX systems. On Windows we define it if needed. */ +#if !defined(_WIN32) + #include +#else + #if !defined(SSIZE_T_DEFINED) + typedef intptr_t ssize_t; + #define SSIZE_T_DEFINED + #endif +#endif + #if defined(GLFW_INCLUDE_VULKAN) #include #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. diff --git a/glfw/input.c b/glfw/input.c index e82a20a41..33c3191d8 100644 --- a/glfw/input.c +++ b/glfw/input.c @@ -31,6 +31,7 @@ #include "../kitty/monotonic.h" #include +#include #include #include #include @@ -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; diff --git a/glfw/internal.h b/glfw/internal.h index eb5761a15..d7a8b0be4 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -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); diff --git a/glfw/null_window.c b/glfw/null_window.c index c4d04d477..193829f81 100644 --- a/glfw/null_window.c +++ b/glfw/null_window.c @@ -30,6 +30,7 @@ #include "internal.h" #include "../kitty/monotonic.h" +#include #include 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); diff --git a/glfw/wl_window.c b/glfw/wl_window.c index c4b1344c1..0cf71b41b 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -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); +} + diff --git a/glfw/x11_window.c b/glfw/x11_window.c index fa136aaac..01f2e9813 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -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); +} +