gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/app/os_macos.m (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 // +build darwin,!ios 4 5 #import <AppKit/AppKit.h> 6 7 #include "_cgo_export.h" 8 9 __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void); 10 11 @interface GioAppDelegate : NSObject<NSApplicationDelegate> 12 @end 13 14 @interface GioWindowDelegate : NSObject<NSWindowDelegate> 15 @end 16 17 @interface GioView : NSView <CALayerDelegate,NSTextInputClient> 18 @property uintptr_t handle; 19 @end 20 21 @implementation GioWindowDelegate 22 - (void)windowWillMiniaturize:(NSNotification *)notification { 23 NSWindow *window = (NSWindow *)[notification object]; 24 GioView *view = (GioView *)window.contentView; 25 gio_onHide(view.handle); 26 } 27 - (void)windowDidDeminiaturize:(NSNotification *)notification { 28 NSWindow *window = (NSWindow *)[notification object]; 29 GioView *view = (GioView *)window.contentView; 30 gio_onShow(view.handle); 31 } 32 - (void)windowWillEnterFullScreen:(NSNotification *)notification { 33 NSWindow *window = (NSWindow *)[notification object]; 34 GioView *view = (GioView *)window.contentView; 35 gio_onFullscreen(view.handle); 36 } 37 - (void)windowWillExitFullScreen:(NSNotification *)notification { 38 NSWindow *window = (NSWindow *)[notification object]; 39 GioView *view = (GioView *)window.contentView; 40 gio_onWindowed(view.handle); 41 } 42 - (void)windowDidChangeScreen:(NSNotification *)notification { 43 NSWindow *window = (NSWindow *)[notification object]; 44 CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue]; 45 GioView *view = (GioView *)window.contentView; 46 gio_onChangeScreen(view.handle, dispID); 47 } 48 - (void)windowDidBecomeKey:(NSNotification *)notification { 49 NSWindow *window = (NSWindow *)[notification object]; 50 GioView *view = (GioView *)window.contentView; 51 gio_onFocus(view.handle, 1); 52 } 53 - (void)windowDidResignKey:(NSNotification *)notification { 54 NSWindow *window = (NSWindow *)[notification object]; 55 GioView *view = (GioView *)window.contentView; 56 gio_onFocus(view.handle, 0); 57 } 58 @end 59 60 static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) { 61 NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil]; 62 if (!event.hasPreciseScrollingDeltas) { 63 // dx and dy are in rows and columns. 64 dx *= 10; 65 dy *= 10; 66 } 67 // Origin is in the lower left corner. Convert to upper left. 68 CGFloat height = view.bounds.size.height; 69 gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]); 70 } 71 72 @implementation GioView 73 - (void)setFrameSize:(NSSize)newSize { 74 [super setFrameSize:newSize]; 75 [self setNeedsDisplay:YES]; 76 } 77 // drawRect is called when OpenGL is used, displayLayer otherwise. 78 // Don't know why. 79 - (void)drawRect:(NSRect)r { 80 gio_onDraw(self.handle); 81 } 82 - (void)displayLayer:(CALayer *)layer { 83 layer.contentsScale = self.window.backingScaleFactor; 84 gio_onDraw(self.handle); 85 } 86 - (CALayer *)makeBackingLayer { 87 CALayer *layer = gio_layerFactory(); 88 layer.delegate = self; 89 return layer; 90 } 91 - (void)viewDidMoveToWindow { 92 gio_onAttached(self.handle, self.window != nil ? 1 : 0); 93 } 94 - (void)mouseDown:(NSEvent *)event { 95 handleMouse(self, event, MOUSE_DOWN, 0, 0); 96 } 97 - (void)mouseUp:(NSEvent *)event { 98 handleMouse(self, event, MOUSE_UP, 0, 0); 99 } 100 - (void)rightMouseDown:(NSEvent *)event { 101 handleMouse(self, event, MOUSE_DOWN, 0, 0); 102 } 103 - (void)rightMouseUp:(NSEvent *)event { 104 handleMouse(self, event, MOUSE_UP, 0, 0); 105 } 106 - (void)otherMouseDown:(NSEvent *)event { 107 handleMouse(self, event, MOUSE_DOWN, 0, 0); 108 } 109 - (void)otherMouseUp:(NSEvent *)event { 110 handleMouse(self, event, MOUSE_UP, 0, 0); 111 } 112 - (void)mouseMoved:(NSEvent *)event { 113 handleMouse(self, event, MOUSE_MOVE, 0, 0); 114 } 115 - (void)mouseDragged:(NSEvent *)event { 116 handleMouse(self, event, MOUSE_MOVE, 0, 0); 117 } 118 - (void)rightMouseDragged:(NSEvent *)event { 119 handleMouse(self, event, MOUSE_MOVE, 0, 0); 120 } 121 - (void)otherMouseDragged:(NSEvent *)event { 122 handleMouse(self, event, MOUSE_MOVE, 0, 0); 123 } 124 - (void)scrollWheel:(NSEvent *)event { 125 CGFloat dx = -event.scrollingDeltaX; 126 CGFloat dy = -event.scrollingDeltaY; 127 handleMouse(self, event, MOUSE_SCROLL, dx, dy); 128 } 129 - (void)keyDown:(NSEvent *)event { 130 [self interpretKeyEvents:[NSArray arrayWithObject:event]]; 131 NSString *keys = [event charactersIgnoringModifiers]; 132 gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true); 133 } 134 - (void)keyUp:(NSEvent *)event { 135 NSString *keys = [event charactersIgnoringModifiers]; 136 gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false); 137 } 138 - (void)insertText:(id)string { 139 gio_onText(self.handle, (__bridge CFTypeRef)string); 140 } 141 - (void)doCommandBySelector:(SEL)sel { 142 // Don't pass commands up the responder chain. 143 // They will end up in a beep. 144 } 145 146 - (BOOL)hasMarkedText { 147 int res = gio_hasMarkedText(self.handle); 148 return res ? YES : NO; 149 } 150 - (NSRange)markedRange { 151 return gio_markedRange(self.handle); 152 } 153 - (NSRange)selectedRange { 154 return gio_selectedRange(self.handle); 155 } 156 - (void)unmarkText { 157 gio_unmarkText(self.handle); 158 } 159 - (void)setMarkedText:(id)string 160 selectedRange:(NSRange)selRange 161 replacementRange:(NSRange)replaceRange { 162 NSString *str; 163 // string is either an NSAttributedString or an NSString. 164 if ([string isKindOfClass:[NSAttributedString class]]) { 165 str = [string string]; 166 } else { 167 str = string; 168 } 169 gio_setMarkedText(self.handle, (__bridge CFTypeRef)str, selRange, replaceRange); 170 } 171 - (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText { 172 return nil; 173 } 174 - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range 175 actualRange:(NSRangePointer)actualRange { 176 NSString *str = CFBridgingRelease(gio_substringForProposedRange(self.handle, range, actualRange)); 177 return [[NSAttributedString alloc] initWithString:str attributes:nil]; 178 } 179 - (void)insertText:(id)string 180 replacementRange:(NSRange)replaceRange { 181 NSString *str; 182 // string is either an NSAttributedString or an NSString. 183 if ([string isKindOfClass:[NSAttributedString class]]) { 184 str = [string string]; 185 } else { 186 str = string; 187 } 188 gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange); 189 } 190 - (NSUInteger)characterIndexForPoint:(NSPoint)p { 191 return gio_characterIndexForPoint(self.handle, p); 192 } 193 - (NSRect)firstRectForCharacterRange:(NSRange)rng 194 actualRange:(NSRangePointer)actual { 195 NSRect r = gio_firstRectForCharacterRange(self.handle, rng, actual); 196 r = [self convertRect:r toView:nil]; 197 return [[self window] convertRectToScreen:r]; 198 } 199 - (void)applicationWillUnhide:(NSNotification *)notification { 200 gio_onShow(self.handle); 201 } 202 - (void)applicationDidHide:(NSNotification *)notification { 203 gio_onHide(self.handle); 204 } 205 - (void)dealloc { 206 gio_onDestroy(self.handle); 207 } 208 @end 209 210 // Delegates are weakly referenced from their peers. Nothing 211 // else holds a strong reference to our window delegate, so 212 // keep a single global reference instead. 213 static GioWindowDelegate *globalWindowDel; 214 215 static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) { 216 gio_onFrameCallback(dl); 217 return kCVReturnSuccess; 218 } 219 220 CFTypeRef gio_createDisplayLink(void) { 221 CVDisplayLinkRef dl; 222 CVDisplayLinkCreateWithActiveCGDisplays(&dl); 223 CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil); 224 return dl; 225 } 226 227 int gio_startDisplayLink(CFTypeRef dl) { 228 return CVDisplayLinkStart((CVDisplayLinkRef)dl); 229 } 230 231 int gio_stopDisplayLink(CFTypeRef dl) { 232 return CVDisplayLinkStop((CVDisplayLinkRef)dl); 233 } 234 235 void gio_releaseDisplayLink(CFTypeRef dl) { 236 CVDisplayLinkRelease((CVDisplayLinkRef)dl); 237 } 238 239 void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) { 240 CVDisplayLinkSetCurrentCGDisplay((CVDisplayLinkRef)dl, (CGDirectDisplayID)did); 241 } 242 243 void gio_hideCursor() { 244 @autoreleasepool { 245 [NSCursor hide]; 246 } 247 } 248 249 void gio_showCursor() { 250 @autoreleasepool { 251 [NSCursor unhide]; 252 } 253 } 254 255 // some cursors are not public, this tries to use a private cursor 256 // and uses fallback when the use of private cursor fails. 257 static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) { 258 if ([NSCursor respondsToSelector:cursorName]) { 259 id object = [NSCursor performSelector:cursorName]; 260 if ([object isKindOfClass:[NSCursor class]]) { 261 [(NSCursor*)object set]; 262 return; 263 } 264 } 265 [fallback set]; 266 } 267 268 void gio_setCursor(NSUInteger curID) { 269 @autoreleasepool { 270 switch (curID) { 271 case 0: // pointer.CursorDefault 272 [NSCursor.arrowCursor set]; 273 break; 274 // case 1: // pointer.CursorNone 275 case 2: // pointer.CursorText 276 [NSCursor.IBeamCursor set]; 277 break; 278 case 3: // pointer.CursorVerticalText 279 [NSCursor.IBeamCursorForVerticalLayout set]; 280 break; 281 case 4: // pointer.CursorPointer 282 [NSCursor.pointingHandCursor set]; 283 break; 284 case 5: // pointer.CursorCrosshair 285 [NSCursor.crosshairCursor set]; 286 break; 287 case 6: // pointer.CursorAllScroll 288 // For some reason, using _moveCursor fails on Monterey. 289 // trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor); 290 [NSCursor.arrowCursor set]; 291 break; 292 case 7: // pointer.CursorColResize 293 [NSCursor.resizeLeftRightCursor set]; 294 break; 295 case 8: // pointer.CursorRowResize 296 [NSCursor.resizeUpDownCursor set]; 297 break; 298 case 9: // pointer.CursorGrab 299 [NSCursor.openHandCursor set]; 300 break; 301 case 10: // pointer.CursorGrabbing 302 [NSCursor.closedHandCursor set]; 303 break; 304 case 11: // pointer.CursorNotAllowed 305 [NSCursor.operationNotAllowedCursor set]; 306 break; 307 case 12: // pointer.CursorWait 308 trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor); 309 break; 310 case 13: // pointer.CursorProgress 311 trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor); 312 break; 313 case 14: // pointer.CursorNorthWestResize 314 trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor); 315 break; 316 case 15: // pointer.CursorNorthEastResize 317 trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor); 318 break; 319 case 16: // pointer.CursorSouthWestResize 320 trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor); 321 break; 322 case 17: // pointer.CursorSouthEastResize 323 trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor); 324 break; 325 case 18: // pointer.CursorNorthSouthResize 326 [NSCursor.resizeUpDownCursor set]; 327 break; 328 case 19: // pointer.CursorEastWestResize 329 [NSCursor.resizeLeftRightCursor set]; 330 break; 331 case 20: // pointer.CursorWestResize 332 [NSCursor.resizeLeftCursor set]; 333 break; 334 case 21: // pointer.CursorEastResize 335 [NSCursor.resizeRightCursor set]; 336 break; 337 case 22: // pointer.CursorNorthResize 338 [NSCursor.resizeUpCursor set]; 339 break; 340 case 23: // pointer.CursorSouthResize 341 [NSCursor.resizeDownCursor set]; 342 break; 343 case 24: // pointer.CursorNorthEastSouthWestResize 344 trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor); 345 break; 346 case 25: // pointer.CursorNorthWestSouthEastResize 347 trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor); 348 break; 349 default: 350 [NSCursor.arrowCursor set]; 351 break; 352 } 353 } 354 } 355 356 CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight) { 357 @autoreleasepool { 358 NSRect rect = NSMakeRect(0, 0, width, height); 359 NSUInteger styleMask = NSTitledWindowMask | 360 NSResizableWindowMask | 361 NSMiniaturizableWindowMask | 362 NSClosableWindowMask; 363 364 NSWindow* window = [[NSWindow alloc] initWithContentRect:rect 365 styleMask:styleMask 366 backing:NSBackingStoreBuffered 367 defer:NO]; 368 if (minWidth > 0 || minHeight > 0) { 369 window.contentMinSize = NSMakeSize(minWidth, minHeight); 370 } 371 if (maxWidth > 0 || maxHeight > 0) { 372 window.contentMaxSize = NSMakeSize(maxWidth, maxHeight); 373 } 374 [window setAcceptsMouseMovedEvents:YES]; 375 NSView *view = (__bridge NSView *)viewRef; 376 [window setContentView:view]; 377 [window makeFirstResponder:view]; 378 window.delegate = globalWindowDel; 379 return (__bridge_retained CFTypeRef)window; 380 } 381 } 382 383 CFTypeRef gio_createView(void) { 384 @autoreleasepool { 385 NSRect frame = NSMakeRect(0, 0, 0, 0); 386 GioView* view = [[GioView alloc] initWithFrame:frame]; 387 view.wantsLayer = YES; 388 view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; 389 390 [[NSNotificationCenter defaultCenter] addObserver:view 391 selector:@selector(applicationWillUnhide:) 392 name:NSApplicationWillUnhideNotification 393 object:nil]; 394 [[NSNotificationCenter defaultCenter] addObserver:view 395 selector:@selector(applicationDidHide:) 396 name:NSApplicationDidHideNotification 397 object:nil]; 398 return CFBridgingRetain(view); 399 } 400 } 401 402 void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) { 403 @autoreleasepool { 404 GioView *v = (__bridge GioView *)viewRef; 405 v.handle = handle; 406 } 407 } 408 409 @implementation GioAppDelegate 410 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 411 [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 412 [NSApp activateIgnoringOtherApps:YES]; 413 gio_onFinishLaunching(); 414 } 415 @end 416 417 void gio_main() { 418 @autoreleasepool { 419 [NSApplication sharedApplication]; 420 GioAppDelegate *del = [[GioAppDelegate alloc] init]; 421 [NSApp setDelegate:del]; 422 423 NSMenuItem *mainMenu = [NSMenuItem new]; 424 425 NSMenu *menu = [NSMenu new]; 426 NSMenuItem *hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide" 427 action:@selector(hide:) 428 keyEquivalent:@"h"]; 429 [menu addItem:hideMenuItem]; 430 NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit" 431 action:@selector(terminate:) 432 keyEquivalent:@"q"]; 433 [menu addItem:quitMenuItem]; 434 [mainMenu setSubmenu:menu]; 435 NSMenu *menuBar = [NSMenu new]; 436 [menuBar addItem:mainMenu]; 437 [NSApp setMainMenu:menuBar]; 438 439 globalWindowDel = [[GioWindowDelegate alloc] init]; 440 441 [NSApp run]; 442 } 443 }