diff --git a/Headers/wayland/WaylandServer.h b/Headers/wayland/WaylandServer.h index 8fc4cb3d..02b230f6 100644 --- a/Headers/wayland/WaylandServer.h +++ b/Headers/wayland/WaylandServer.h @@ -82,6 +82,8 @@ typedef struct _WaylandConfig struct wl_shm *shm; struct wl_seat *seat; struct wl_keyboard *keyboard; + struct wl_data_device_manager *data_device_manager; + struct wl_data_device *data_device; struct xdg_wm_base *wm_base; struct zwlr_layer_shell_v1 *layer_shell; #ifdef HAVE_EGL diff --git a/Source/wayland/GNUmakefile b/Source/wayland/GNUmakefile index 3a9b764e..ab863b6d 100644 --- a/Source/wayland/GNUmakefile +++ b/Source/wayland/GNUmakefile @@ -51,6 +51,7 @@ WaylandServer+Keyboard.m \ WaylandServer+Seat.m \ WaylandServer+Xdgshell.m \ WaylandServer+Layershell.m \ +WaylandDragView.m \ WaylandInputServer.m \ ifeq ($(WITH_EGL),yes) diff --git a/Source/wayland/WaylandDragView.m b/Source/wayland/WaylandDragView.m new file mode 100644 index 00000000..ae012feb --- /dev/null +++ b/Source/wayland/WaylandDragView.m @@ -0,0 +1,513 @@ +/* + WaylandDragView - Drag and drop for the Wayland backend. + + Copyright (C) 2026 Free Software Foundation, Inc. + + This file is part of GNUstep. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; see the file COPYING.LIB. + If not, see or write to the + Free Software Foundation, 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "wayland/WaylandServer.h" + +#include +#include + +extern NSWindow *GSWindowWithNumber(NSInteger windowNumber); + +@interface WaylandDragView : GSDragView +{ + /* Source side: the offer we present to the compositor. */ + struct wl_data_source *_dataSource; + BOOL _dragActive; + NSDragOperation _resultOperation; + + /* Destination side: the offer the compositor hands us over one of our + * own surfaces, plus the enter serial needed to accept it. */ + struct wl_data_offer *_destOffer; + uint32_t _destEnterSerial; + struct window *_destWlWindow; +} ++ (id) sharedDragView; +- (void) _writeMime: (const char *)mime toFD: (int)fd; +- (void) _dragEnded: (NSDragOperation)op; +- (void) _dragEntered: (struct window *)win + offer: (struct wl_data_offer *)offer + serial: (uint32_t)serial + at: (NSPoint)surfacePoint; +- (void) _dragMovedTo: (NSPoint)surfacePoint; +- (void) _dragDropped; +- (void) _dragExited; +@end + +@interface WaylandServer (DragAndDrop) +- (WaylandConfig *) wlconfig; +- (id ) dragInfo; +@end + +/* Map an NSPasteboard type to the wayland mime types we advertise for it. */ +static NSArray * +mimesForType(NSString *type) +{ + if ([type isEqualToString: NSStringPboardType]) + { + return [NSArray arrayWithObjects: @"text/plain;charset=utf-8", + @"UTF8_STRING", @"text/plain", @"TEXT", nil]; + } + return [NSArray arrayWithObject: type]; +} + +/* Map an advertised wayland mime type back to an NSPasteboard type. */ +static NSString * +typeForMime(const char *mime) +{ + NSString *m = [NSString stringWithUTF8String: mime]; + + if ([m hasPrefix: @"text/plain"] || [m isEqualToString: @"UTF8_STRING"] + || [m isEqualToString: @"TEXT"]) + { + return NSStringPboardType; + } + return m; +} + +/* ---- wl_data_source (drag source) callbacks ---- */ + +static void +ds_target(void *data, struct wl_data_source *src, const char *mime) {} + +static void +ds_send(void *data, struct wl_data_source *src, const char *mime, int32_t fd) +{ + [(WaylandDragView *)data _writeMime: mime toFD: fd]; +} + +static void +ds_cancelled(void *data, struct wl_data_source *src) +{ + [(WaylandDragView *)data _dragEnded: NSDragOperationNone]; +} + +static void +ds_dnd_drop_performed(void *data, struct wl_data_source *src) {} + +static void +ds_dnd_finished(void *data, struct wl_data_source *src) +{ + [(WaylandDragView *)data _dragEnded: NSDragOperationCopy]; +} + +static void +ds_action(void *data, struct wl_data_source *src, uint32_t action) {} + +static const struct wl_data_source_listener data_source_listener = { + ds_target, ds_send, ds_cancelled, ds_dnd_drop_performed, + ds_dnd_finished, ds_action +}; + +/* ---- wl_data_device (drag destination) callbacks ---- * + * The compositor drives a drag over one of our surfaces and delivers these + * events. We translate them into the GSAppKitDragging* events that + * -[NSWindow sendEvent:] dispatches to the destination view, exactly as the + * X11 backend does from XDND client messages. */ + +static void +dd_data_offer(void *data, struct wl_data_device *dd, struct wl_data_offer *offer) +{ + /* A new offer is announced before the enter; the mime types follow on the + * offer object itself. We accept during the drag once the view has said it + * wants the data, so nothing to do here beyond noting it exists. */ + NSDebugLLog(@"NSDragging", @"wayland: data_offer %p", offer); +} + +static void +dd_enter(void *data, struct wl_data_device *dd, uint32_t serial, + struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, + struct wl_data_offer *offer) +{ + struct window *win = surface ? wl_surface_get_user_data(surface) : NULL; + + [[WaylandDragView sharedDragView] + _dragEntered: win + offer: offer + serial: serial + at: NSMakePoint(wl_fixed_to_double(x), wl_fixed_to_double(y))]; +} + +static void +dd_leave(void *data, struct wl_data_device *dd) +{ + [[WaylandDragView sharedDragView] _dragExited]; +} + +static void +dd_motion(void *data, struct wl_data_device *dd, uint32_t time, + wl_fixed_t x, wl_fixed_t y) +{ + [[WaylandDragView sharedDragView] + _dragMovedTo: NSMakePoint(wl_fixed_to_double(x), wl_fixed_to_double(y))]; +} + +static void +dd_drop(void *data, struct wl_data_device *dd) +{ + [[WaylandDragView sharedDragView] _dragDropped]; +} + +static void +dd_selection(void *data, struct wl_data_device *dd, struct wl_data_offer *offer) +{ + /* Clipboard selection is handled through XWayland by gpbs, ignore here. */ +} + +const struct wl_data_device_listener data_device_listener = { + dd_data_offer, dd_enter, dd_leave, dd_motion, dd_drop, dd_selection +}; + +/* ---- the drag view ---- */ + +@implementation WaylandDragView + +static WaylandDragView *sharedDragView = nil; + ++ (id) sharedDragView +{ + if (sharedDragView == nil) + { + sharedDragView = [WaylandDragView new]; + } + return sharedDragView; +} + +- (WaylandConfig *) _wlconfig +{ + return [(WaylandServer *)GSCurrentServer() wlconfig]; +} + +- (void) _writeMime: (const char *)mime toFD: (int)fd +{ + NSString *type = typeForMime(mime); + NSData *d = nil; + + if ([type isEqualToString: NSStringPboardType]) + { + NSString *s = [dragPasteboard stringForType: NSStringPboardType]; + d = [s dataUsingEncoding: NSUTF8StringEncoding]; + } + else + { + d = [dragPasteboard dataForType: type]; + } + + if (d != nil) + { + const char *bytes = [d bytes]; + NSUInteger len = [d length]; + NSUInteger off = 0; + while (off < len) + { + ssize_t n = write(fd, bytes + off, len - off); + if (n <= 0) + break; + off += n; + } + } + close(fd); +} + +- (void) _dragEnded: (NSDragOperation)op +{ + _resultOperation = op; + _dragActive = NO; +} + +- (void) dragImage: (NSImage *)anImage + at: (NSPoint)screenLocation + offset: (NSSize)initialOffset + event: (NSEvent *)event + pasteboard: (NSPasteboard *)pboard + source: (id)sourceObject + slideBack: (BOOL)slideFlag +{ + WaylandConfig *wlconfig = [self _wlconfig]; + struct window *win; + struct wl_surface *origin = NULL; + NSEnumerator *e; + NSString *type; + + if (wlconfig == NULL || wlconfig->data_device == NULL + || wlconfig->data_device_manager == NULL) + { + NSDebugLLog(@"NSDragging", @"wayland: no data device, cannot drag"); + return; + } + + ASSIGN(dragPasteboard, pboard); + ASSIGN(dragSource, sourceObject); + slideBack = slideFlag; + dragSequence = [event timestamp]; + + win = get_window_with_id(wlconfig, [[event window] windowNumber]); + if (win != NULL) + origin = win->surface; + + /* Build a data source offering the pasteboard types. */ + _dataSource = wl_data_device_manager_create_data_source( + wlconfig->data_device_manager); + wl_data_source_add_listener(_dataSource, &data_source_listener, self); + + e = [[pboard types] objectEnumerator]; + while ((type = [e nextObject]) != nil) + { + NSEnumerator *me = [mimesForType(type) objectEnumerator]; + NSString *mime; + while ((mime = [me nextObject]) != nil) + { + wl_data_source_offer(_dataSource, [mime UTF8String]); + } + } + + wl_data_source_set_actions(_dataSource, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY + | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE); + + if ([dragSource respondsToSelector: @selector(draggedImage:beganAt:)]) + { + [dragSource draggedImage: anImage beganAt: screenLocation]; + } + + /* Hand the drag over to the compositor, which resolves the drop target. + * Unlike the X11 backend we do not run a client-side tracking loop: on + * Wayland the compositor owns the pointer for the duration of the drag. */ + _dragActive = YES; + _resultOperation = NSDragOperationNone; + wl_data_device_start_drag(wlconfig->data_device, _dataSource, origin, + NULL /* no drag icon surface */, wlconfig->pointer.serial); + wl_display_flush(wlconfig->display); + + /* Modal loop until the compositor concludes the drag. This mirrors the + * ordinary application event loop: pump events in the default run loop mode + * (where the Wayland display fd is watched) so the data_device callbacks + * fire, the GSAppKitDragging* events we post are delivered to the + * destination view, and the drop's Finished handler clears _dragActive. */ + while (_dragActive) + { + NSEvent *ev; + + ev = [NSApp nextEventMatchingMask: NSAnyEventMask + untilDate: [NSDate dateWithTimeIntervalSinceNow: 0.02] + inMode: NSDefaultRunLoopMode + dequeue: YES]; + if (ev != nil) + { + [NSApp sendEvent: ev]; + } + } + + if ([dragSource respondsToSelector: @selector(draggedImage:endedAt:operation:)]) + { + [dragSource draggedImage: anImage + endedAt: dragPosition + operation: _resultOperation]; + } + + _dataSource = NULL; + DESTROY(dragSource); +} + +/* ---- destination side ---- */ + +/* Post one GSAppKitDragging* event to the destination window; -[NSWindow + * sendEvent:] picks it up out of the queue and drives the view's dragging + * protocol using this drag view as the NSDraggingInfo. */ +- (void) _postDrag: (GSAppKitSubtype)subtype + at: (NSPoint)winPoint + window: (int)winId + action: (NSDragOperation)action +{ + NSEvent *e; + + e = [NSEvent otherEventWithType: NSAppKitDefined + location: winPoint + modifierFlags: 0 + timestamp: 0 + windowNumber: winId + context: GSCurrentContext() + subtype: subtype + data1: winId + data2: action]; + [GSCurrentServer() postEvent: e atStart: NO]; +} + +- (void) _dragEntered: (struct window *)win + offer: (struct wl_data_offer *)offer + serial: (uint32_t)serial + at: (NSPoint)surfacePoint +{ + NSPoint winPoint; + + if (win == NULL) + { + return; + } + + _destWlWindow = win; + _destOffer = offer; + _destEnterSerial = serial; + destWindow = GSWindowWithNumber(win->window_id); + + /* Make the inherited NSDraggingInfo getters report a usable operation mask. + * For a drag started in this process dragMask/dragPasteboard are already set + * by -dragImage:; guard the cross-process case so the mask is never empty. */ + operationMask = NSDragOperationEvery; + if (dragMask == NSDragOperationNone) + { + dragMask = NSDragOperationCopy | NSDragOperationMove + | NSDragOperationGeneric | NSDragOperationPrivate; + } + + winPoint = NSMakePoint(surfacePoint.x, win->height - surfacePoint.y); + dragPoint = winPoint; + + [self _postDrag: GSAppKitDraggingEnter at: winPoint window: win->window_id + action: NSDragOperationNone]; + [self _postDrag: GSAppKitDraggingUpdate at: winPoint window: win->window_id + action: dragMask]; +} + +- (void) _dragMovedTo: (NSPoint)surfacePoint +{ + NSPoint winPoint; + + if (_destWlWindow == NULL) + { + return; + } + winPoint = NSMakePoint(surfacePoint.x, _destWlWindow->height - surfacePoint.y); + dragPoint = winPoint; + [self _postDrag: GSAppKitDraggingUpdate + at: winPoint + window: _destWlWindow->window_id + action: dragMask]; +} + +- (void) _dragDropped +{ + if (_destWlWindow == NULL) + { + return; + } + [self _postDrag: GSAppKitDraggingDrop + at: dragPoint + window: _destWlWindow->window_id + action: NSDragOperationNone]; +} + +- (void) _dragExited +{ + if (_destWlWindow == NULL) + { + return; + } + [self _postDrag: GSAppKitDraggingExit + at: dragPoint + window: _destWlWindow->window_id + action: NSDragOperationNone]; + _destWlWindow = NULL; +} + +/* Called back by -[NSWindow sendEvent:] after it has run the view's dragging + * protocol: Status carries the operation the view will accept, Finished marks + * the drop as concluded. */ +- (void) postDragEvent: (NSEvent *)theEvent +{ + WaylandConfig *wlconfig = [self _wlconfig]; + GSAppKitSubtype subtype = (GSAppKitSubtype)[theEvent subtype]; + + if (subtype == GSAppKitDraggingStatus) + { + NSDragOperation action = [theEvent data2]; + + if (_destOffer != NULL) + { + if (action != NSDragOperationNone && [[dragPasteboard types] count] > 0) + { + NSString *type = [[dragPasteboard types] objectAtIndex: 0]; + NSString *mime = [mimesForType(type) objectAtIndex: 0]; + + wl_data_offer_set_actions(_destOffer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY + | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); + wl_data_offer_accept(_destOffer, _destEnterSerial, + [mime UTF8String]); + } + else + { + wl_data_offer_accept(_destOffer, _destEnterSerial, NULL); + } + if (wlconfig != NULL) + wl_display_flush(wlconfig->display); + } + } + else if (subtype == GSAppKitDraggingFinished) + { + if (_destOffer != NULL) + { + wl_data_offer_finish(_destOffer); + wl_data_offer_destroy(_destOffer); + _destOffer = NULL; + } + if (wlconfig != NULL) + wl_display_flush(wlconfig->display); + + /* For a drag that both started and ended in this process the compositor + * may not round-trip a data_source.dnd_finished, so release the modal + * loop here as well. */ + _destWlWindow = NULL; + _resultOperation = NSDragOperationCopy; + _dragActive = NO; + } +} + +@end + +/* Wire the drag view into the Wayland server. */ + +@implementation WaylandServer (DragAndDrop) + +- (WaylandConfig *) wlconfig +{ + return wlconfig; +} + +- (id ) dragInfo +{ + return [WaylandDragView sharedDragView]; +} + +@end diff --git a/Source/wayland/WaylandServer.m b/Source/wayland/WaylandServer.m index 5a09b371..6fd55ad2 100644 --- a/Source/wayland/WaylandServer.m +++ b/Source/wayland/WaylandServer.m @@ -132,6 +132,12 @@ NSDebugLog(@"wayland: found seat interface"); wl_seat_add_listener(wlconfig->seat, &seat_listener, wlconfig); } + else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) + { + wlconfig->data_device_manager + = wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3); + NSDebugLog(@"wayland: found data_device_manager interface"); + } #ifdef HAVE_EGL else if (strcmp(interface, wl_subcompositor_interface.name) == 0) { @@ -212,6 +218,19 @@ - (id)_initWaylandContext wl_display_dispatch(wlconfig->display); wl_display_roundtrip(wlconfig->display); + /* Once the seat and the data device manager are both bound we can create + * the data device used for drag and drop and clipboard transfers. */ + if (wlconfig->data_device_manager != NULL && wlconfig->seat != NULL + && wlconfig->data_device == NULL) + { + extern const struct wl_data_device_listener data_device_listener; + + wlconfig->data_device = wl_data_device_manager_get_data_device( + wlconfig->data_device_manager, wlconfig->seat); + wl_data_device_add_listener(wlconfig->data_device, + &data_device_listener, wlconfig); + } + if (!wlconfig->compositor) { [NSException raise:NSWindowServerCommunicationException