diff --git a/glfw/cocoa_platform.h b/glfw/cocoa_platform.h index da19415ff..9bc765cd6 100644 --- a/glfw/cocoa_platform.h +++ b/glfw/cocoa_platform.h @@ -169,12 +169,16 @@ typedef struct _GLFWwindowNS GLFWcocoarenderframefun resizeCallback; // Current drag operation type for NSDraggingSource - GLFWDragOperationType dragOperationType; + int dragOperations; // Bitfield of GLFWDragOperationType // Cached MIME types from drag enter (for move events) const char** dragMimes; int dragMimeCount; // Current count of MIME types (may be reduced by callback) int dragMimeArraySize; // Original array size for proper cleanup + + // Drag source state + char** sourceMimes; // Array of MIME type strings for drag source + int sourceMimeCount; // Number of source MIME types } _GLFWwindowNS; // Cocoa-specific global data diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 03641529d..33a9dda5c 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -759,6 +759,134 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; } @end // }}} +// File Promise Provider Delegate for async drag data {{{ + +// Structure to hold async drag state +typedef struct { + NSURL* destinationURL; // URL to write to + void (^completionHandler)(NSError*); // Completion block to call + NSFileHandle* fileHandle; // File handle for writing + bool finished; // Whether writing is complete + int errorCode; // Error code if any +} GLFWFilePromiseState; + +@interface GLFWFilePromiseProviderDelegate : NSObject +{ + GLFWid windowId; + char* mimeType; // MIME type for this provider +} +- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime; +@end + +@implementation GLFWFilePromiseProviderDelegate + +- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime { + self = [super init]; + if (self) { + windowId = initWindow ? initWindow->id : 0; + mimeType = _glfw_strdup(mime); + } + return self; +} + +- (void)dealloc { + free(mimeType); + [super dealloc]; +} + +- (NSString*)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString*)fileType { + (void)filePromiseProvider; + (void)fileType; + // Generate a unique filename based on the MIME type + NSString* extension = @"data"; + if (mimeType) { + NSString* mt = @(mimeType); + if ([mt hasPrefix:@"text/"]) extension = @"txt"; + else if ([mt isEqualToString:@"image/png"]) extension = @"png"; + else if ([mt isEqualToString:@"image/jpeg"]) extension = @"jpg"; + else if ([mt isEqualToString:@"application/json"]) extension = @"json"; + } + return [NSString stringWithFormat:@"glfw-drag-%@.%@", [[NSUUID UUID] UUIDString], extension]; +} + +- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider + writePromiseToURL:(NSURL*)url + completionHandler:(void (^)(NSError*))completionHandler { + (void)filePromiseProvider; + + // Get the window from the ID + _GLFWwindow* window = _glfwWindowForId(windowId); + if (!window) { + completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:nil]); + return; + } + + // Create the file + NSError* error = nil; + if (![[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]) { + error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EIO userInfo:nil]; + completionHandler(error); + return; + } + + NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:&error]; + if (!fileHandle) { + completionHandler(error); + return; + } + + // Create the drag source data with file promise state + GLFWDragSourceData* source_data = calloc(1, sizeof(GLFWDragSourceData)); + if (!source_data) { + [fileHandle closeFile]; + completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]); + return; + } + + // Create the file promise state + GLFWFilePromiseState* state = calloc(1, sizeof(GLFWFilePromiseState)); + if (!state) { + free(source_data); + [fileHandle closeFile]; + completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]); + return; + } + + state->destinationURL = [url retain]; + state->completionHandler = [completionHandler copy]; + state->fileHandle = [fileHandle retain]; + state->finished = false; + state->errorCode = 0; + + source_data->window_id = windowId; + source_data->mime_type = _glfw_strdup(mimeType); + source_data->write_fd = -1; + source_data->finished = false; + source_data->error_code = 0; + source_data->platform_data = state; + + if (!source_data->mime_type) { + [state->fileHandle closeFile]; + [state->fileHandle release]; + [state->destinationURL release]; + Block_release(state->completionHandler); + free(state); + free(source_data); + completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]); + return; + } + + // Notify the application via callback - this will trigger glfwSendDragData calls + _glfwInputDragSourceRequest(window, mimeType, source_data); + + // Note: The completion handler will be called from glfwSendDragData when finished + // If the application didn't finish (sync callback didn't complete), we need to handle it + // The platform_data still holds the state for async completion +} + +@end +// }}} + // Content view class for the GLFW window {{{ @interface GLFWContentView : NSView @@ -1539,15 +1667,15 @@ static void freeFilteredDragMimes(_GLFWwindow* window, int old_count, int new_co { (void)session; (void)context; - // Return the operation based on the stored drag operation type - switch (window->ns.dragOperationType) { - case GLFW_DRAG_OPERATION_COPY: - return NSDragOperationCopy; - case GLFW_DRAG_OPERATION_MOVE: - return NSDragOperationMove; - case GLFW_DRAG_OPERATION_GENERIC: - return NSDragOperationGeneric; - } + // Return the operation based on the stored drag operations bitfield + NSDragOperation ops = 0; + if (window->ns.dragOperations & GLFW_DRAG_OPERATION_COPY) + ops |= NSDragOperationCopy; + if (window->ns.dragOperations & GLFW_DRAG_OPERATION_MOVE) + ops |= NSDragOperationMove; + if (window->ns.dragOperations & GLFW_DRAG_OPERATION_GENERIC) + ops |= NSDragOperationGeneric; + return ops ? ops : NSDragOperationCopy; } - (void)draggingSession:(NSDraggingSession *)session @@ -1557,7 +1685,15 @@ static void freeFilteredDragMimes(_GLFWwindow* window, int old_count, int new_co (void)session; (void)screenPoint; (void)operation; - // Drag session ended + // Notify the application that the drag source is closed + _glfwInputDragSourceRequest(window, NULL, NULL); + // Clean up source MIME types + for (int i = 0; i < window->ns.sourceMimeCount; i++) { + free(window->ns.sourceMimes[i]); + } + free(window->ns.sourceMimes); + window->ns.sourceMimes = NULL; + window->ns.sourceMimeCount = 0; } - (BOOL)hasMarkedText @@ -3771,101 +3907,231 @@ void _glfwCocoaPostEmptyEvent(void) { [NSApp postEvent:event atStart:YES]; } +void _glfwPlatformCancelDrag(_GLFWwindow* window) { + // Clean up source MIME types + for (int i = 0; i < window->ns.sourceMimeCount; i++) { + free(window->ns.sourceMimes[i]); + } + free(window->ns.sourceMimes); + window->ns.sourceMimes = NULL; + window->ns.sourceMimeCount = 0; + // Notify the application that the drag source is closed + _glfwInputDragSourceRequest(window, NULL, NULL); +} + int _glfwPlatformStartDrag(_GLFWwindow* window, - const GLFWdragitem* items, - int item_count, + const char* const* mime_types, + int mime_count, const GLFWimage* thumbnail, - GLFWDragOperationType operation) { - // Store the operation type for the dragging source callback - window->ns.dragOperationType = operation; + int operations) { + // Cancel any existing drag operation + _glfwPlatformCancelDrag(window); + + // Store the operations for the dragging source callback + window->ns.dragOperations = operations; @autoreleasepool { - // Create pasteboard items for each drag item - NSMutableArray* pasteboardItems = [[NSMutableArray alloc] init]; - - for (int i = 0; i < item_count; i++) { - NSPasteboardItem* item = [[NSPasteboardItem alloc] init]; - - // Convert MIME type to UTI using the existing helper function - NSString* utiString = mime_to_uti(items[i].mime_type); - NSData* data = [NSData dataWithBytes:items[i].data length:items[i].data_size]; - - [item setData:data forType:utiString]; - [pasteboardItems addObject:item]; + // Store MIME types for later lookup + window->ns.sourceMimes = calloc(mime_count, sizeof(char*)); + window->ns.sourceMimeCount = mime_count; + if (!window->ns.sourceMimes) { + return ENOMEM; } - - // Create the dragging item - NSDraggingItem* dragItem = nil; - - if (thumbnail && thumbnail->pixels) { - // Create NSImage from thumbnail - NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] - initWithBitmapDataPlanes:NULL - pixelsWide:thumbnail->width - pixelsHigh:thumbnail->height - bitsPerSample:8 - samplesPerPixel:4 - hasAlpha:YES - isPlanar:NO - colorSpaceName:NSDeviceRGBColorSpace - bytesPerRow:thumbnail->width * 4 - bitsPerPixel:32]; - - if (imageRep) { - memcpy([imageRep bitmapData], thumbnail->pixels, - thumbnail->width * thumbnail->height * 4); - - NSImage* image = [[NSImage alloc] initWithSize: - NSMakeSize(thumbnail->width, thumbnail->height)]; - [image addRepresentation:imageRep]; - - dragItem = [[NSDraggingItem alloc] - initWithPasteboardWriter:pasteboardItems.firstObject]; - [dragItem setDraggingFrame:NSMakeRect(0, 0, thumbnail->width, thumbnail->height) - contents:image]; + for (int i = 0; i < mime_count; i++) { + window->ns.sourceMimes[i] = _glfw_strdup(mime_types[i]); + if (!window->ns.sourceMimes[i]) { + _glfwPlatformCancelDrag(window); + return ENOMEM; } } - if (!dragItem && pasteboardItems.count > 0) { - dragItem = [[NSDraggingItem alloc] - initWithPasteboardWriter:pasteboardItems.firstObject]; - [dragItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil]; - } + // Create dragging items array - one NSFilePromiseProvider per MIME type + NSMutableArray* dragItems = [[NSMutableArray alloc] init]; - if (dragItem) { - // Start the drag session - try current event first, then create a synthetic one - NSEvent* event = [NSApp currentEvent]; - if (!event || ([event type] != NSEventTypeLeftMouseDown && - [event type] != NSEventTypeLeftMouseDragged)) { - // Create a synthetic left mouse down event using stored cursor position - // Convert window coordinates to screen coordinates - NSRect contentRect = [window->ns.view frame]; - NSPoint windowPos = NSMakePoint(window->virtualCursorPosX, - contentRect.size.height - window->virtualCursorPosY); + for (int i = 0; i < mime_count; i++) { + NSString* utiString = mime_to_uti(mime_types[i]); - event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown - location:windowPos - modifierFlags:0 - timestamp:[[NSProcessInfo processInfo] systemUptime] - windowNumber:[window->ns.object windowNumber] - context:nil - eventNumber:0 - clickCount:1 - pressure:1.0]; + // Create file promise provider with our delegate + GLFWFilePromiseProviderDelegate* delegate = [[GLFWFilePromiseProviderDelegate alloc] + initWithWindow:window mimeType:mime_types[i]]; + NSFilePromiseProvider* provider = [[NSFilePromiseProvider alloc] + initWithFileType:utiString delegate:delegate]; + + // Store the delegate in the provider's user info so it's retained + provider.userInfo = delegate; + + // Create the dragging item + NSDraggingItem* dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:provider]; + + if (i == 0 && thumbnail && thumbnail->pixels) { + // Create NSImage from thumbnail for the first item + NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:thumbnail->width + pixelsHigh:thumbnail->height + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:thumbnail->width * 4 + bitsPerPixel:32]; + + if (imageRep) { + memcpy([imageRep bitmapData], thumbnail->pixels, + thumbnail->width * thumbnail->height * 4); + + NSImage* image = [[NSImage alloc] initWithSize: + NSMakeSize(thumbnail->width, thumbnail->height)]; + [image addRepresentation:imageRep]; + + [dragItem setDraggingFrame:NSMakeRect(0, 0, thumbnail->width, thumbnail->height) + contents:image]; + } + } else { + [dragItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil]; } - if (event) { - [window->ns.view beginDraggingSessionWithItems:@[dragItem] - event:event - source:window->ns.view]; - return true; - } + [dragItems addObject:dragItem]; } - return false; + if (dragItems.count == 0) { + _glfwPlatformCancelDrag(window); + return EINVAL; + } + + // Start the drag session - try current event first, then create a synthetic one + NSEvent* event = [NSApp currentEvent]; + if (!event || ([event type] != NSEventTypeLeftMouseDown && + [event type] != NSEventTypeLeftMouseDragged)) { + // Create a synthetic left mouse down event using stored cursor position + // Convert window coordinates to screen coordinates + NSRect contentRect = [window->ns.view frame]; + NSPoint windowPos = NSMakePoint(window->virtualCursorPosX, + contentRect.size.height - window->virtualCursorPosY); + + event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown + location:windowPos + modifierFlags:0 + timestamp:[[NSProcessInfo processInfo] systemUptime] + windowNumber:[window->ns.object windowNumber] + context:nil + eventNumber:0 + clickCount:1 + pressure:1.0]; + } + + if (event) { + [window->ns.view beginDraggingSessionWithItems:dragItems + event:event + source:window->ns.view]; + return 0; + } + + return EIO; } } +ssize_t _glfwPlatformSendDragData(GLFWDragSourceData* source_data, const void* data, size_t size) { + if (!source_data || source_data->finished) return -EINVAL; + if (!source_data->platform_data) return -EINVAL; + + GLFWFilePromiseState* state = (GLFWFilePromiseState*)source_data->platform_data; + + // End of data: NULL data pointer and size zero + if (!data && size == 0) { + source_data->finished = true; + + // Close the file handle + @try { + [state->fileHandle closeFile]; + } @catch (NSException* e) { + (void)e; + } + + // Call the completion handler with success + if (state->completionHandler) { + state->completionHandler(nil); + Block_release(state->completionHandler); + state->completionHandler = nil; + } + + // Clean up + [state->fileHandle release]; + state->fileHandle = nil; + [state->destinationURL release]; + state->destinationURL = nil; + + return 0; + } + + // Error from application: NULL data pointer and size is error code + if (!data && size > 0) { + source_data->finished = true; + source_data->error_code = (int)size; + state->errorCode = (int)size; + + // Close the file handle + @try { + [state->fileHandle closeFile]; + } @catch (NSException* e) { + (void)e; + } + + // Call the completion handler with error + if (state->completionHandler) { + NSError* error = [NSError errorWithDomain:NSPOSIXErrorDomain code:size userInfo:nil]; + state->completionHandler(error); + Block_release(state->completionHandler); + state->completionHandler = nil; + } + + // Clean up + [state->fileHandle release]; + state->fileHandle = nil; + [state->destinationURL release]; + state->destinationURL = nil; + + return 0; + } + + // Write data to the file - Cocoa file operations are typically synchronous + // but we return the number of bytes written to match the non-blocking interface + if (state->fileHandle) { + @try { + NSData* nsData = [NSData dataWithBytes:data length:size]; + if (@available(macOS 10.15, *)) { + NSError* error = nil; + if (![state->fileHandle writeData:nsData error:&error]) { + int errCode = error ? (int)error.code : EIO; + if (errCode == 0) errCode = EIO; // Ensure we have a valid error code + source_data->error_code = errCode; + _glfwInputError(GLFW_PLATFORM_ERROR, + "Cocoa: Failed to write drag data: %s", + error ? [[error localizedDescription] UTF8String] : "unknown error"); + return -errCode; + } + } else { + // Pre-10.15 writeData: writes all bytes synchronously or throws an exception. + // NSFileHandle.writeData: is documented to write all data atomically, + // so returning size is correct. Any failure throws NSFileHandleOperationException. + [state->fileHandle writeData:nsData]; + } + // NSFileHandle.writeData writes all data atomically, so size == bytes written + return (ssize_t)size; + } @catch (NSException* e) { + source_data->error_code = EIO; + _glfwInputError(GLFW_PLATFORM_ERROR, + "Cocoa: Exception writing drag data: %s", + e ? [[e reason] UTF8String] : "unknown exception"); + return -EIO; + } + } + + // No file handle, consider all data accepted + return (ssize_t)size; +} + void _glfwPlatformUpdateDragState(_GLFWwindow* window) { // Call the drag callback with STATUS_UPDATE to get updated acceptance and MIME list // Position values are not valid for this event type diff --git a/glfw/glfw3.h b/glfw/glfw3.h index 0cfb4b6b0..58b908ee5 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -1844,25 +1844,40 @@ typedef enum { GLFW_DRAG_OPERATION_GENERIC = 4 } GLFWDragOperationType; -/*! @brief Drag data item. +/*! @brief Opaque drag source data handle. * - * This structure describes a single item of drag data with its MIME type. - * - * @sa @ref drag_start - * @sa @ref glfwStartDrag + * This is an opaque handle to a heap-allocated object that represents + * data being requested from a drag source. The lifetime is managed by + * the GLFW backend - it is freed on end of data, error, drag source + * cancellation, or at exit. * * @since Added in version 4.0. * * @ingroup input */ -typedef struct GLFWdragitem { - /*! The MIME type of this data item (e.g., "text/plain", "image/png"). */ - const char* mime_type; - /*! Pointer to the binary data. */ - const unsigned char* data; - /*! Size of the data in bytes. */ - size_t data_size; -} GLFWdragitem; +typedef struct GLFWDragSourceData GLFWDragSourceData; + +/*! @brief The function pointer type for drag source data request callbacks. + * + * This is the function pointer type for callbacks invoked when the OS + * requests data for a specific MIME type from the active drag source. + * The callback is called on the GUI thread. + * + * @param[in] window The window that initiated the drag. + * @param[in] mime_type The MIME type being requested, or NULL if the OS + * has closed the drag source. + * @param[in] source_data Opaque pointer to a heap-allocated object. Use this + * pointer when calling @ref glfwSendDragData to send data chunks. + * + * @sa @ref glfwStartDrag + * @sa @ref glfwSendDragData + * @sa @ref glfwSetDragSourceCallback + * + * @since Added in version 4.0. + * + * @ingroup input + */ +typedef void (* GLFWdragsourcefun)(GLFWwindow* window, const char* mime_type, GLFWDragSourceData* source_data); /*! @brief The function pointer type for drag event callbacks. * @@ -5069,27 +5084,55 @@ GLFWAPI GLFWliveresizefun glfwSetLiveResizeCallback(GLFWwindow* window, GLFWlive */ GLFWAPI GLFWdragfun glfwSetDragCallback(GLFWwindow* window, GLFWdragfun callback); -/*! @brief Starts a drag operation. +/*! @brief Sets the drag source data request callback. * - * This function starts a drag operation from the specified window with the - * given data items and optional thumbnail image. The drag operation will - * continue until the user releases the mouse button. + * This function sets the callback that is invoked when the OS requests data + * for a specific MIME type from the currently active drag source. The callback + * receives the MIME type and an opaque pointer to a heap-allocated object. + * The application should call @ref glfwSendDragData with chunks of data. * - * The data items array contains one or more MIME types with their associated - * binary data. The data is copied internally, so the caller can free it after - * this function returns. + * If the callback is called with a NULL mime_type, the OS has closed the + * drag source. + * + * @param[in] window The window whose callback to set. + * @param[in] callback The new callback, or `NULL` to remove the currently set + * callback. + * @return The previously set callback, or `NULL` if no callback was set or the + * library had not been [initialized](@ref intro_init). + * + * @errors Possible errors include @ref GLFW_NOT_INITIALIZED. + * + * @thread_safety This function must only be called from the main thread. + * + * @sa @ref glfwStartDrag + * @sa @ref glfwSendDragData + * + * @since Added in version 4.0. + * + * @ingroup input + */ +GLFWAPI GLFWdragsourcefun glfwSetDragSourceCallback(GLFWwindow* window, GLFWdragsourcefun callback); + +/*! @brief Starts a drag operation with lazy data loading. + * + * This function starts a drag operation from the specified window. Data for + * each MIME type is loaded on demand when the OS requests it via the drag + * source callback set with @ref glfwSetDragSourceCallback. + * + * Calling with NULL mime_types or mime_count of 0 cancels the currently + * active drag source, if any. Similarly, when called with mime types, any + * currently active drag is canceled and replaced. * * @param[in] window The window initiating the drag. - * @param[in] items Array of drag data items. - * @param[in] item_count Number of items in the array. + * @param[in] mime_types Array of MIME type strings. + * @param[in] mime_count Number of MIME types in the array. * @param[in] thumbnail Optional thumbnail/icon image to display during the * drag operation, or `NULL` for no thumbnail. The image data is copied. - * @param[in] operation The type of drag operation: @ref GLFW_DRAG_OPERATION_MOVE, - * @ref GLFW_DRAG_OPERATION_COPY, or @ref GLFW_DRAG_OPERATION_GENERIC. The default - * should be @ref GLFW_DRAG_OPERATION_MOVE. + * @param[in] operations A bitfield containing ORed values from + * @ref GLFWDragOperationType specifying allowed operations. * - * @return `true` if the drag operation was started successfully, `false` - * otherwise. + * @return Zero on success, or a POSIX error code such as EINVAL or EIO on + * failure. * * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref * GLFW_PLATFORM_ERROR. @@ -5097,13 +5140,49 @@ GLFWAPI GLFWdragfun glfwSetDragCallback(GLFWwindow* window, GLFWdragfun callback * @thread_safety This function must only be called from the main thread. * * @sa @ref drag_start - * @sa @ref glfwSetDragCallback + * @sa @ref glfwSetDragSourceCallback + * @sa @ref glfwSendDragData * * @since Added in version 4.0. * * @ingroup input */ -GLFWAPI int glfwStartDrag(GLFWwindow* window, const GLFWdragitem* items, int item_count, const GLFWimage* thumbnail, GLFWDragOperationType operation); +GLFWAPI int glfwStartDrag(GLFWwindow* window, const char* const* mime_types, int mime_count, const GLFWimage* thumbnail, int operations); + +/*! @brief Sends a chunk of drag data. + * + * This function is called by the application on the GUI thread to send chunks + * of data for a drag operation. Call this in response to the drag source + * callback. This function is non-blocking and may return before all data + * is written to the destination. + * + * End of data is indicated by calling with NULL data pointer and size zero. + * If an error occurs while reading data, call with NULL data pointer and + * size set to a POSIX error code. + * + * @param[in] source_data The opaque pointer received in the drag source callback. + * @param[in] data Pointer to the data chunk, or NULL to signal end of data or error. + * @param[in] size Size of the data chunk in bytes, or 0 for end of data, + * or a POSIX error code when data is NULL. + * + * @return The number of bytes sent (which may be less than size if the + * operation would block), or a negative POSIX error code on failure. + * For end-of-data or error signaling (NULL data), returns 0 on success + * or a negative error code. + * + * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref + * GLFW_PLATFORM_ERROR. + * + * @thread_safety This function must only be called from the main thread. + * + * @sa @ref glfwStartDrag + * @sa @ref glfwSetDragSourceCallback + * + * @since Added in version 4.0. + * + * @ingroup input + */ +GLFWAPI ssize_t glfwSendDragData(GLFWDragSourceData* source_data, const void* data, size_t size); /*! @brief Schedules a call to the drag callback to update drag state. * diff --git a/glfw/input.c b/glfw/input.c index 4a1a61845..86667c544 100644 --- a/glfw/input.c +++ b/glfw/input.c @@ -419,6 +419,14 @@ int _glfwInputDragEvent(_GLFWwindow* window, int event, double xpos, double ypos return 0; } +// Notifies shared code that the OS wants data for a MIME type from the drag source +// +void _glfwInputDragSourceRequest(_GLFWwindow* window, const char* mime_type, GLFWDragSourceData* source_data) +{ + if (window->callbacks.dragSource) + window->callbacks.dragSource((GLFWwindow*) window, mime_type, source_data); +} + // Notifies shared code of a joystick connection or disconnection // void _glfwInputJoystick(_GLFWjoystick* js, int event) @@ -1130,15 +1138,37 @@ GLFWAPI GLFWdragfun glfwSetDragCallback(GLFWwindow* handle, GLFWdragfun cbfun) return cbfun; } -GLFWAPI int glfwStartDrag(GLFWwindow* handle, const GLFWdragitem* items, int item_count, const GLFWimage* thumbnail, GLFWDragOperationType operation) +GLFWAPI GLFWdragsourcefun glfwSetDragSourceCallback(GLFWwindow* handle, GLFWdragsourcefun cbfun) { _GLFWwindow* window = (_GLFWwindow*) handle; assert(window != NULL); - assert(items != NULL); - assert(item_count > 0); - _GLFW_REQUIRE_INIT_OR_RETURN(false); - return _glfwPlatformStartDrag(window, items, item_count, thumbnail, operation); + _GLFW_REQUIRE_INIT_OR_RETURN(NULL); + _GLFW_SWAP_POINTERS(window->callbacks.dragSource, cbfun); + return cbfun; +} + +GLFWAPI int glfwStartDrag(GLFWwindow* handle, const char* const* mime_types, int mime_count, const GLFWimage* thumbnail, int operations) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + assert(window != NULL); + + _GLFW_REQUIRE_INIT_OR_RETURN(EINVAL); + + // If no mime types, cancel any existing drag + if (!mime_types || mime_count <= 0) { + _glfwPlatformCancelDrag(window); + return 0; + } + + return _glfwPlatformStartDrag(window, mime_types, mime_count, thumbnail, operations); +} + +GLFWAPI ssize_t glfwSendDragData(GLFWDragSourceData* source_data, const void* data, size_t size) +{ + if (!source_data) return -EINVAL; + _GLFW_REQUIRE_INIT_OR_RETURN(-EINVAL); + return _glfwPlatformSendDragData(source_data, data, size); } GLFWAPI void glfwUpdateDragState(GLFWwindow* handle) diff --git a/glfw/internal.h b/glfw/internal.h index b58cc155b..a946b34aa 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -106,6 +106,17 @@ struct GLFWDropData { int x11_version; // Xdnd protocol version (X11) }; +// Drag source data structure for chunked writing of drag data +// Lifetime is managed by the backend - freed on end of data, error, drag cancellation, or exit +struct GLFWDragSourceData { + GLFWid window_id; // ID of window that initiated the drag (use _glfwWindowForId to get pointer) + char* mime_type; // MIME type being sent (owned, copied from request) + int write_fd; // File descriptor for writing data (Wayland/X11), -1 if not used + bool finished; // Whether data sending is complete (EOF or error) + int error_code; // POSIX error code if an error occurred, 0 otherwise + void* platform_data; // Platform-specific data +}; + typedef void (* _GLFWmakecontextcurrentfun)(_GLFWwindow*); typedef void (* _GLFWswapbuffersfun)(_GLFWwindow*); typedef void (* _GLFWswapintervalfun)(int); @@ -502,6 +513,7 @@ struct _GLFWwindow GLFWdropfun drop; GLFWliveresizefun liveResize; GLFWdragfun drag; + GLFWdragsourcefun dragSource; } callbacks; // This is defined in the window API's platform.h @@ -788,7 +800,9 @@ void _glfwPlatformSetWindowOpacity(_GLFWwindow* window, float opacity); void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev); void _glfwPlatformChangeCursorTheme(void); -int _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWdragitem* items, int item_count, const GLFWimage* thumbnail, GLFWDragOperationType operation); +int _glfwPlatformStartDrag(_GLFWwindow* window, const char* const* mime_types, int mime_count, const GLFWimage* thumbnail, int operations); +ssize_t _glfwPlatformSendDragData(GLFWDragSourceData* source_data, const void* data, size_t size); +void _glfwPlatformCancelDrag(_GLFWwindow* window); void _glfwPlatformUpdateDragState(_GLFWwindow* window); void _glfwPlatformPollEvents(void); @@ -845,6 +859,7 @@ void _glfwInputCursorPos(_GLFWwindow* window, double xpos, double ypos); void _glfwInputCursorEnter(_GLFWwindow* window, bool entered); void _glfwInputDrop(_GLFWwindow* window, GLFWDropData* drop); int _glfwInputDragEvent(_GLFWwindow* window, int event, double xpos, double ypos, const char** mime_types, int* mime_count); +void _glfwInputDragSourceRequest(_GLFWwindow* window, const char* mime_type, GLFWDragSourceData* source_data); // Platform functions for drop data reading const char** _glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count); diff --git a/glfw/null_window.c b/glfw/null_window.c index 193829f81..9d90a791f 100644 --- a/glfw/null_window.c +++ b/glfw/null_window.c @@ -531,13 +531,25 @@ void _glfwPlatformSetCursor(_GLFWwindow* window UNUSED, _GLFWcursor* cursor UNUS { } -int _glfwPlatformStartDrag(_GLFWwindow* window UNUSED, - const GLFWdragitem* items UNUSED, - int item_count UNUSED, - const GLFWimage* thumbnail UNUSED, - GLFWDragOperationType operation UNUSED) +void _glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) { - return false; + // No-op for null platform +} + +int _glfwPlatformStartDrag(_GLFWwindow* window UNUSED, + const char* const* mime_types UNUSED, + int mime_count UNUSED, + const GLFWimage* thumbnail UNUSED, + int operations UNUSED) +{ + return ENOTSUP; +} + +ssize_t _glfwPlatformSendDragData(GLFWDragSourceData* source_data UNUSED, + const void* data UNUSED, + size_t size UNUSED) +{ + return -ENOTSUP; } void _glfwPlatformUpdateDragState(_GLFWwindow* window UNUSED) diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index 8baa710bf..64bc7378f 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -412,10 +412,10 @@ typedef struct _GLFWlibraryWayland // Drag operation state struct { struct wl_data_source* source; - unsigned char** items_data; - size_t* items_sizes; - char** items_mimes; - int item_count; + char** mimes; // Array of MIME type strings + int mime_count; // Number of MIME types + _GLFWwindow* window; // Window that initiated the drag + GLFWDragSourceData* current_request; // Current data request being processed } drag; } _GLFWlibraryWayland; diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 39e1ff812..5008ce1b2 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -3006,37 +3006,83 @@ GLFWAPI bool glfwWaylandBeep(GLFWwindow *handle) { // Drag operation implementation +static void cleanup_drag_source_data(GLFWDragSourceData* data) { + if (!data) return; + if (data->write_fd >= 0) { + close(data->write_fd); + data->write_fd = -1; + } + free(data->mime_type); + free(data); +} + +static void cleanup_drag(void) { + // 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); + } + + // Clean up any pending data request + if (_glfw.wl.drag.current_request) { + cleanup_drag_source_data(_glfw.wl.drag.current_request); + _glfw.wl.drag.current_request = NULL; + } + + // Clean up MIME type strings + 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; +} + static void drag_source_send(void *data UNUSED, struct wl_data_source *source UNUSED, const char *mime_type, int fd) { - // Find the matching MIME type and send its data - for (int i = 0; i < _glfw.wl.drag.item_count; i++) { - if (strcmp(_glfw.wl.drag.items_mimes[i], mime_type) == 0) { - write_all(fd, (const char*)_glfw.wl.drag.items_data[i], _glfw.wl.drag.items_sizes[i]); - break; - } + if (!_glfw.wl.drag.window) { + close(fd); + return; } - close(fd); + + // Create a new drag source data request + GLFWDragSourceData* request = calloc(1, sizeof(GLFWDragSourceData)); + if (!request) { + close(fd); + return; + } + + request->window_id = _glfw.wl.drag.window ? _glfw.wl.drag.window->id : 0; + request->mime_type = _glfw_strdup(mime_type); + request->write_fd = fd; + request->finished = false; + request->error_code = 0; + + if (!request->mime_type) { + cleanup_drag_source_data(request); + return; + } + + // Store as 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); } static void drag_source_cancelled(void *data UNUSED, struct wl_data_source *source) { // Clean up drag data if (_glfw.wl.drag.source == source) { - for (int i = 0; i < _glfw.wl.drag.item_count; i++) { - free(_glfw.wl.drag.items_data[i]); - free(_glfw.wl.drag.items_mimes[i]); - } - free(_glfw.wl.drag.items_data); - free(_glfw.wl.drag.items_sizes); - free(_glfw.wl.drag.items_mimes); - _glfw.wl.drag.items_data = NULL; - _glfw.wl.drag.items_sizes = NULL; - _glfw.wl.drag.items_mimes = NULL; - _glfw.wl.drag.item_count = 0; + cleanup_drag(); _glfw.wl.drag.source = NULL; } - wl_data_source_destroy(source); - _glfw.wl.drag.source = NULL; + if (source) { + wl_data_source_destroy(source); + } } static void @@ -3065,67 +3111,66 @@ static const struct wl_data_source_listener drag_source_listener = { .dnd_finished = drag_source_dnd_finished, }; +void +_glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) { + if (_glfw.wl.drag.source) { + drag_source_cancelled(NULL, _glfw.wl.drag.source); + _glfw.wl.drag.source = NULL; + } +} + int -_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWdragitem* items, int item_count, const GLFWimage* thumbnail, GLFWDragOperationType operation) { +_glfwPlatformStartDrag(_GLFWwindow* window, const char* const* mime_types, int mime_count, const GLFWimage* thumbnail, int operations) { if (!_glfw.wl.dataDeviceManager) { _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Data device manager not available"); - return false; + return EIO; } if (!_glfw.wl.dataDevice) { _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Data device not available"); - return false; + return EIO; } // Clean up any existing drag operation - if (_glfw.wl.drag.source) drag_source_cancelled(NULL, _glfw.wl.drag.source); + _glfwPlatformCancelDrag(window); // Create the data source _glfw.wl.drag.source = wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager); if (!_glfw.wl.drag.source) { _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to create data source for drag"); - return false; + return EIO; } - // Set the DND action based on operation type + // Set the DND action based on operation type (bitfield) uint32_t wl_actions = 0; - switch (operation) { - case GLFW_DRAG_OPERATION_COPY: wl_actions = WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY; break; - case GLFW_DRAG_OPERATION_MOVE: wl_actions = WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE; break; - case GLFW_DRAG_OPERATION_GENERIC: - wl_actions = WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE; break; - } + if (operations & GLFW_DRAG_OPERATION_COPY) + wl_actions |= WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY; + if (operations & GLFW_DRAG_OPERATION_MOVE) + wl_actions |= WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE; + if (operations & GLFW_DRAG_OPERATION_GENERIC) + wl_actions |= WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE; wl_data_source_set_actions(_glfw.wl.drag.source, wl_actions); - // Allocate storage for drag data (copy the data) - _glfw.wl.drag.items_data = calloc(item_count, sizeof(unsigned char*)); - _glfw.wl.drag.items_sizes = calloc(item_count, sizeof(size_t)); - _glfw.wl.drag.items_mimes = calloc(item_count, sizeof(char*)); - _glfw.wl.drag.item_count = item_count; + // 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; - if (!_glfw.wl.drag.items_data || !_glfw.wl.drag.items_sizes || !_glfw.wl.drag.items_mimes) { - drag_source_cancelled(NULL, _glfw.wl.drag.source); - _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to allocate drag data"); - return false; + if (!_glfw.wl.drag.mimes) { + _glfwPlatformCancelDrag(window); + _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to allocate drag MIME types"); + return ENOMEM; } - // Copy the data and offer MIME types - for (int i = 0; i < item_count; i++) { - _glfw.wl.drag.items_data[i] = malloc(items[i].data_size); - if (!_glfw.wl.drag.items_data[i]) { - drag_source_cancelled(NULL, _glfw.wl.drag.source); - _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to allocate drag item data"); - return false; + // Copy MIME types and offer them + for (int i = 0; i < mime_count; i++) { + _glfw.wl.drag.mimes[i] = _glfw_strdup(mime_types[i]); + if (!_glfw.wl.drag.mimes[i]) { + _glfwPlatformCancelDrag(window); + _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to allocate drag MIME type"); + return ENOMEM; } - memcpy(_glfw.wl.drag.items_data[i], items[i].data, items[i].data_size); - _glfw.wl.drag.items_sizes[i] = items[i].data_size; - _glfw.wl.drag.items_mimes[i] = _glfw_strdup(items[i].mime_type); - if (!_glfw.wl.drag.items_mimes[i]) { - drag_source_cancelled(NULL, _glfw.wl.drag.source); - _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Failed to allocate drag item MIME type"); - return false; - } - wl_data_source_offer(_glfw.wl.drag.source, items[i].mime_type); + wl_data_source_offer(_glfw.wl.drag.source, mime_types[i]); } wl_data_source_add_listener(_glfw.wl.drag.source, &drag_source_listener, NULL); @@ -3158,7 +3203,51 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWdragitem* items, int item_ wl_surface_destroy(icon_surface); } - return true; + return 0; +} + +ssize_t +_glfwPlatformSendDragData(GLFWDragSourceData* source_data, const void* data, size_t size) { + if (!source_data || source_data->finished) return -EINVAL; + if (source_data->write_fd < 0) return -EIO; + + // End of data: NULL data pointer and size zero + if (!data && size == 0) { + source_data->finished = true; + close(source_data->write_fd); + source_data->write_fd = -1; + return 0; + } + + // Error from application: NULL data pointer and size is error code + if (!data && size > 0) { + source_data->finished = true; + source_data->error_code = (int)size; + close(source_data->write_fd); + source_data->write_fd = -1; + return 0; + } + + // Non-blocking write - retry on EINTR, return 0 on would-block + ssize_t written; + do { + written = write(source_data->write_fd, data, size); + } while (written < 0 && errno == EINTR); + + if (written < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Would block, return 0 bytes written + return 0; + } + // Actual error + source_data->finished = true; + source_data->error_code = errno; + close(source_data->write_fd); + source_data->write_fd = -1; + return -errno; + } + + return written; } void diff --git a/glfw/x11_platform.h b/glfw/x11_platform.h index 6971939a6..462a11c58 100644 --- a/glfw/x11_platform.h +++ b/glfw/x11_platform.h @@ -394,13 +394,13 @@ typedef struct _GLFWlibraryX11 // Drag source state struct { Window source_window; - unsigned char** items_data; - size_t* items_sizes; - char** items_mimes; - int item_count; - Atom* type_atoms; - Atom action_atom; // XdndActionCopy, XdndActionMove, or XdndActionLink + char** mimes; // Array of MIME type strings + int mime_count; // Number of MIME types + Atom* type_atoms; // Atoms for each MIME type + Atom action_atom; // XdndActionCopy, XdndActionMove, or XdndActionLink bool active; + _GLFWwindow* window; // Window that initiated the drag + GLFWDragSourceData* current_request; // Current data request being processed } drag; struct { diff --git a/glfw/x11_window.c b/glfw/x11_window.c index e5f4e3697..3a37c35c5 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -3711,86 +3711,93 @@ GLFWAPI int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc) } // Helper function to clean up drag source data -static void cleanupDragSource(void) { - if (_glfw.x11.drag.items_data) { - for (int i = 0; i < _glfw.x11.drag.item_count; i++) { - free(_glfw.x11.drag.items_data[i]); - free(_glfw.x11.drag.items_mimes[i]); - } - free(_glfw.x11.drag.items_data); - free(_glfw.x11.drag.items_sizes); - free(_glfw.x11.drag.items_mimes); - free(_glfw.x11.drag.type_atoms); - _glfw.x11.drag.items_data = NULL; - _glfw.x11.drag.items_sizes = NULL; - _glfw.x11.drag.items_mimes = NULL; - _glfw.x11.drag.type_atoms = NULL; - _glfw.x11.drag.item_count = 0; - _glfw.x11.drag.source_window = None; - _glfw.x11.drag.active = false; +static void cleanup_x11_drag_source_data(GLFWDragSourceData* data) { + if (!data) return; + if (data->write_fd >= 0) { + close(data->write_fd); + data->write_fd = -1; } + free(data->mime_type); + free(data); +} + +static void cleanupDragSource(void) { + // Notify the application that the drag source is closed + if (_glfw.x11.drag.window && _glfw.x11.drag.window->callbacks.dragSource) { + _glfwInputDragSourceRequest(_glfw.x11.drag.window, NULL, NULL); + } + + // Clean up any pending data request + if (_glfw.x11.drag.current_request) { + cleanup_x11_drag_source_data(_glfw.x11.drag.current_request); + _glfw.x11.drag.current_request = NULL; + } + + // Clean up MIME type strings and atoms + for (int i = 0; i < _glfw.x11.drag.mime_count; i++) { + free(_glfw.x11.drag.mimes[i]); + } + free(_glfw.x11.drag.mimes); + free(_glfw.x11.drag.type_atoms); + _glfw.x11.drag.mimes = NULL; + _glfw.x11.drag.type_atoms = NULL; + _glfw.x11.drag.mime_count = 0; + _glfw.x11.drag.source_window = None; + _glfw.x11.drag.active = false; + _glfw.x11.drag.window = NULL; +} + +void _glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) { + cleanupDragSource(); } int _glfwPlatformStartDrag(_GLFWwindow* window, - const GLFWdragitem* items, - int item_count, + const char* const* mime_types, + int mime_count, const GLFWimage* thumbnail UNUSED, - GLFWDragOperationType operation) { + int operations) { // Clean up any existing drag operation cleanupDragSource(); - // Set the drag action based on operation type - switch (operation) { - case GLFW_DRAG_OPERATION_COPY: - _glfw.x11.drag.action_atom = _glfw.x11.XdndActionCopy; - break; - case GLFW_DRAG_OPERATION_MOVE: - _glfw.x11.drag.action_atom = _glfw.x11.XdndActionMove; - break; - case GLFW_DRAG_OPERATION_GENERIC: - _glfw.x11.drag.action_atom = _glfw.x11.XdndActionCopy; - break; + // Set the drag action based on operation type (bitfield) + // Default to copy, prefer move if specified + if (operations & GLFW_DRAG_OPERATION_MOVE) { + _glfw.x11.drag.action_atom = _glfw.x11.XdndActionMove; + } else if (operations & GLFW_DRAG_OPERATION_COPY) { + _glfw.x11.drag.action_atom = _glfw.x11.XdndActionCopy; + } else { + _glfw.x11.drag.action_atom = _glfw.x11.XdndActionCopy; } - // Allocate storage for drag data (copy the data) - _glfw.x11.drag.items_data = calloc(item_count, sizeof(unsigned char*)); - _glfw.x11.drag.items_sizes = calloc(item_count, sizeof(size_t)); - _glfw.x11.drag.items_mimes = calloc(item_count, sizeof(char*)); - _glfw.x11.drag.type_atoms = calloc(item_count, sizeof(Atom)); - _glfw.x11.drag.item_count = item_count; + // Allocate storage for MIME types + _glfw.x11.drag.mimes = calloc(mime_count, sizeof(char*)); + _glfw.x11.drag.type_atoms = calloc(mime_count, sizeof(Atom)); + _glfw.x11.drag.mime_count = mime_count; _glfw.x11.drag.source_window = window->x11.handle; + _glfw.x11.drag.window = window; - if (!_glfw.x11.drag.items_data || !_glfw.x11.drag.items_sizes || - !_glfw.x11.drag.items_mimes || !_glfw.x11.drag.type_atoms) { + if (!_glfw.x11.drag.mimes || !_glfw.x11.drag.type_atoms) { cleanupDragSource(); _glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to allocate drag data"); - return false; + return ENOMEM; } - // Copy the data and create atoms for MIME types - for (int i = 0; i < item_count; i++) { - _glfw.x11.drag.items_data[i] = malloc(items[i].data_size); - if (!_glfw.x11.drag.items_data[i]) { + // Copy MIME types and create atoms + for (int i = 0; i < mime_count; i++) { + _glfw.x11.drag.mimes[i] = _glfw_strdup(mime_types[i]); + if (!_glfw.x11.drag.mimes[i]) { cleanupDragSource(); - _glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to allocate drag item data"); - return false; + _glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to allocate drag MIME type"); + return ENOMEM; } - memcpy(_glfw.x11.drag.items_data[i], items[i].data, items[i].data_size); - _glfw.x11.drag.items_sizes[i] = items[i].data_size; - _glfw.x11.drag.items_mimes[i] = _glfw_strdup(items[i].mime_type); - if (!_glfw.x11.drag.items_mimes[i]) { - cleanupDragSource(); - _glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to allocate drag item MIME type"); - return false; - } - _glfw.x11.drag.type_atoms[i] = XInternAtom(_glfw.x11.display, items[i].mime_type, False); + _glfw.x11.drag.type_atoms[i] = XInternAtom(_glfw.x11.display, mime_types[i], False); } // Set up XdndTypeList property if we have more than 3 types - if (item_count > 3) { + if (mime_count > 3) { XChangeProperty(_glfw.x11.display, window->x11.handle, _glfw.x11.XdndTypeList, XA_ATOM, 32, PropModeReplace, - (unsigned char*)_glfw.x11.drag.type_atoms, item_count); + (unsigned char*)_glfw.x11.drag.type_atoms, mime_count); } // Take ownership of XdndSelection @@ -3800,7 +3807,7 @@ int _glfwPlatformStartDrag(_GLFWwindow* window, if (XGetSelectionOwner(_glfw.x11.display, _glfw.x11.XdndSelection) != window->x11.handle) { cleanupDragSource(); _glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to acquire XdndSelection ownership"); - return false; + return EIO; } _glfw.x11.drag.active = true; @@ -3821,7 +3828,61 @@ int _glfwPlatformStartDrag(_GLFWwindow* window, // event loop. For now, we set up the data source so the application can // handle its own drag tracking if needed. - return true; + return 0; +} + +ssize_t _glfwPlatformSendDragData(GLFWDragSourceData* source_data, const void* data, size_t size) { + if (!source_data || source_data->finished) return -EINVAL; + + // For X11, we typically set properties for SelectionRequest events + // The write_fd is used if we set up a pipe-based transfer + + // End of data: NULL data pointer and size zero + if (!data && size == 0) { + source_data->finished = true; + if (source_data->write_fd >= 0) { + close(source_data->write_fd); + source_data->write_fd = -1; + } + return 0; + } + + // Error from application: NULL data pointer and size is error code + if (!data && size > 0) { + source_data->finished = true; + source_data->error_code = (int)size; + if (source_data->write_fd >= 0) { + close(source_data->write_fd); + source_data->write_fd = -1; + } + return 0; + } + + // For X11, data is typically set via XChangeProperty in response to SelectionRequest + // Store the data in platform_data for the SelectionRequest handler + // Non-blocking write - retry on EINTR, return 0 on would-block + if (source_data->write_fd >= 0) { + ssize_t written; + do { + written = write(source_data->write_fd, data, size); + } while (written < 0 && errno == EINTR); + + if (written < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Would block, return 0 bytes written + return 0; + } + source_data->finished = true; + source_data->error_code = errno; + close(source_data->write_fd); + source_data->write_fd = -1; + return -errno; + } + return written; + } + + // No fd available, all data is accepted (buffered by platform_data) + return (ssize_t)size; } void _glfwPlatformUpdateDragState(_GLFWwindow* window) { diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index c5653bf9a..40042820d 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -362,9 +362,15 @@ load_glfw(const char* path) { *(void **) (&glfwSetDragCallback_impl) = dlsym(handle, "glfwSetDragCallback"); if (glfwSetDragCallback_impl == NULL) fail("Failed to load glfw function glfwSetDragCallback with error: %s", dlerror()); + *(void **) (&glfwSetDragSourceCallback_impl) = dlsym(handle, "glfwSetDragSourceCallback"); + if (glfwSetDragSourceCallback_impl == NULL) fail("Failed to load glfw function glfwSetDragSourceCallback with error: %s", dlerror()); + *(void **) (&glfwStartDrag_impl) = dlsym(handle, "glfwStartDrag"); if (glfwStartDrag_impl == NULL) fail("Failed to load glfw function glfwStartDrag with error: %s", dlerror()); + *(void **) (&glfwSendDragData_impl) = dlsym(handle, "glfwSendDragData"); + if (glfwSendDragData_impl == NULL) fail("Failed to load glfw function glfwSendDragData with error: %s", dlerror()); + *(void **) (&glfwUpdateDragState_impl) = dlsym(handle, "glfwUpdateDragState"); if (glfwUpdateDragState_impl == NULL) fail("Failed to load glfw function glfwUpdateDragState with error: %s", dlerror()); diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 5f625de53..b3df6b33b 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1572,25 +1572,40 @@ typedef enum { GLFW_DRAG_OPERATION_GENERIC = 4 } GLFWDragOperationType; -/*! @brief Drag data item. +/*! @brief Opaque drag source data handle. * - * This structure describes a single item of drag data with its MIME type. - * - * @sa @ref drag_start - * @sa @ref glfwStartDrag + * This is an opaque handle to a heap-allocated object that represents + * data being requested from a drag source. The lifetime is managed by + * the GLFW backend - it is freed on end of data, error, drag source + * cancellation, or at exit. * * @since Added in version 4.0. * * @ingroup input */ -typedef struct GLFWdragitem { - /*! The MIME type of this data item (e.g., "text/plain", "image/png"). */ - const char* mime_type; - /*! Pointer to the binary data. */ - const unsigned char* data; - /*! Size of the data in bytes. */ - size_t data_size; -} GLFWdragitem; +typedef struct GLFWDragSourceData GLFWDragSourceData; + +/*! @brief The function pointer type for drag source data request callbacks. + * + * This is the function pointer type for callbacks invoked when the OS + * requests data for a specific MIME type from the active drag source. + * The callback is called on the GUI thread. + * + * @param[in] window The window that initiated the drag. + * @param[in] mime_type The MIME type being requested, or NULL if the OS + * has closed the drag source. + * @param[in] source_data Opaque pointer to a heap-allocated object. Use this + * pointer when calling @ref glfwSendDragData to send data chunks. + * + * @sa @ref glfwStartDrag + * @sa @ref glfwSendDragData + * @sa @ref glfwSetDragSourceCallback + * + * @since Added in version 4.0. + * + * @ingroup input + */ +typedef void (* GLFWdragsourcefun)(GLFWwindow* window, const char* mime_type, GLFWDragSourceData* source_data); /*! @brief The function pointer type for drag event callbacks. * @@ -2315,10 +2330,18 @@ typedef GLFWdragfun (*glfwSetDragCallback_func)(GLFWwindow*, GLFWdragfun); GFW_EXTERN glfwSetDragCallback_func glfwSetDragCallback_impl; #define glfwSetDragCallback glfwSetDragCallback_impl -typedef int (*glfwStartDrag_func)(GLFWwindow*, const GLFWdragitem*, int, const GLFWimage*, GLFWDragOperationType); +typedef GLFWdragsourcefun (*glfwSetDragSourceCallback_func)(GLFWwindow*, GLFWdragsourcefun); +GFW_EXTERN glfwSetDragSourceCallback_func glfwSetDragSourceCallback_impl; +#define glfwSetDragSourceCallback glfwSetDragSourceCallback_impl + +typedef int (*glfwStartDrag_func)(GLFWwindow*, const char* const*, int, const GLFWimage*, int); GFW_EXTERN glfwStartDrag_func glfwStartDrag_impl; #define glfwStartDrag glfwStartDrag_impl +typedef ssize_t (*glfwSendDragData_func)(GLFWDragSourceData*, const void*, size_t); +GFW_EXTERN glfwSendDragData_func glfwSendDragData_impl; +#define glfwSendDragData glfwSendDragData_impl + typedef void (*glfwUpdateDragState_func)(GLFWwindow*); GFW_EXTERN glfwUpdateDragState_func glfwUpdateDragState_impl; #define glfwUpdateDragState glfwUpdateDragState_impl