From 768e5976e4b7ab5a71dd9ec870ebb1bed4c68189 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 15 Dec 2023 15:34:54 -0600 Subject: [PATCH] app: [macOS] implement non-blocking window resizing Despite the option to control the main thread event loop, some gestures still block the event loop until completion. This change disables the blocking resize gestures and implements a non-blocking replacement. A complete replacement is left for future work, or the implementation of https://github.com/golang/go/issues/64755. Signed-off-by: Elias Naur --- app/os_macos.m | 221 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 212 insertions(+), 9 deletions(-) diff --git a/app/os_macos.m b/app/os_macos.m index 0a3f62629..768d109fe 100644 --- a/app/os_macos.m +++ b/app/os_macos.m @@ -69,6 +69,212 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]); } +@interface GioApplication: NSApplication +@end + +// Variables for tracking resizes. +static struct { + NSPoint dir; + NSEvent *lastMouseDown; + NSPoint off; +} resizeState = {}; + +static NSBitmapImageRep *nsImageBitmap(NSImage *img) { + NSArray *reps = img.representations; + if ([reps count] == 0) { + return nil; + } + NSImageRep *rep = reps[0]; + if (![rep isKindOfClass:[NSBitmapImageRep class]]) { + return nil; + } + return (NSBitmapImageRep *)rep; +} + +static NSCursor *lookupPrivateNSCursor(SEL name) { + if (![NSCursor respondsToSelector:name]) { + return nil; + } + id obj = [NSCursor performSelector:name]; + if (![obj isKindOfClass:[NSCursor class]]) { + return nil; + } + return (NSCursor *)obj; +} + +static BOOL isEqualNSCursor(NSCursor *c1, SEL name2) { + NSCursor *c2 = lookupPrivateNSCursor(name2); + if (c2 == nil || !NSEqualPoints(c1.hotSpot, c2.hotSpot)) { + return NO; + } + NSImage *img1 = c1.image; + NSImage *img2 = c2.image; + if (!NSEqualSizes(img1.size, img2.size)) { + return NO; + } + NSBitmapImageRep *bit1 = nsImageBitmap(img1); + NSBitmapImageRep *bit2 = nsImageBitmap(img2); + if (bit1 == nil || bit2 == nil) { + return NO; + } + NSInteger n1 = bit1.numberOfPlanes*bit1.bytesPerPlane; + NSInteger n2 = bit1.numberOfPlanes*bit1.bytesPerPlane; + if (n1 != n2) { + return NO; + } + if (memcmp(bit1.bitmapData, bit2.bitmapData, n1) != 0) { + return NO; + } + return YES; +} + +@implementation GioApplication +- (NSEvent *)nextEventMatchingMask:(NSEventMask)mask + untilDate:(NSDate *)expiration + inMode:(NSRunLoopMode)mode + dequeue:(BOOL)deqFlag { + if ([mode isEqualToString:NSEventTrackingRunLoopMode]) { + NSEvent *l = resizeState.lastMouseDown; + if (l != nil) { + //lastMouseDown = nil; + NSCursor *cur = [NSCursor currentSystemCursor]; + NSPoint dir = {}; + NSPoint off = {}; + NSSize wsz = [l window].frame.size; + NSPoint center = NSMakePoint(wsz.width/2, wsz.height/2); + NSPoint p = [l locationInWindow]; + if (p.x >= center.x) { + dir.x = 1; + off.x = p.x - wsz.width; + } else { + dir.x = -1; + off.x = p.x; + } + if (p.y >= center.y) { + dir.y = 1; + off.y = p.y - wsz.height; + } else { + dir.y = -1; + off.y = p.y; + } + // The button down coordinate distinguish the four quadrants. Use the + // cursor image to determine the precise direction. + SEL nw = @selector(_windowResizeNorthWestCursor); + SEL n = @selector(_windowResizeNorthCursor); + SEL ne = @selector(_windowResizeNorthEastCursor); + SEL e = @selector(_windowResizeEastCursor); + SEL se = @selector(_windowResizeSouthEastCursor); + SEL s = @selector(_windowResizeSouthCursor); + SEL sw = @selector(_windowResizeSouthWestCursor); + SEL w = @selector(_windowResizeWestCursor); + SEL ns = @selector(_windowResizeNorthSouthCursor); + SEL ew = @selector(_windowResizeEastWestCursor); + SEL nwse = @selector(_windowResizeNorthWestSouthEastCursor); + SEL nesw = @selector(_windowResizeNorthEastSouthWestCursor); + BOOL match = YES; + if (dir.x != 0 && (isEqualNSCursor(cur, ew) || isEqualNSCursor(cur, w) || isEqualNSCursor(cur, e))) { + dir.y = 0; + } + if (dir.y != 0 && (isEqualNSCursor(cur, ns) || isEqualNSCursor(cur, s) || isEqualNSCursor(cur, n))) { + dir.x = 0; + } + // If none of the cursors matched, we may deduce that the resize + // direction is one of the corners. However, to ensure that at least + // one cursor matches, check the corner cursors. + if (dir.x == 1 && dir.y == 1) { + if (!isEqualNSCursor(cur, nesw) && !isEqualNSCursor(cur, sw)) { + dir = NSZeroPoint; + } + } else if (dir.x == 1 && dir.y == -1) { + if (!isEqualNSCursor(cur, nwse) && !isEqualNSCursor(cur, nw)) { + dir = NSZeroPoint; + } + } else if (dir.x == -1 && dir.y == 1) { + if (!isEqualNSCursor(cur, nwse) && !isEqualNSCursor(cur, se)) { + dir = NSZeroPoint; + } + } else if (dir.x == -1 && dir.y == -1) { + if (!isEqualNSCursor(cur, nesw) && !isEqualNSCursor(cur, ne)) { + dir = NSZeroPoint; + } + } + if (!NSEqualPoints(dir, NSZeroPoint)) { + NSEvent *cancel = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp + location:l.locationInWindow + modifierFlags:l.modifierFlags + timestamp:l.timestamp + windowNumber:l.windowNumber + context:l.context + eventNumber:l.eventNumber + clickCount:l.clickCount + pressure:l.pressure]; + resizeState.off = off; + resizeState.dir = dir; + return cancel; + } + } + } + return [super nextEventMatchingMask:mask untilDate:expiration inMode:mode dequeue:deqFlag]; +} +@end + +@interface GioWindow: NSWindow +@end + +@implementation GioWindow +- (void)sendEvent:(NSEvent *)evt { + if (evt.type == NSEventTypeLeftMouseDown) { + resizeState.lastMouseDown = evt; + } + NSPoint dir = resizeState.dir; + if (NSEqualPoints(dir, NSZeroPoint)) { + [super sendEvent:evt]; + return; + } + switch (evt.type) { + default: + return; + case NSEventTypeLeftMouseUp: + resizeState.dir = NSZeroPoint; + resizeState.lastMouseDown = nil; + return; + case NSEventTypeLeftMouseDragged: + // Ok to proceed. + break; + } + NSPoint loc = evt.locationInWindow; + NSPoint off = resizeState.off; + loc.x -= off.x; + loc.y -= off.y; + NSRect frame = [self frame]; + NSSize min = [self minSize]; + NSSize max = [self maxSize]; + CGFloat width = frame.size.width; + if (dir.x > 0) { + width = loc.x; + } else if (dir.x < 0) { + width -= loc.x; + } + width = MIN(max.width, MAX(min.width, width)); + if (dir.x < 0) { + frame.origin.x += frame.size.width - width; + } + frame.size.width = width; + CGFloat height = frame.size.height; + if (dir.y > 0) { + height = loc.y; + } else if (dir.y < 0) { + height -= loc.y; + } + height = MIN(max.height, MAX(min.height, height)); + if (dir.y < 0) { + frame.origin.y += frame.size.height - height; + } + frame.size.height = height; + [self setFrame:frame display:YES animate:NO]; +} +@end + @implementation GioView - (void)setFrameSize:(NSSize)newSize { [super setFrameSize:newSize]; @@ -255,14 +461,11 @@ void gio_showCursor() { // some cursors are not public, this tries to use a private cursor // and uses fallback when the use of private cursor fails. static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) { - if ([NSCursor respondsToSelector:cursorName]) { - id object = [NSCursor performSelector:cursorName]; - if ([object isKindOfClass:[NSCursor class]]) { - [(NSCursor*)object set]; - return; - } + NSCursor *cur = lookupPrivateNSCursor(cursorName); + if (cur == nil) { + cur = fallback; } - [fallback set]; + [cur set]; } void gio_setCursor(NSUInteger curID) { @@ -361,7 +564,7 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF NSMiniaturizableWindowMask | NSClosableWindowMask; - NSWindow* window = [[NSWindow alloc] initWithContentRect:rect + GioWindow* window = [[GioWindow alloc] initWithContentRect:rect styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]; @@ -416,7 +619,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { void gio_main() { @autoreleasepool { - [NSApplication sharedApplication]; + [GioApplication sharedApplication]; GioAppDelegate *del = [[GioAppDelegate alloc] init]; [NSApp setDelegate:del];