Files
gio-patched/app/os_macos.m
T
Elias Naur 513250122c app: [macOS] defer Window destroy to after window close
The windowWillClose callback is too soon to destroy our Window:
at least draw callbacks may be called after windowWillClose but
before the window is gone. This change moves cleanup to the
viewDidMoveToWindow callback where we're sure the NSView is no longer
active.

Fixes: https://todo.sr.ht/~eliasnaur/gio/466
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-12-13 22:06:57 -06:00

416 lines
14 KiB
Objective-C

// SPDX-License-Identifier: Unlicense OR MIT
// +build darwin,!ios
#import <AppKit/AppKit.h>
#include "_cgo_export.h"
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioAppDelegate : NSObject<NSApplicationDelegate>
@end
@interface GioWindowDelegate : NSObject<NSWindowDelegate>
@end
@implementation GioWindowDelegate
- (void)windowWillMiniaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onHide((__bridge CFTypeRef)window.contentView);
}
- (void)windowDidDeminiaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onShow((__bridge CFTypeRef)window.contentView);
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFullscreen((__bridge CFTypeRef)window.contentView);
}
- (void)windowWillExitFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onWindowed((__bridge CFTypeRef)window.contentView);
}
- (void)windowDidChangeScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
CFTypeRef view = (__bridge CFTypeRef)window.contentView;
gio_onChangeScreen(view, dispID);
}
- (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFocus((__bridge CFTypeRef)window.contentView, 1);
}
- (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFocus((__bridge CFTypeRef)window.contentView, 0);
}
@end
static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
if (!event.hasPreciseScrollingDeltas) {
// dx and dy are in rows and columns.
dx *= 10;
dy *= 10;
}
// Origin is in the lower left corner. Convert to upper left.
CGFloat height = view.bounds.size.height;
gio_onMouse((__bridge CFTypeRef)view, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
}
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@end
@implementation GioView
- (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize];
[self setNeedsDisplay:YES];
}
// drawRect is called when OpenGL is used, displayLayer otherwise.
// Don't know why.
- (void)drawRect:(NSRect)r {
gio_onDraw((__bridge CFTypeRef)self);
}
- (void)displayLayer:(CALayer *)layer {
layer.contentsScale = self.window.backingScaleFactor;
gio_onDraw((__bridge CFTypeRef)self);
}
- (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory();
layer.delegate = self;
return layer;
}
- (void)viewDidMoveToWindow {
if (self.window == nil) {
gio_onClose((__bridge CFTypeRef)self);
}
}
- (void)mouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)mouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)middleMouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)middleMouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)rightMouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)rightMouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)mouseMoved:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)mouseDragged:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)scrollWheel:(NSEvent *)event {
CGFloat dx = -event.scrollingDeltaX;
CGFloat dy = -event.scrollingDeltaY;
handleMouse(self, event, MOUSE_SCROLL, dx, dy);
}
- (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
}
- (void)keyUp:(NSEvent *)event {
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
}
- (void)insertText:(id)string {
gio_onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)string);
}
- (void)doCommandBySelector:(SEL)sel {
// Don't pass commands up the responder chain.
// They will end up in a beep.
}
- (BOOL)hasMarkedText {
int res = gio_hasMarkedText((__bridge CFTypeRef)self);
return res ? YES : NO;
}
- (NSRange)markedRange {
return gio_markedRange((__bridge CFTypeRef)self);
}
- (NSRange)selectedRange {
return gio_selectedRange((__bridge CFTypeRef)self);
}
- (void)unmarkText {
gio_unmarkText((__bridge CFTypeRef)self);
}
- (void)setMarkedText:(id)string
selectedRange:(NSRange)selRange
replacementRange:(NSRange)replaceRange {
NSString *str;
// string is either an NSAttributedString or an NSString.
if ([string isKindOfClass:[NSAttributedString class]]) {
str = [string string];
} else {
str = string;
}
gio_setMarkedText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, selRange, replaceRange);
}
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
return nil;
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
actualRange:(NSRangePointer)actualRange {
NSString *str = CFBridgingRelease(gio_substringForProposedRange((__bridge CFTypeRef)self, range, actualRange));
return [[NSAttributedString alloc] initWithString:str attributes:nil];
}
- (void)insertText:(id)string
replacementRange:(NSRange)replaceRange {
NSString *str;
// string is either an NSAttributedString or an NSString.
if ([string isKindOfClass:[NSAttributedString class]]) {
str = [string string];
} else {
str = string;
}
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p);
}
- (NSRect)firstRectForCharacterRange:(NSRange)rng
actualRange:(NSRangePointer)actual {
NSRect r = gio_firstRectForCharacterRange((__bridge CFTypeRef)self, rng, actual);
r = [self convertRect:r toView:nil];
return [[self window] convertRectToScreen:r];
}
@end
// Delegates are weakly referenced from their peers. Nothing
// else holds a strong reference to our window delegate, so
// keep a single global reference instead.
static GioWindowDelegate *globalWindowDel;
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) {
gio_onFrameCallback(dl, (uintptr_t)handle);
return kCVReturnSuccess;
}
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
CVDisplayLinkRef dl;
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, (void *)(handle));
return dl;
}
int gio_startDisplayLink(CFTypeRef dl) {
return CVDisplayLinkStart((CVDisplayLinkRef)dl);
}
int gio_stopDisplayLink(CFTypeRef dl) {
return CVDisplayLinkStop((CVDisplayLinkRef)dl);
}
void gio_releaseDisplayLink(CFTypeRef dl) {
CVDisplayLinkRelease((CVDisplayLinkRef)dl);
}
void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) {
CVDisplayLinkSetCurrentCGDisplay((CVDisplayLinkRef)dl, (CGDirectDisplayID)did);
}
void gio_hideCursor() {
@autoreleasepool {
[NSCursor hide];
}
}
void gio_showCursor() {
@autoreleasepool {
[NSCursor unhide];
}
}
// some cursors are not public, this tries to use a private cursor
// and uses fallback when the use of private cursor fails.
void gio_trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
if ([NSCursor respondsToSelector:cursorName]) {
id object = [NSCursor performSelector:cursorName];
if ([object isKindOfClass:[NSCursor class]]) {
[(NSCursor*)object set];
return;
}
}
[fallback set];
}
void gio_setCursor(NSUInteger curID) {
@autoreleasepool {
switch (curID) {
case 0: // pointer.CursorDefault
[NSCursor.arrowCursor set];
break;
// case 1: // pointer.CursorNone
case 2: // pointer.CursorText
[NSCursor.IBeamCursor set];
break;
case 3: // pointer.CursorVerticalText
[NSCursor.IBeamCursorForVerticalLayout set];
break;
case 4: // pointer.CursorPointer
[NSCursor.pointingHandCursor set];
break;
case 5: // pointer.CursorCrosshair
[NSCursor.crosshairCursor set];
break;
case 6: // pointer.CursorAllScroll
// For some reason, using _moveCursor fails on Monterey.
// gio_trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
[NSCursor.arrowCursor set];
break;
case 7: // pointer.CursorColResize
[NSCursor.resizeLeftRightCursor set];
break;
case 8: // pointer.CursorRowResize
[NSCursor.resizeUpDownCursor set];
break;
case 9: // pointer.CursorGrab
// [NSCursor.openHandCursor set];
gio_trySetPrivateCursor(@selector(openHandCursor), NSCursor.arrowCursor);
break;
case 10: // pointer.CursorGrabbing
// [NSCursor.closedHandCursor set];
gio_trySetPrivateCursor(@selector(closedHandCursor), NSCursor.arrowCursor);
break;
case 11: // pointer.CursorNotAllowed
[NSCursor.operationNotAllowedCursor set];
break;
case 12: // pointer.CursorWait
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break;
case 13: // pointer.CursorProgress
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break;
case 14: // pointer.CursorNorthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 15: // pointer.CursorNorthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
break;
case 16: // pointer.CursorSouthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 17: // pointer.CursorSouthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
break;
case 18: // pointer.CursorNorthSouthResize
[NSCursor.resizeUpDownCursor set];
break;
case 19: // pointer.CursorEastWestResize
[NSCursor.resizeLeftRightCursor set];
break;
case 20: // pointer.CursorWestResize
[NSCursor.resizeLeftCursor set];
break;
case 21: // pointer.CursorEastResize
[NSCursor.resizeRightCursor set];
break;
case 22: // pointer.CursorNorthResize
[NSCursor.resizeUpCursor set];
break;
case 23: // pointer.CursorSouthResize
[NSCursor.resizeDownCursor set];
break;
case 24: // pointer.CursorNorthEastSouthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 25: // pointer.CursorNorthWestSouthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
break;
default:
[NSCursor.arrowCursor set];
break;
}
}
}
CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight) {
@autoreleasepool {
NSRect rect = NSMakeRect(0, 0, width, height);
NSUInteger styleMask = NSTitledWindowMask |
NSResizableWindowMask |
NSMiniaturizableWindowMask |
NSClosableWindowMask;
NSWindow* window = [[NSWindow alloc] initWithContentRect:rect
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
if (minWidth > 0 || minHeight > 0) {
window.contentMinSize = NSMakeSize(minWidth, minHeight);
}
if (maxWidth > 0 || maxHeight > 0) {
window.contentMaxSize = NSMakeSize(maxWidth, maxHeight);
}
[window setAcceptsMouseMovedEvents:YES];
NSView *view = (__bridge NSView *)viewRef;
[window setContentView:view];
[window makeFirstResponder:view];
window.delegate = globalWindowDel;
return (__bridge_retained CFTypeRef)window;
}
}
CFTypeRef gio_createView(void) {
@autoreleasepool {
NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame];
view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
return CFBridgingRetain(view);
}
}
@implementation GioAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES];
gio_onFinishLaunching();
}
- (void)applicationDidHide:(NSNotification *)aNotification {
gio_onAppHide();
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onAppShow();
}
@end
void gio_main() {
@autoreleasepool {
[NSApplication sharedApplication];
GioAppDelegate *del = [[GioAppDelegate alloc] init];
[NSApp setDelegate:del];
NSMenuItem *mainMenu = [NSMenuItem new];
NSMenu *menu = [NSMenu new];
NSMenuItem *hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide"
action:@selector(hide:)
keyEquivalent:@"h"];
[menu addItem:hideMenuItem];
NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit"
action:@selector(terminate:)
keyEquivalent:@"q"];
[menu addItem:quitMenuItem];
[mainMenu setSubmenu:menu];
NSMenu *menuBar = [NSMenu new];
[menuBar addItem:mainMenu];
[NSApp setMainMenu:menuBar];
globalWindowDel = [[GioWindowDelegate alloc] init];
[NSApp run];
}
}