From 15c7d79e16a4b6203083a618787576801e701b5a Mon Sep 17 00:00:00 2001 From: Polar <343746874+PolarSeeker77@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:54:46 +0000 Subject: [PATCH 1/3] AppKit: run drags in-process when the backend has no drag manager The X11 backend has no NSDraggingManager, so -[NSWindow dragImage:...] returned at once without calling -draggedImage:endedAt:operation:. Chromium waits for that callback before the page takes mouse input again, so any HTML5 drag in a CEF/Electron window (the League client's emote wheel) left the page ignoring the mouse. NSWindow now runs the drag itself between the app's own windows, with the drag image under the pointer, and always ends it with the callback. X11Pasteboard sends the optional -pasteboardChangedOwner: only to owners that implement it (Chromium's drag source doesn't; the second drag crashed). NSView's default -prepareForDragOperation: returns YES, as AppKit does for destinations that don't implement it. --- patches/cocotron.patch | 427 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 392 insertions(+), 35 deletions(-) diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 83d64b3..9e67845 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -1377,7 +1377,7 @@ index c3f5d056..dbd93174 100644 @"NSTouchBarItemIdentifierCharacterPicker"; diff --git a/AppKit/NSView.m b/AppKit/NSView.m -index 8ac2bbdc..08197fbc 100644 +index 8ac2bbdc..a0fb4f37 100644 --- a/AppKit/NSView.m +++ b/AppKit/NSView.m @@ -46,6 +46,12 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ @@ -1590,11 +1590,47 @@ index 8ac2bbdc..08197fbc 100644 } [_subviews +@@ -2687,6 +2786,17 @@ static NSView *viewBeingPrinted = nil; + slideBack: (BOOL) slideBack + { + location = [self convertPoint: location toView: nil]; ++ if ([NSDraggingManager draggingManager] == nil) { ++ // darling-lol: no drag manager on X11; NSWindow runs the drag in-process. ++ [[self window] dragImage: image ++ at: location ++ offset: offset ++ event: event ++ pasteboard: pasteboard ++ source: source ++ slideBack: slideBack]; ++ return; ++ } + [[NSDraggingManager draggingManager] dragImage: image + at: location + offset: offset +@@ -2783,7 +2893,8 @@ static NSView *viewBeingPrinted = nil; + } + + - (BOOL) prepareForDragOperation: (id) sender { +- return NO; ++ // darling-lol: AppKit goes ahead when a destination doesn't implement this (Chromium). ++ return YES; + } + + - (BOOL) performDragOperation: (id) sender { diff --git a/AppKit/NSWindow.m b/AppKit/NSWindow.m -index d545120a..b2845478 100644 +index d545120a..79760f47 100644 --- a/AppKit/NSWindow.m +++ b/AppKit/NSWindow.m -@@ -45,6 +45,21 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@@ -27,6 +27,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#import + #import + #import + #import +@@ -45,6 +46,21 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #import #import #import @@ -1616,7 +1652,7 @@ index d545120a..b2845478 100644 const NSNotificationName NSWindowDidBecomeKeyNotification = @"NSWindowDidBecomeKeyNotification"; -@@ -204,6 +219,25 @@ NSInteger NSBitsPerPixelFromDepth(NSWindowDepth depth) { +@@ -204,6 +220,122 @@ NSInteger NSBitsPerPixelFromDepth(NSWindowDepth depth) { @end @@ -1638,11 +1674,108 @@ index d545120a..b2845478 100644 + } + return view; +} ++ ++/* darling-lol: the X11 backend has no NSDraggingManager, so drags run in-process ++ * (see -_localDragImage:...): this is the NSDraggingInfo handed to destinations. */ ++@interface _NSLocalDraggingInfo : NSObject { ++ @public ++ NSPasteboard *_pasteboard; ++ NSImage *_image; ++ id _source; ++ NSWindow *_destinationWindow; ++ NSPoint _screenPoint; ++ NSDragOperation _sourceMask; ++} ++@end ++ ++@implementation _NSLocalDraggingInfo ++ ++- (NSPasteboard *) draggingPasteboard { ++ return _pasteboard; ++} ++ ++- (NSDragOperation) draggingSourceOperationMask { ++ return _sourceMask; ++} ++ ++- (NSPoint) draggingLocation { ++ if (_destinationWindow == nil) ++ return _screenPoint; ++ return [_destinationWindow convertScreenToBase: _screenPoint]; ++} ++ ++- (NSImage *) draggedImage { ++ return _image; ++} ++ ++- (NSPoint) draggedImageLocation { ++ return [self draggingLocation]; ++} ++ ++- (NSWindow *) draggingDestinationWindow { ++ return _destinationWindow; ++} ++ ++- (id) draggingSource { ++ return _source; ++} ++ ++- (void) slideDraggedImageTo: (NSPoint) point { ++} ++ ++- (int) draggingSequenceNumber { ++ return 0; ++} ++ ++- (NSArray *) namesOfPromisedFilesDroppedAtDestination: (NSURL *) destination { ++ return nil; ++} ++ ++@end ++ ++// Shows the drag image under the pointer. Can't become key, so X11 makes it ++// override-redirect and the window manager leaves it alone. ++@interface _NSDragImageWindow : NSWindow ++@end ++ ++@implementation _NSDragImageWindow ++ ++- (BOOL) canBecomeKeyWindow { ++ return NO; ++} ++ ++- (BOOL) canBecomeMainWindow { ++ return NO; ++} ++ ++@end ++ ++/* One -nextEventMatchingMask: pass handles a single run loop source, and Chromium ++ * runs one task per pass: during a drag its drop-target replies fall behind. ++ * Run sources until idle (wait == 0) or until the deadline (wait > 0). */ ++static void _NSLocalDragRunPending(NSTimeInterval budget, BOOL wait) { ++ CFAbsoluteTime deadline = CFAbsoluteTimeGetCurrent() + budget; ++ for (;;) { ++ CFTimeInterval left = deadline - CFAbsoluteTimeGetCurrent(); ++ if (left <= 0) ++ break; ++ SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, wait ? MIN(left, 0.01) : 0, true); ++ if (!wait && result != kCFRunLoopRunHandledSource) ++ break; ++ } ++} ++ ++static BOOL _NSLocalDragTrace(void) { ++ static int trace = -1; ++ if (trace < 0) ++ trace = getenv("DARLING_DRAG_TRACE") != NULL; ++ return trace; ++} + @implementation NSWindow @synthesize identifier = _identifier; -@@ -218,7 +252,8 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -218,7 +350,8 @@ static BOOL _allowsAutomaticWindowTabbing; /* This method is Cococtron specific and can be override by subclasses, do not * change method name */ + (BOOL) hasMainMenuForStyleMask: (NSWindowStyleMask) styleMask { @@ -1652,7 +1785,7 @@ index d545120a..b2845478 100644 } /* This method is Cococtron specific and can be override by subclasses, do not -@@ -318,6 +353,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -318,6 +451,7 @@ static BOOL _allowsAutomaticWindowTabbing; [NSApplication sharedApplication]; } _styleMask = styleMask; @@ -1660,7 +1793,7 @@ index d545120a..b2845478 100644 _backingType = backing; _isDeferred = defer; -@@ -351,7 +387,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -351,7 +485,7 @@ static BOOL _allowsAutomaticWindowTabbing; [_backgroundView _setWindow: self]; [_backgroundView setNextResponder: self]; @@ -1669,7 +1802,7 @@ index d545120a..b2845478 100644 _contentView = [[NSView alloc] initWithFrame: contentViewFrame]; [_contentView setAutoresizesSubviews: YES]; [_contentView -@@ -399,6 +435,8 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -399,6 +533,8 @@ static BOOL _allowsAutomaticWindowTabbing; } - (void) dealloc { @@ -1678,7 +1811,7 @@ index d545120a..b2845478 100644 [[NSNotificationCenter defaultCenter] removeObserver: self]; [_childWindows release]; [_title release]; -@@ -934,6 +972,22 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -934,6 +1070,22 @@ static BOOL _allowsAutomaticWindowTabbing; return _animationContext; } @@ -1701,7 +1834,7 @@ index d545120a..b2845478 100644 - (void) setFrame: (NSRect) newFrame display: (BOOL) display animate: (BOOL) animate -@@ -944,8 +998,19 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -944,8 +1096,19 @@ static BOOL _allowsAutomaticWindowTabbing; _frame = newFrame; _makeSureIsOnAScreen = YES; @@ -1722,7 +1855,7 @@ index d545120a..b2845478 100644 if (didSize) { [self resetCursorRects]; -@@ -1025,7 +1090,34 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1025,7 +1188,34 @@ static BOOL _allowsAutomaticWindowTabbing; } - (void) setMovable: (BOOL) movable { @@ -1758,7 +1891,7 @@ index d545120a..b2845478 100644 } - (void) setBackingType: (NSBackingStoreType) value { -@@ -1234,6 +1326,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1234,6 +1424,7 @@ static BOOL _allowsAutomaticWindowTabbing; - (void) setIgnoresMouseEvents: (BOOL) value { _ignoresMouseEvents = value; @@ -1766,7 +1899,7 @@ index d545120a..b2845478 100644 } - (void) setAspectRatio: (NSSize) value { -@@ -1413,8 +1506,14 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1413,8 +1604,14 @@ static BOOL _allowsAutomaticWindowTabbing; } - (NSButton *) standardWindowButton: (NSWindowButton) value { @@ -1783,7 +1916,7 @@ index d545120a..b2845478 100644 } - (NSButtonCell *) defaultButtonCell { -@@ -1425,6 +1524,17 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1425,6 +1622,17 @@ static BOOL _allowsAutomaticWindowTabbing; return [_sheetContext sheet]; } @@ -1801,7 +1934,7 @@ index d545120a..b2845478 100644 - (id) windowController { return _windowController; } -@@ -1511,10 +1621,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1511,10 +1719,7 @@ static BOOL _allowsAutomaticWindowTabbing; return [_platformWindow isMiniaturized]; } @@ -1813,7 +1946,7 @@ index d545120a..b2845478 100644 - (BOOL) inLiveResize { return _isInLiveResize; -@@ -1561,6 +1668,117 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1561,6 +1766,117 @@ static BOOL _allowsAutomaticWindowTabbing; return point; } @@ -1931,7 +2064,7 @@ index d545120a..b2845478 100644 - (NSRect) frameRectForContentRect: (NSRect) contentRect { // hasMainMenu is an instance method so we can't just use the class method // frameRectForContentRect:styleMask: -@@ -1580,6 +1798,10 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1580,6 +1896,10 @@ static BOOL _allowsAutomaticWindowTabbing; } - (NSRect) contentRectForFrameRect: (NSRect) frameRect { @@ -1942,7 +2075,7 @@ index d545120a..b2845478 100644 NSRect result = [[NSDisplay currentDisplay] insetRect: frameRect forNativeWindowBorderWithStyle: [self styleMask]]; -@@ -2155,6 +2377,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2155,6 +2475,7 @@ static BOOL _allowsAutomaticWindowTabbing; - (void) orderWindow: (NSWindowOrderingMode) place relativeTo: (NSInteger) relativeTo { @@ -1950,7 +2083,7 @@ index d545120a..b2845478 100644 // The move notifications are sent under unknown conditions around // orderFront: in the Apple AppKit, we do them all the time here until it's // figured out. I suspect it is a side effect of off-screen windows being at -@@ -2208,6 +2431,8 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2208,6 +2529,8 @@ static BOOL _allowsAutomaticWindowTabbing; break; } @@ -1959,7 +2092,7 @@ index d545120a..b2845478 100644 [self postNotificationName: NSWindowDidMoveNotification]; } -@@ -2257,6 +2482,12 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2257,6 +2580,12 @@ static BOOL _allowsAutomaticWindowTabbing; } - (void) sendEvent: (NSEvent *) event { @@ -1972,7 +2105,7 @@ index d545120a..b2845478 100644 // Some events can cause our window to be destroyed // So make sure self lives at least through this current run loop... [[self retain] autorelease]; -@@ -2300,6 +2531,10 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2300,6 +2629,10 @@ static BOOL _allowsAutomaticWindowTabbing; case NSLeftMouseDown: { NSView *view = [_backgroundView hitTest: [event locationInWindow]]; @@ -1983,7 +2116,7 @@ index d545120a..b2845478 100644 if ([view acceptsFirstResponder]) { if ([view needsPanelToBecomeKey]) { -@@ -2307,6 +2542,27 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2307,6 +2640,27 @@ static BOOL _allowsAutomaticWindowTabbing; } } @@ -2011,7 +2144,7 @@ index d545120a..b2845478 100644 // Event goes to view, not first responder. [view mouseDown: event]; _mouseDownLocationInWindow = [event locationInWindow]; -@@ -2330,6 +2586,31 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2330,6 +2684,31 @@ static BOOL _allowsAutomaticWindowTabbing; _mouseDownLocationInWindow = NSMakePoint(NAN, NAN); break; @@ -2043,7 +2176,209 @@ index d545120a..b2845478 100644 case NSMouseMoved: { NSView *hit = [_backgroundView hitTest: [event locationInWindow]]; -@@ -2536,6 +2817,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2478,6 +2857,190 @@ static BOOL _allowsAutomaticWindowTabbing; + _draggedTypes = nil; + } + ++// The registered drag destination under screenPoint in this window, if any. ++- (NSView *) _localDragDestinationAt: (NSPoint) screenPoint types: (NSArray *) types { ++ NSView *root = [self contentView]; ++ while ([root superview] != nil) ++ root = [root superview]; ++ NSView *view = [root hitTest: [self convertScreenToBase: screenPoint]]; ++ while (view != nil && [[view registeredDraggedTypes] firstObjectCommonWithArray: types] == nil) ++ view = [view superview]; ++ return view; ++} ++ ++/* darling-lol: drag loop for backends without an NSDraggingManager (X11). Covers drags ++ * between this app's own windows; nothing crosses to other X clients. Like AppKit's, ++ * it returns once the drag is over and always ends with -draggedImage:endedAt:operation:, ++ * which Chromium waits for before the page takes mouse input again. */ ++- (void) _localDragImage: (NSImage *) image ++ at: (NSPoint) imageLocation ++ event: (NSEvent *) event ++ pasteboard: (NSPasteboard *) pasteboard ++ source: (id) source ++{ ++ _NSLocalDraggingInfo *info = [[_NSLocalDraggingInfo alloc] init]; ++ info->_pasteboard = pasteboard; ++ info->_image = image; ++ info->_source = source; ++ info->_sourceMask = NSDragOperationEvery; ++ if ([source respondsToSelector: @selector(draggingSourceOperationMaskForLocal:)]) ++ info->_sourceMask = [source draggingSourceOperationMaskForLocal: YES]; ++ ++ NSPoint screenPoint; ++ if (event != nil && [event window] != nil) ++ screenPoint = [[event window] convertBaseToScreen: [event locationInWindow]]; ++ else ++ screenPoint = [self convertBaseToScreen: [self mouseLocationOutsideOfEventStream]]; ++ ++ // The image keeps its offset from the pointer, as on macOS. ++ _NSDragImageWindow *imageWindow = nil; ++ NSPoint imageOffset = NSZeroPoint; ++ NSSize imageSize = [image size]; ++ if (imageSize.width >= 1 && imageSize.height >= 1) { ++ NSPoint imageOrigin = [self convertBaseToScreen: imageLocation]; ++ imageOffset = NSMakePoint(screenPoint.x - imageOrigin.x, screenPoint.y - imageOrigin.y); ++ imageWindow = [[_NSDragImageWindow alloc] ++ initWithContentRect: NSMakeRect(imageOrigin.x, imageOrigin.y, imageSize.width, imageSize.height) ++ styleMask: NSBorderlessWindowMask ++ backing: NSBackingStoreBuffered ++ defer: NO]; ++ [imageWindow setReleasedWhenClosed: NO]; ++ [imageWindow setIgnoresMouseEvents: YES]; ++ [imageWindow setLevel: NSPopUpMenuWindowLevel]; ++ [imageWindow setHasShadow: NO]; ++ NSImageView *imageView = [[NSImageView alloc] ++ initWithFrame: NSMakeRect(0, 0, imageSize.width, imageSize.height)]; ++ [imageView setImage: image]; ++ [imageView setImageScaling: NSImageScaleNone]; ++ [imageWindow setContentView: imageView]; ++ [imageView release]; ++ [imageWindow orderFront: nil]; ++ } ++ ++ NSArray *types = [pasteboard types]; ++ id destination = nil; ++ NSDragOperation operation = NSDragOperationNone; ++ BOOL dropped = NO; ++ BOOL moved = YES; ++ const char *ending = "release"; ++ ++ if (_NSLocalDragTrace()) ++ NSLog(@"DRAGTRACE start at %.0f,%.0f window=%@ image=%.0fx%.0f mask=0x%lx types=%@", ++ screenPoint.x, screenPoint.y, [self className], imageSize.width, imageSize.height, ++ (unsigned long) info->_sourceMask, types); ++ ++ for (;;) { ++ if (moved) { ++ NSWindow *window = nil; ++ if (NSPointInRect(screenPoint, [self frame])) ++ window = self; ++ else { ++ for (NSWindow *candidate in [NSApp orderedWindows]) { ++ if (candidate != imageWindow && [candidate isVisible] && ++ NSPointInRect(screenPoint, [candidate frame])) { ++ window = candidate; ++ break; ++ } ++ } ++ } ++ info->_screenPoint = screenPoint; ++ info->_destinationWindow = window; ++ NSView *view = [window _localDragDestinationAt: screenPoint types: types]; ++ ++ if (view != destination) { ++ if ([destination respondsToSelector: @selector(draggingExited:)]) ++ [destination draggingExited: info]; ++ destination = view; ++ operation = NSDragOperationNone; ++ if ([destination respondsToSelector: @selector(draggingEntered:)]) ++ operation = [destination draggingEntered: info]; ++ if (_NSLocalDragTrace()) ++ NSLog(@"DRAGTRACE destination %@ at %.0f,%.0f operation=0x%lx", [view className], ++ screenPoint.x, screenPoint.y, (unsigned long) operation); ++ } else if ([destination respondsToSelector: @selector(draggingUpdated:)]) { ++ operation = [destination draggingUpdated: info]; ++ } ++ [imageWindow setFrameOrigin: NSMakePoint(screenPoint.x - imageOffset.x, ++ screenPoint.y - imageOffset.y)]; ++ if ([source respondsToSelector: @selector(draggedImage:movedTo:)]) ++ [source draggedImage: image movedTo: screenPoint]; ++ _NSLocalDragRunPending(0.008, NO); ++ } ++ if (dropped) ++ break; ++ ++ // Short timeout: the wait below doesn't wake for run loop sources, and ++ // a release delivered before the loop started never shows up as an event. ++ NSEvent *next = [NSApp nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask | ++ NSMouseMovedMask | NSKeyDownMask ++ untilDate: [NSDate dateWithTimeIntervalSinceNow: 0.05] ++ inMode: NSDefaultRunLoopMode ++ dequeue: YES]; ++ moved = NO; ++ if (next == nil) { ++ NSUInteger buttons = [NSEvent pressedMouseButtons]; ++ if ((buttons & 1) == 0) { ++ dropped = moved = YES; ++ ending = "button up, no event"; ++ } ++ continue; ++ } ++ if ([next type] == NSKeyDown) { ++ if ([next keyCode] == 53 || [[next characters] isEqual: @"\033"]) { ++ ending = "escape"; ++ break; ++ } ++ continue; ++ } ++ if ([next window] != nil) ++ screenPoint = [[next window] convertBaseToScreen: [next locationInWindow]]; ++ else ++ screenPoint = [next locationInWindow]; ++ moved = YES; ++ if (_NSLocalDragTrace()) ++ NSLog(@"DRAGTRACE event type=%d window=%@ at %.0f,%.0f", (int) [next type], ++ [[next window] className], screenPoint.x, screenPoint.y); ++ if ([next type] == NSLeftMouseUp) ++ dropped = YES; ++ } ++ ++ [imageWindow orderOut: nil]; ++ [imageWindow release]; ++ ++ // The operation for the release point arrives asynchronously (Chromium): ++ // let it come in, then ask again. ++ if (dropped && [destination respondsToSelector: @selector(draggingUpdated:)]) { ++ _NSLocalDragRunPending(0.1, YES); ++ operation = [destination draggingUpdated: info]; ++ if (_NSLocalDragTrace()) ++ NSLog(@"DRAGTRACE release operation=0x%lx", (unsigned long) operation); ++ } ++ ++ BOOL performed = NO; ++ if (dropped && destination != nil && operation != NSDragOperationNone) { ++ performed = YES; ++ if ([destination respondsToSelector: @selector(prepareForDragOperation:)]) ++ performed = [destination prepareForDragOperation: info]; ++ if (performed) ++ performed = [destination respondsToSelector: @selector(performDragOperation:)] && ++ [destination performDragOperation: info]; ++ if (performed && [destination respondsToSelector: @selector(concludeDragOperation:)]) ++ [destination concludeDragOperation: info]; ++ } else if ([destination respondsToSelector: @selector(draggingExited:)]) { ++ [destination draggingExited: info]; ++ } ++ if (!performed) ++ operation = NSDragOperationNone; ++ ++ if (_NSLocalDragTrace()) ++ NSLog(@"DRAGTRACE end (%s) at %.0f,%.0f destination=%@ performed=%d operation=0x%lx", ending, ++ screenPoint.x, screenPoint.y, [destination className], performed, (unsigned long) operation); ++ ++ if ([source respondsToSelector: @selector(draggedImage:endedAt:operation:)]) ++ [source draggedImage: image endedAt: screenPoint operation: operation]; ++ [info release]; ++} ++ + - (void) dragImage: (NSImage *) image + at: (NSPoint) location + offset: (NSSize) offset +@@ -2486,6 +3049,10 @@ static BOOL _allowsAutomaticWindowTabbing; + source: (id) source + slideBack: (BOOL) slideBack + { ++ if ([NSDraggingManager draggingManager] == nil) { ++ [self _localDragImage: image at: location event: event pasteboard: pasteboard source: source]; ++ return; ++ } + [[NSDraggingManager draggingManager] dragImage: image + at: location + offset: offset +@@ -2536,6 +3103,7 @@ static BOOL _allowsAutomaticWindowTabbing; } else { [[self platformWindow] hideWindow]; } @@ -2051,7 +2386,7 @@ index d545120a..b2845478 100644 } - (void) orderFront: (id) sender { -@@ -3064,6 +3346,13 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3064,6 +3632,13 @@ static BOOL _allowsAutomaticWindowTabbing; if (didSize) { // Don't redraw everything unless we really have to. [_backgroundView setFrameSize: _frame.size]; @@ -2065,7 +2400,7 @@ index d545120a..b2845478 100644 [_backgroundView setNeedsDisplay: YES]; [self resetCursorRects]; [self saveFrameUsingName: _autosaveFrameName]; -@@ -3260,8 +3549,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3260,8 +3835,7 @@ static BOOL _allowsAutomaticWindowTabbing; mouseWasInside && !mouseIsInside) { [exited addObject: area]; } @@ -2075,7 +2410,7 @@ index d545120a..b2845478 100644 [moved addObject: area]; } if (options & NSTrackingCursorUpdate && !mouseWasInside && -@@ -3459,7 +3747,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3459,7 +4033,7 @@ static BOOL _allowsAutomaticWindowTabbing; } - (CGSubWindow *) _createSubWindowWithFrame: (CGRect) frame { @@ -4130,7 +4465,7 @@ index 411903cc..4b196afd 100644 NSMutableDictionary *_typeToData; NSMutableDictionary> diff --git a/AppKit/X11.backend/X11Pasteboard.m b/AppKit/X11.backend/X11Pasteboard.m -index 0e91a5d1..1e4cec1b 100644 +index 0e91a5d1..9feeffa1 100644 --- a/AppKit/X11.backend/X11Pasteboard.m +++ b/AppKit/X11.backend/X11Pasteboard.m @@ -18,6 +18,8 @@ along with Darling. If not, see . @@ -4161,17 +4496,39 @@ index 0e91a5d1..1e4cec1b 100644 if (_typeToData != nil) { // Already an owner. return; -@@ -93,7 +94,8 @@ static NSMutableDictionary *nameToPboard; +@@ -78,12 +79,19 @@ static NSMutableDictionary *nameToPboard; + _changeCount++; + } + ++// pasteboardChangedOwner: is optional (Chromium's drag source lacks it); once per owner. ++- (void) notifyOwnersChanged { ++ NSArray *owners = [[NSSet setWithArray: [_typeToOwner allValues]] allObjects]; ++ for (id owner in owners) { ++ if ([owner respondsToSelector: @selector(pasteboardChangedOwner:)]) ++ [owner pasteboardChangedOwner: self]; ++ } ++} ++ + - (NSInteger) clearContents { + [self ensureSelectionOwner]; + +- for (id owner in [_typeToOwner allValues]) { +- [owner pasteboardChangedOwner: self]; +- } ++ [self notifyOwnersChanged]; + [_typeToOwner removeAllObjects]; + [_typeToData removeAllObjects]; + +@@ -93,7 +101,7 @@ static NSMutableDictionary *nameToPboard; - (void) giveUpSelectionOwner { _changeCount++; - [self clearContents]; -+ for (id owner in [_typeToOwner allValues]) -+ [owner pasteboardChangedOwner: self]; ++ [self notifyOwnersChanged]; [_typeToOwner release]; _typeToOwner = nil; -@@ -115,12 +117,47 @@ static NSMutableDictionary *nameToPboard; +@@ -115,12 +123,47 @@ static NSMutableDictionary *nameToPboard; [super dealloc]; } @@ -4220,7 +4577,7 @@ index 0e91a5d1..1e4cec1b 100644 } + (NSArray *) targetsForType: (NSPasteboardType) type { -@@ -177,7 +214,7 @@ static NSMutableDictionary *nameToPboard; +@@ -177,7 +220,7 @@ static NSMutableDictionary *nameToPboard; } - (NSData *) dataForType: (NSPasteboardType) type { @@ -4229,7 +4586,7 @@ index 0e91a5d1..1e4cec1b 100644 if (_typeToOwner != nil) { // We're the owner; just fetch the data directly. id owner = _typeToOwner[type]; -@@ -185,25 +222,26 @@ static NSMutableDictionary *nameToPboard; +@@ -185,25 +228,26 @@ static NSMutableDictionary *nameToPboard; return _typeToData[type]; } @@ -4270,7 +4627,7 @@ index 0e91a5d1..1e4cec1b 100644 NSStringEncoding encoding = NSUnicodeStringEncoding; if ([type isEqual: NSStringPboardType]) { -@@ -252,8 +290,37 @@ static NSMutableDictionary *nameToPboard; +@@ -252,8 +296,37 @@ static NSMutableDictionary *nameToPboard; } - (NSArray *) types { From 7eabb937ec0e28db793e40d8f8447bdb60637d07 Mon Sep 17 00:00:00 2001 From: Polar <343746874+PolarSeeker77@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:08:26 +0000 Subject: [PATCH 2/3] AppKit: retain the drag destination and its window during a local drag The drag loop runs run loop sources and main queue blocks between moves, and Chromium can tear down a web view or close a window there. Hold both until the drag ends, after -draggedImage:endedAt:operation:. --- patches/cocotron.patch | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 9e67845..9c1f0ae 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -2176,7 +2176,7 @@ index d545120a..79760f47 100644 case NSMouseMoved: { NSView *hit = [_backgroundView hitTest: [event locationInWindow]]; -@@ -2478,6 +2857,190 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2478,6 +2857,197 @@ static BOOL _allowsAutomaticWindowTabbing; _draggedTypes = nil; } @@ -2267,13 +2267,17 @@ index d545120a..79760f47 100644 + } + } + info->_screenPoint = screenPoint; -+ info->_destinationWindow = window; ++ if (info->_destinationWindow != window) { ++ [info->_destinationWindow release]; ++ info->_destinationWindow = [window retain]; ++ } + NSView *view = [window _localDragDestinationAt: screenPoint types: types]; + + if (view != destination) { + if ([destination respondsToSelector: @selector(draggingExited:)]) + [destination draggingExited: info]; -+ destination = view; ++ [destination release]; ++ destination = [view retain]; + operation = NSDragOperationNone; + if ([destination respondsToSelector: @selector(draggingEntered:)]) + operation = [destination draggingEntered: info]; @@ -2361,13 +2365,16 @@ index d545120a..79760f47 100644 + + if ([source respondsToSelector: @selector(draggedImage:endedAt:operation:)]) + [source draggedImage: image endedAt: screenPoint operation: operation]; ++ [destination release]; ++ [info->_destinationWindow release]; ++ info->_destinationWindow = nil; + [info release]; +} + - (void) dragImage: (NSImage *) image at: (NSPoint) location offset: (NSSize) offset -@@ -2486,6 +3049,10 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2486,6 +3056,10 @@ static BOOL _allowsAutomaticWindowTabbing; source: (id) source slideBack: (BOOL) slideBack { @@ -2378,7 +2385,7 @@ index d545120a..79760f47 100644 [[NSDraggingManager draggingManager] dragImage: image at: location offset: offset -@@ -2536,6 +3103,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2536,6 +3110,7 @@ static BOOL _allowsAutomaticWindowTabbing; } else { [[self platformWindow] hideWindow]; } @@ -2386,7 +2393,7 @@ index d545120a..79760f47 100644 } - (void) orderFront: (id) sender { -@@ -3064,6 +3632,13 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3064,6 +3639,13 @@ static BOOL _allowsAutomaticWindowTabbing; if (didSize) { // Don't redraw everything unless we really have to. [_backgroundView setFrameSize: _frame.size]; @@ -2400,7 +2407,7 @@ index d545120a..79760f47 100644 [_backgroundView setNeedsDisplay: YES]; [self resetCursorRects]; [self saveFrameUsingName: _autosaveFrameName]; -@@ -3260,8 +3835,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3260,8 +3842,7 @@ static BOOL _allowsAutomaticWindowTabbing; mouseWasInside && !mouseIsInside) { [exited addObject: area]; } @@ -2410,7 +2417,7 @@ index d545120a..79760f47 100644 [moved addObject: area]; } if (options & NSTrackingCursorUpdate && !mouseWasInside && -@@ -3459,7 +4033,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3459,7 +4040,7 @@ static BOOL _allowsAutomaticWindowTabbing; } - (CGSubWindow *) _createSubWindowWithFrame: (CGRect) frame { From 8cad24c7062813fa0fe43605e11b54b6246eb2ea Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:30:15 +0200 Subject: [PATCH 3/3] Add standalone X11 drag-and-drop check for PR #63 Co-Authored-By: Claude Sonnet 5.5 --- docs/TESTING.md | 6 + tools/build-riot-x11-drag-check.py | 21 +++ tools/riot-x11-drag-check.m | 276 +++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100755 tools/build-riot-x11-drag-check.py create mode 100644 tools/riot-x11-drag-check.m diff --git a/docs/TESTING.md b/docs/TESTING.md index 2fdedef..b1d7df4 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -129,6 +129,12 @@ With clients closed, run `build/riot-client-resize-check --handoff` through Darl The probe cycles through 1280×720, 1600×900, 1024×576 and 1280×720. Each stage changes the frame color and samples its own X11 window near the bottom-right corner, beyond the original frame's extent when growing. It waits up to 2.5 seconds for presentation and prints `RESIZE_PIXEL ... pass=1` for every stage. This verifies presented pixels rather than only view properties. Success does not reproduce League's own resize callbacks; the reported League UI failure remains open, with the opt-in [geometry trace](TROUBLESHOOTING.md#language-dropdown-closes-riot-or-client-resize-leaves-a-clipped-ui) needed to locate the remaining mismatch. +### X11 drag and drop + +`python3 build/runtime/tools/build-riot-x11-drag-check.py` builds an independent AppKit check for the in-process drag loop that the X11 backend uses (`-[NSWindow dragImage:at:offset:event:pasteboard:source:slideBack:]`). It needs the cocotron AppKit built with the drag-and-drop patch. With clients closed, run `build/riot-x11-drag-check` through Darling with the same framework/library overrides as the other probes. It opens its own source and destination windows, posts synthetic events to its own application queue, and does not inject X11 input or touch other clients. + +It prints `PASS` or `FAIL` for seven cases: a drop onto a destination without `prepareForDragOperation:` (the default must proceed), a drag ending outside any destination, Escape cancel, two drags back to back with a source lacking `pasteboardChangedOwner:`, main-queue blocks and run loop timers firing during the drag, a mouse-down reaching a view afterwards, and a destination view removed from its window mid-drag. Each drag must call `draggedImage:endedAt:operation:` exactly once. The process exits nonzero on any failure, and a case that makes no progress for 20 seconds is reported as a hang, which is the original failure mode. Success covers the AppKit drag loop only; it does not exercise Chromium's own drag source. + ## Legacy operating-system version query ```sh diff --git a/tools/build-riot-x11-drag-check.py b/tools/build-riot-x11-drag-check.py new file mode 100755 index 0000000..9eea4b8 --- /dev/null +++ b/tools/build-riot-x11-drag-check.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Build an owned AppKit check for the in-process X11 drag-and-drop loop.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/cocotron/AppKit' +flags = {} +for line in (folder / 'CMakeFiles/X11_backend.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +obj = root / 'build/riot-x11-drag-check.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-fblocks', '-Wno-macro-redefined', '-c', str(root / 'tools/riot-x11-drag-check.m'), + '-o', str(obj)], check=True) +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lobjc', + '-framework', 'AppKit', '-framework', 'CoreFoundation', '-framework', 'Foundation', + '-o', str(root / 'build/riot-x11-drag-check')], check=True) diff --git a/tools/riot-x11-drag-check.m b/tools/riot-x11-drag-check.m new file mode 100644 index 0000000..2e7a841 --- /dev/null +++ b/tools/riot-x11-drag-check.m @@ -0,0 +1,276 @@ +/* Standalone check for the in-process X11 drag loop (-[NSWindow _localDragImage:...]). + * Opens its own windows, drives -dragImage:at:offset:event:pasteboard:source:slideBack: + * with synthetic NSEvents posted to this app's own queue, and asserts the outcome. + * It injects no X11 input and touches no other client. A hang is reported as FAIL. */ +#import +#include +#include +#include +#include +#include +#include + +#define CASE_TIMEOUT_SECONDS 20.0 + +@interface DragLog : NSObject { + @public + int entered, updated, exited, prepared, performed, concluded, ended, mouseDowns; + NSDragOperation endOperation; + BOOL removeWhenUpdated; +} +@end +@implementation DragLog @end + +@interface NSView (DragCheckDefaults) +- (BOOL)prepareForDragOperation:(id)sender; +@end + +/* Destination without -prepareForDragOperation:, so the NSView default applies. */ +@interface DropView : NSView { @public DragLog *log; } +@end +@implementation DropView +- (NSDragOperation)draggingEntered:(id)s { log->entered++; return NSDragOperationCopy; } +- (NSDragOperation)draggingUpdated:(id)s { log->updated++; return NSDragOperationCopy; } +- (void)draggingExited:(id)s { log->exited++; } +- (BOOL)performDragOperation:(id)s { log->performed++; return YES; } +- (void)concludeDragOperation:(id)s { log->concluded++; } +- (void)mouseDown:(NSEvent *)e { log->mouseDowns++; } +@end + +/* Source that, like Chromium's, does not implement -pasteboardChangedOwner:. */ +@interface DragSource : NSObject { @public DragLog *log; } +@end +@implementation DragSource +- (NSDragOperation)draggingSourceOperationMask { return NSDragOperationEvery; } +- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)local { return NSDragOperationEvery; } +- (void)draggedImage:(NSImage *)image endedAt:(NSPoint)p operation:(NSDragOperation)op { + log->ended++; log->endOperation = op; +} +@end + +static const char *currentCase = "startup"; +static volatile time_t caseStart; +static int failures = 0; + +static void *watchdog(void *unused) { + for (;;) { + usleep(100000); + if (time(NULL) - caseStart > CASE_TIMEOUT_SECONDS) { + printf("FAIL %s: no progress for %.0fs (drag hang)\n", currentCase, CASE_TIMEOUT_SECONDS); + fflush(stdout); + _exit(1); + } + } + return NULL; +} +static void beginCase(const char *name) { currentCase = name; caseStart = time(NULL); } +static void expect(BOOL ok, const char *what) { + if (!ok) { failures++; printf(" FAIL %s: %s\n", currentCase, what); } +} +static void endCase(int failuresBefore) { + printf("%s %s\n", failures == failuresBefore ? "PASS" : "FAIL", currentCase); + fflush(stdout); +} + +static void pump(double seconds) { + NSDate *end = [NSDate dateWithTimeIntervalSinceNow:seconds]; + while ([end timeIntervalSinceNow] > 0) { + NSEvent *e = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode dequeue:YES]; + if (e) [NSApp sendEvent:e]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.005]]; + } +} + +static NSWindow *makeWindow(NSString *title, NSRect frame) { + NSWindow *w = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered defer:NO]; + [w setTitle:title]; + [w setReleasedWhenClosed:NO]; + [w makeKeyAndOrderFront:nil]; + return w; +} + +/* Destination window holding a registered DropView that only its superview owns. */ +static NSWindow *makeDestination(DragLog *log, DropView **viewOut) { + NSWindow *w = makeWindow(@"Darling drag check destination", NSMakeRect(500, 200, 240, 240)); + DropView *v = [[DropView alloc] initWithFrame:NSMakeRect(20, 20, 200, 200)]; + v->log = log; + [v registerForDraggedTypes:[NSArray arrayWithObject:NSStringPboardType]]; + [[w contentView] addSubview:v]; + if (viewOut) *viewOut = v; + [v release]; + return w; +} + +static NSEvent *mouse(NSEventType type, NSWindow *w, NSPoint p) { + return [NSEvent mouseEventWithType:type location:p modifierFlags:0 + timestamp:[[NSProcessInfo processInfo] systemUptime] + windowNumber:[w windowNumber] context:nil eventNumber:0 + clickCount:1 pressure:1.0]; +} +static NSEvent *escapeKey(NSWindow *w) { + return [NSEvent keyEventWithType:NSKeyDown location:NSMakePoint(0, 0) modifierFlags:0 + timestamp:[[NSProcessInfo processInfo] systemUptime] + windowNumber:[w windowNumber] context:nil characters:@"\033" + charactersIgnoringModifiers:@"\033" isARepeat:NO keyCode:53]; +} +static void post(NSEvent *e) { [NSApp postEvent:e atStart:NO]; } + +static NSPasteboard *declarePasteboard(id owner) { + NSPasteboard *pb = [NSPasteboard pasteboardWithName:NSDragPboard]; + [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:owner]; + [pb setString:@"darling drag check" forType:NSStringPboardType]; + return pb; +} + +static void runDrag(NSWindow *sourceWindow, DragSource *source) { + NSPasteboard *pb = declarePasteboard(source); + NSImage *image = [[[NSImage alloc] initWithSize:NSMakeSize(32, 32)] autorelease]; + NSEvent *down = mouse(NSLeftMouseDown, sourceWindow, NSMakePoint(50, 50)); + [sourceWindow dragImage:image at:NSMakePoint(50, 50) offset:NSZeroSize event:down + pasteboard:pb source:source slideBack:YES]; +} + +static NSPoint inside = {120, 120}; /* inside DropView, in destination window base coordinates */ +static NSPoint outside = {-4000, -4000}; + +int main(int argc, const char **argv) { + @autoreleasepool { + [NSApplication sharedApplication]; + pthread_t thread; + beginCase("startup"); + pthread_create(&thread, NULL, watchdog, NULL); + NSWindow *sourceWindow = makeWindow(@"Darling drag check source", NSMakeRect(100, 200, 240, 240)); + pump(0.3); + int before; + + /* 1. Drop onto a destination that lacks prepareForDragOperation:. */ + { + beginCase("1 drop onto destination"); before = failures; + DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log; + DropView *view; NSWindow *dw = makeDestination(log, &view); pump(0.2); + expect([view prepareForDragOperation:nil] == YES, "NSView default prepareForDragOperation: is not YES"); + NSWindow *dest = dw; + post(mouse(NSLeftMouseDragged, dest, inside)); + post(mouse(NSLeftMouseDragged, dest, NSMakePoint(inside.x + 2, inside.y))); + post(mouse(NSLeftMouseUp, dest, inside)); + runDrag(sourceWindow, src); + expect(log->entered == 1, "draggingEntered: not called exactly once"); + expect(log->performed == 1, "performDragOperation: not called exactly once"); + expect(log->concluded == 1, "concludeDragOperation: not called exactly once"); + expect(log->ended == 1, "draggedImage:endedAt:operation: not called exactly once"); + expect(log->endOperation != NSDragOperationNone, "end operation is None after a successful drop"); + [dw close]; [dw release]; [src release]; [log release]; + endCase(before); + } + + /* 2. Drag leaves the destination and ends outside any window. */ + { + beginCase("2 end outside destination"); before = failures; + DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log; + NSWindow *dw = makeDestination(log, NULL); pump(0.2); + post(mouse(NSLeftMouseDragged, dw, inside)); + post(mouse(NSLeftMouseDragged, sourceWindow, outside)); + post(mouse(NSLeftMouseUp, sourceWindow, outside)); + runDrag(sourceWindow, src); + expect(log->entered == 1, "draggingEntered: not called exactly once"); + expect(log->exited >= 1, "draggingExited: not called"); + expect(log->performed == 0, "performDragOperation: called for a drop outside"); + expect(log->ended == 1, "draggedImage:endedAt:operation: not called exactly once"); + expect(log->endOperation == NSDragOperationNone, "end operation is not None"); + [dw close]; [dw release]; [src release]; [log release]; + endCase(before); + } + + /* 3. Escape cancels while over the destination. */ + { + beginCase("3 escape cancel"); before = failures; + DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log; + NSWindow *dw = makeDestination(log, NULL); pump(0.2); + post(mouse(NSLeftMouseDragged, dw, inside)); + post(escapeKey(sourceWindow)); + runDrag(sourceWindow, src); + expect(log->performed == 0, "performDragOperation: called after Escape"); + expect(log->ended == 1, "draggedImage:endedAt:operation: not called exactly once"); + expect(log->endOperation == NSDragOperationNone, "end operation is not None"); + [dw close]; [dw release]; [src release]; [log release]; + endCase(before); + } + + /* 4. Two drags back to back with sources lacking pasteboardChangedOwner:. */ + { + beginCase("4 back-to-back drags"); before = failures; + DragLog *log1 = [[DragLog alloc] init], *log2 = [[DragLog alloc] init]; + DragSource *s1 = [[DragSource alloc] init], *s2 = [[DragSource alloc] init]; + s1->log = log1; s2->log = log2; + expect(![s1 respondsToSelector:@selector(pasteboardChangedOwner:)], "fixture source implements pasteboardChangedOwner:"); + NSWindow *dw = makeDestination(log1, NULL); pump(0.2); + post(mouse(NSLeftMouseDragged, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside)); + runDrag(sourceWindow, s1); + post(mouse(NSLeftMouseDragged, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside)); + runDrag(sourceWindow, s2); /* declaring new owner notifies s1, which must be tolerated */ + expect(log1->ended == 1, "first drag did not end exactly once"); + expect(log2->ended == 1, "second drag did not end exactly once"); + expect(log1->performed == 2, "destination did not receive both drops"); + [dw close]; [dw release]; [s1 release]; [s2 release]; [log1 release]; [log2 release]; + endCase(before); + } + + /* 5. Main queue and run loop timers are serviced during the drag. */ + { + beginCase("5 main queue and timer serviced"); before = failures; + DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log; + NSWindow *dw = makeDestination(log, NULL); pump(0.2); + __block BOOL queueFired = NO, timerFired = NO, inDrag = YES; + __block BOOL queueDuring = NO, timerDuring = NO; + CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(NULL, CFAbsoluteTimeGetCurrent() + 0.03, 0, 0, 0, + ^(CFRunLoopTimerRef t) { timerFired = YES; timerDuring = inDrag; }); + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopDefaultMode); + dispatch_async(dispatch_get_main_queue(), ^{ queueFired = YES; queueDuring = inDrag; }); + post(mouse(NSLeftMouseDragged, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside)); + runDrag(sourceWindow, src); + inDrag = NO; + expect(queueDuring, queueFired ? "dispatch_async block ran only after the drag returned" : "dispatch_async block never ran"); + expect(timerDuring, timerFired ? "run loop timer fired only after the drag returned" : "run loop timer never fired"); + CFRunLoopTimerInvalidate(timer); CFRelease(timer); + expect(log->ended == 1, "drag did not end exactly once"); + [dw close]; [dw release]; [src release]; [log release]; + endCase(before); + } + + /* 6. Mouse input reaches views normally after the drags. */ + { + beginCase("6 input after drags"); before = failures; + DragLog *log = [[DragLog alloc] init]; + DropView *view; NSWindow *dw = makeDestination(log, &view); pump(0.2); + post(mouse(NSLeftMouseDown, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside)); + pump(0.3); + expect(log->mouseDowns == 1, "mouse-down was not delivered to the view after drags"); + [dw close]; [dw release]; [log release]; + endCase(before); + } + + /* 7. Destination view removed (and window closed) from a main-queue block mid-drag. */ + { + beginCase("7 destination removed mid-drag"); before = failures; + DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log; + DropView *view; NSWindow *dw = makeDestination(log, &view); pump(0.2); + post(mouse(NSLeftMouseDragged, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside)); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + [view removeFromSuperview]; /* the drag loop must have retained it */ + [dw orderOut:nil]; + [dw close]; + }); + runDrag(sourceWindow, src); + expect(log->ended == 1, "drag did not end exactly once after the destination went away"); + pump(0.2); + [dw release]; [src release]; [log release]; + endCase(before); + } + + [sourceWindow close]; [sourceWindow release]; + printf("%s x11 drag check (%d failure%s)\n", failures ? "FAIL" : "PASS", failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; + } +}