gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/app/os_macos.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 //go:build darwin && !ios 4 // +build darwin,!ios 5 6 package app 7 8 import ( 9 "errors" 10 "image" 11 "io" 12 "runtime" 13 "runtime/cgo" 14 "strings" 15 "time" 16 "unicode" 17 "unicode/utf8" 18 19 "gioui.org/internal/f32" 20 "gioui.org/io/event" 21 "gioui.org/io/key" 22 "gioui.org/io/pointer" 23 "gioui.org/io/system" 24 "gioui.org/io/transfer" 25 "gioui.org/op" 26 "gioui.org/unit" 27 28 _ "gioui.org/internal/cocoainit" 29 ) 30 31 /* 32 #cgo CFLAGS: -Werror -Wno-deprecated-declarations -fobjc-arc -x objective-c 33 #cgo LDFLAGS: -framework AppKit -framework QuartzCore 34 35 #include <AppKit/AppKit.h> 36 37 #define MOUSE_MOVE 1 38 #define MOUSE_UP 2 39 #define MOUSE_DOWN 3 40 #define MOUSE_SCROLL 4 41 42 __attribute__ ((visibility ("hidden"))) void gio_main(void); 43 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void); 44 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); 45 __attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); 46 47 static void writeClipboard(CFTypeRef str) { 48 @autoreleasepool { 49 NSString *s = (__bridge NSString *)str; 50 NSPasteboard *p = NSPasteboard.generalPasteboard; 51 [p declareTypes:@[NSPasteboardTypeString] owner:nil]; 52 [p setString:s forType:NSPasteboardTypeString]; 53 } 54 } 55 56 static CFTypeRef readClipboard(void) { 57 @autoreleasepool { 58 NSPasteboard *p = NSPasteboard.generalPasteboard; 59 NSString *content = [p stringForType:NSPasteboardTypeString]; 60 return (__bridge_retained CFTypeRef)content; 61 } 62 } 63 64 static CGFloat viewHeight(CFTypeRef viewRef) { 65 @autoreleasepool { 66 NSView *view = (__bridge NSView *)viewRef; 67 return [view bounds].size.height; 68 } 69 } 70 71 static CGFloat viewWidth(CFTypeRef viewRef) { 72 @autoreleasepool { 73 NSView *view = (__bridge NSView *)viewRef; 74 return [view bounds].size.width; 75 } 76 } 77 78 static CGFloat getScreenBackingScale(void) { 79 @autoreleasepool { 80 return [NSScreen.mainScreen backingScaleFactor]; 81 } 82 } 83 84 static CGFloat getViewBackingScale(CFTypeRef viewRef) { 85 @autoreleasepool { 86 NSView *view = (__bridge NSView *)viewRef; 87 return [view.window backingScaleFactor]; 88 } 89 } 90 91 static void setNeedsDisplay(CFTypeRef viewRef) { 92 @autoreleasepool { 93 NSView *view = (__bridge NSView *)viewRef; 94 [view setNeedsDisplay:YES]; 95 } 96 } 97 98 static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) { 99 @autoreleasepool { 100 NSWindow *window = (__bridge NSWindow *)windowRef; 101 return [window cascadeTopLeftFromPoint:topLeft]; 102 } 103 } 104 105 static void makeKeyAndOrderFront(CFTypeRef windowRef) { 106 @autoreleasepool { 107 NSWindow *window = (__bridge NSWindow *)windowRef; 108 [window makeKeyAndOrderFront:nil]; 109 } 110 } 111 112 static void toggleFullScreen(CFTypeRef windowRef) { 113 @autoreleasepool { 114 NSWindow *window = (__bridge NSWindow *)windowRef; 115 [window toggleFullScreen:nil]; 116 } 117 } 118 119 static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) { 120 @autoreleasepool { 121 NSWindow *window = (__bridge NSWindow *)windowRef; 122 return [window styleMask]; 123 } 124 } 125 126 static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) { 127 @autoreleasepool { 128 NSWindow *window = (__bridge NSWindow *)windowRef; 129 window.styleMask = mask; 130 } 131 } 132 133 static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) { 134 @autoreleasepool { 135 NSWindow *window = (__bridge NSWindow *)windowRef; 136 window.titleVisibility = state; 137 } 138 } 139 140 static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) { 141 @autoreleasepool { 142 NSWindow *window = (__bridge NSWindow *)windowRef; 143 window.titlebarAppearsTransparent = (BOOL)transparent; 144 } 145 } 146 147 static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) { 148 @autoreleasepool { 149 NSWindow *window = (__bridge NSWindow *)windowRef; 150 [window standardWindowButton:btn].hidden = (BOOL)hide; 151 } 152 } 153 154 static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) { 155 @autoreleasepool { 156 NSWindow *window = (__bridge NSWindow *)windowRef; 157 [window performWindowDragWithEvent:(__bridge NSEvent*)evt]; 158 } 159 } 160 161 static void closeWindow(CFTypeRef windowRef) { 162 @autoreleasepool { 163 NSWindow* window = (__bridge NSWindow *)windowRef; 164 [window performClose:nil]; 165 } 166 } 167 168 static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { 169 @autoreleasepool { 170 NSWindow* window = (__bridge NSWindow *)windowRef; 171 NSSize size = NSMakeSize(width, height); 172 [window setContentSize:size]; 173 } 174 } 175 176 static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { 177 @autoreleasepool { 178 NSWindow* window = (__bridge NSWindow *)windowRef; 179 window.contentMinSize = NSMakeSize(width, height); 180 } 181 } 182 183 static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { 184 @autoreleasepool { 185 NSWindow* window = (__bridge NSWindow *)windowRef; 186 window.contentMaxSize = NSMakeSize(width, height); 187 } 188 } 189 190 static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) { 191 @autoreleasepool { 192 NSWindow* window = (__bridge NSWindow *)windowRef; 193 NSRect r = NSMakeRect(x, y, w, h); 194 [window setFrame:r display:YES]; 195 } 196 } 197 198 static void resetLayerFrame(CFTypeRef viewRef) { 199 @autoreleasepool { 200 NSView* view = (__bridge NSView *)viewRef; 201 NSRect r = view.frame; 202 view.layer.frame = r; 203 } 204 } 205 206 static void hideWindow(CFTypeRef windowRef) { 207 @autoreleasepool { 208 NSWindow* window = (__bridge NSWindow *)windowRef; 209 [window miniaturize:window]; 210 } 211 } 212 213 static void unhideWindow(CFTypeRef windowRef) { 214 @autoreleasepool { 215 NSWindow* window = (__bridge NSWindow *)windowRef; 216 [window deminiaturize:window]; 217 } 218 } 219 220 static NSRect getScreenFrame(CFTypeRef windowRef) { 221 @autoreleasepool { 222 NSWindow* window = (__bridge NSWindow *)windowRef; 223 return [[window screen] frame]; 224 } 225 } 226 227 static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) { 228 @autoreleasepool { 229 NSWindow *window = (__bridge NSWindow *)windowRef; 230 window.title = (__bridge NSString *)titleRef; 231 } 232 } 233 234 static int isWindowZoomed(CFTypeRef windowRef) { 235 @autoreleasepool { 236 NSWindow *window = (__bridge NSWindow *)windowRef; 237 return window.zoomed ? 1 : 0; 238 } 239 } 240 241 static void zoomWindow(CFTypeRef windowRef) { 242 @autoreleasepool { 243 NSWindow *window = (__bridge NSWindow *)windowRef; 244 [window zoom:nil]; 245 } 246 } 247 248 static CFTypeRef layerForView(CFTypeRef viewRef) { 249 @autoreleasepool { 250 NSView *view = (__bridge NSView *)viewRef; 251 return (__bridge CFTypeRef)view.layer; 252 } 253 } 254 255 static CFTypeRef windowForView(CFTypeRef viewRef) { 256 @autoreleasepool { 257 NSView *view = (__bridge NSView *)viewRef; 258 return (__bridge CFTypeRef)view.window; 259 } 260 } 261 262 static void raiseWindow(CFTypeRef windowRef) { 263 @autoreleasepool { 264 NSRunningApplication *currentApp = [NSRunningApplication currentApplication]; 265 if (![currentApp isActive]) { 266 [currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; 267 } 268 NSWindow* window = (__bridge NSWindow *)windowRef; 269 [window makeKeyAndOrderFront:nil]; 270 } 271 } 272 273 static CFTypeRef createInputContext(CFTypeRef clientRef) { 274 @autoreleasepool { 275 id<NSTextInputClient> client = (__bridge id<NSTextInputClient>)clientRef; 276 NSTextInputContext *ctx = [[NSTextInputContext alloc] initWithClient:client]; 277 return CFBridgingRetain(ctx); 278 } 279 } 280 281 static void discardMarkedText(CFTypeRef viewRef) { 282 @autoreleasepool { 283 id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef; 284 NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; 285 if (view == [ctx client]) { 286 [ctx discardMarkedText]; 287 } 288 } 289 } 290 291 static void invalidateCharacterCoordinates(CFTypeRef viewRef) { 292 @autoreleasepool { 293 id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef; 294 NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; 295 if (view == [ctx client]) { 296 [ctx invalidateCharacterCoordinates]; 297 } 298 } 299 } 300 */ 301 import "C" 302 303 func init() { 304 // Darwin requires that UI operations happen on the main thread only. 305 runtime.LockOSThread() 306 } 307 308 // AppKitViewEvent notifies the client of changes to the window AppKit handles. 309 // The handles are retained until another AppKitViewEvent is sent. 310 type AppKitViewEvent struct { 311 // View is a CFTypeRef for the NSView for the window. 312 View uintptr 313 // Layer is a CFTypeRef of the CALayer of View. 314 Layer uintptr 315 } 316 317 type window struct { 318 view C.CFTypeRef 319 w *callbacks 320 anim bool 321 visible bool 322 displayLink *displayLink 323 // redraw is a single entry channel for making sure only one 324 // display link redraw request is in flight. 325 redraw chan struct{} 326 cursor pointer.Cursor 327 pointerBtns pointer.Buttons 328 loop *eventLoop 329 initialized bool 330 331 scale float32 332 config Config 333 } 334 335 // launched is closed when applicationDidFinishLaunching is called. 336 var launched = make(chan struct{}) 337 338 // nextTopLeft is the offset to use for the next window's call to 339 // cascadeTopLeftFromPoint. 340 var nextTopLeft C.NSPoint 341 342 func windowFor(h C.uintptr_t) *window { 343 return cgo.Handle(h).Value().(*window) 344 } 345 346 func (w *window) contextView() C.CFTypeRef { 347 return w.view 348 } 349 350 func (w *window) ReadClipboard() { 351 cstr := C.readClipboard() 352 if cstr != 0 { 353 defer C.CFRelease(cstr) 354 } 355 content := nsstringToString(cstr) 356 w.ProcessEvent(transfer.DataEvent{ 357 Type: "application/text", 358 Open: func() io.ReadCloser { 359 return io.NopCloser(strings.NewReader(content)) 360 }, 361 }) 362 } 363 364 func (w *window) WriteClipboard(mime string, s []byte) { 365 cstr := stringToNSString(string(s)) 366 defer C.CFRelease(cstr) 367 C.writeClipboard(cstr) 368 } 369 370 func (w *window) updateWindowMode() { 371 style := int(C.getWindowStyleMask(C.windowForView(w.view))) 372 if style&C.NSWindowStyleMaskFullScreen != 0 { 373 w.config.Mode = Fullscreen 374 } else { 375 w.config.Mode = Windowed 376 } 377 w.config.Decorated = style&C.NSWindowStyleMaskFullSizeContentView == 0 378 } 379 380 func (w *window) Configure(options []Option) { 381 screenScale := float32(C.getScreenBackingScale()) 382 cfg := configFor(screenScale) 383 prev := w.config 384 w.updateWindowMode() 385 cnf := w.config 386 cnf.apply(cfg, options) 387 window := C.windowForView(w.view) 388 389 switch cnf.Mode { 390 case Fullscreen: 391 switch prev.Mode { 392 case Fullscreen: 393 case Minimized: 394 C.unhideWindow(window) 395 fallthrough 396 default: 397 w.config.Mode = Fullscreen 398 C.toggleFullScreen(window) 399 } 400 case Minimized: 401 switch prev.Mode { 402 case Minimized, Fullscreen: 403 default: 404 w.config.Mode = Minimized 405 C.hideWindow(window) 406 } 407 case Maximized: 408 switch prev.Mode { 409 case Fullscreen: 410 case Minimized: 411 C.unhideWindow(window) 412 fallthrough 413 default: 414 w.config.Mode = Maximized 415 w.setTitle(prev, cnf) 416 if C.isWindowZoomed(window) == 0 { 417 C.zoomWindow(window) 418 } 419 } 420 case Windowed: 421 switch prev.Mode { 422 case Fullscreen: 423 C.toggleFullScreen(window) 424 case Minimized: 425 C.unhideWindow(window) 426 case Maximized: 427 if C.isWindowZoomed(window) != 0 { 428 C.zoomWindow(window) 429 } 430 } 431 w.config.Mode = Windowed 432 w.setTitle(prev, cnf) 433 if prev.Size != cnf.Size { 434 w.config.Size = cnf.Size 435 cnf.Size = cnf.Size.Div(int(screenScale)) 436 C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y)) 437 } 438 if prev.MinSize != cnf.MinSize { 439 w.config.MinSize = cnf.MinSize 440 cnf.MinSize = cnf.MinSize.Div(int(screenScale)) 441 C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y)) 442 } 443 if prev.MaxSize != cnf.MaxSize { 444 w.config.MaxSize = cnf.MaxSize 445 cnf.MaxSize = cnf.MaxSize.Div(int(screenScale)) 446 C.setMaxSize(window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y)) 447 } 448 } 449 if cnf.Decorated != prev.Decorated { 450 w.config.Decorated = cnf.Decorated 451 mask := C.getWindowStyleMask(window) 452 style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable) 453 style = C.NSWindowStyleMaskFullSizeContentView 454 mask &^= style 455 barTrans := C.int(C.NO) 456 titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible) 457 if !cnf.Decorated { 458 mask |= style 459 barTrans = C.YES 460 titleVis = C.NSWindowTitleHidden 461 } 462 C.setWindowTitlebarAppearsTransparent(window, barTrans) 463 C.setWindowTitleVisibility(window, titleVis) 464 C.setWindowStyleMask(window, mask) 465 C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans) 466 C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans) 467 C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans) 468 // When toggling the titlebar, the layer doesn't update its frame 469 // until the next resize. Force it. 470 C.resetLayerFrame(w.view) 471 } 472 w.ProcessEvent(ConfigEvent{Config: w.config}) 473 } 474 475 func (w *window) setTitle(prev, cnf Config) { 476 if prev.Title != cnf.Title { 477 w.config.Title = cnf.Title 478 title := stringToNSString(cnf.Title) 479 defer C.CFRelease(title) 480 C.setTitle(C.windowForView(w.view), title) 481 } 482 } 483 484 func (w *window) Perform(acts system.Action) { 485 window := C.windowForView(w.view) 486 walkActions(acts, func(a system.Action) { 487 switch a { 488 case system.ActionCenter: 489 r := C.getScreenFrame(window) // the screen size of the window 490 screenScale := float32(C.getScreenBackingScale()) 491 sz := w.config.Size.Div(int(screenScale)) 492 x := (int(r.size.width) - sz.X) / 2 493 y := (int(r.size.height) - sz.Y) / 2 494 C.setScreenFrame(window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y)) 495 case system.ActionRaise: 496 C.raiseWindow(window) 497 } 498 }) 499 if acts&system.ActionClose != 0 { 500 C.closeWindow(window) 501 } 502 } 503 504 func (w *window) SetCursor(cursor pointer.Cursor) { 505 w.cursor = windowSetCursor(w.cursor, cursor) 506 } 507 508 func (w *window) EditorStateChanged(old, new editorState) { 509 if old.Selection.Range != new.Selection.Range || old.Snippet != new.Snippet { 510 C.discardMarkedText(w.view) 511 w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) 512 } 513 if old.Selection.Caret != new.Selection.Caret || old.Selection.Transform != new.Selection.Transform { 514 C.invalidateCharacterCoordinates(w.view) 515 } 516 } 517 518 func (w *window) ShowTextInput(show bool) {} 519 520 func (w *window) SetInputHint(_ key.InputHint) {} 521 522 func (w *window) SetAnimating(anim bool) { 523 w.anim = anim 524 if w.anim && w.visible { 525 w.displayLink.Start() 526 } else { 527 w.displayLink.Stop() 528 } 529 } 530 531 func (w *window) runOnMain(f func()) { 532 runOnMain(func() { 533 // Make sure the view is still valid. The window might've been closed 534 // during the switch to the main thread. 535 if w.view != 0 { 536 f() 537 } 538 }) 539 } 540 541 //export gio_onKeys 542 func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) { 543 str := nsstringToString(cstr) 544 kmods := convertMods(mods) 545 ks := key.Release 546 if keyDown { 547 ks = key.Press 548 } 549 w := windowFor(h) 550 for _, k := range str { 551 if n, ok := convertKey(k); ok { 552 w.ProcessEvent(key.Event{ 553 Name: n, 554 Modifiers: kmods, 555 State: ks, 556 }) 557 } 558 } 559 } 560 561 //export gio_onText 562 func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) { 563 str := nsstringToString(cstr) 564 w := windowFor(h) 565 w.w.EditorInsert(str) 566 } 567 568 //export gio_onMouse 569 func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) { 570 w := windowFor(h) 571 t := time.Duration(float64(ti)*float64(time.Second) + .5) 572 xf, yf := float32(x)*w.scale, float32(y)*w.scale 573 dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale 574 pos := f32.Point{X: xf, Y: yf} 575 var btn pointer.Buttons 576 switch cbtn { 577 case 0: 578 btn = pointer.ButtonPrimary 579 case 1: 580 btn = pointer.ButtonSecondary 581 case 2: 582 btn = pointer.ButtonTertiary 583 } 584 var typ pointer.Kind 585 switch cdir { 586 case C.MOUSE_MOVE: 587 typ = pointer.Move 588 case C.MOUSE_UP: 589 typ = pointer.Release 590 w.pointerBtns &^= btn 591 case C.MOUSE_DOWN: 592 typ = pointer.Press 593 w.pointerBtns |= btn 594 act, ok := w.w.ActionAt(pos) 595 if ok && w.config.Mode != Fullscreen { 596 switch act { 597 case system.ActionMove: 598 C.performWindowDragWithEvent(C.windowForView(w.view), evt) 599 return 600 } 601 } 602 case C.MOUSE_SCROLL: 603 typ = pointer.Scroll 604 default: 605 panic("invalid direction") 606 } 607 w.ProcessEvent(pointer.Event{ 608 Kind: typ, 609 Source: pointer.Mouse, 610 Time: t, 611 Buttons: w.pointerBtns, 612 Position: pos, 613 Scroll: f32.Point{X: dxf, Y: dyf}, 614 Modifiers: convertMods(mods), 615 }) 616 } 617 618 //export gio_onDraw 619 func gio_onDraw(h C.uintptr_t) { 620 w := windowFor(h) 621 w.draw() 622 } 623 624 //export gio_onFocus 625 func gio_onFocus(h C.uintptr_t, focus C.int) { 626 w := windowFor(h) 627 w.SetCursor(w.cursor) 628 w.config.Focused = focus == 1 629 w.ProcessEvent(ConfigEvent{Config: w.config}) 630 } 631 632 //export gio_onChangeScreen 633 func gio_onChangeScreen(h C.uintptr_t, did uint64) { 634 w := windowFor(h) 635 w.displayLink.SetDisplayID(did) 636 C.setNeedsDisplay(w.view) 637 } 638 639 //export gio_hasMarkedText 640 func gio_hasMarkedText(h C.uintptr_t) C.int { 641 w := windowFor(h) 642 state := w.w.EditorState() 643 if state.compose.Start != -1 { 644 return 1 645 } 646 return 0 647 } 648 649 //export gio_markedRange 650 func gio_markedRange(h C.uintptr_t) C.NSRange { 651 w := windowFor(h) 652 state := w.w.EditorState() 653 rng := state.compose 654 start, end := rng.Start, rng.End 655 if start == -1 { 656 return C.NSMakeRange(C.NSNotFound, 0) 657 } 658 u16start := state.UTF16Index(start) 659 return C.NSMakeRange( 660 C.NSUInteger(u16start), 661 C.NSUInteger(state.UTF16Index(end)-u16start), 662 ) 663 } 664 665 //export gio_selectedRange 666 func gio_selectedRange(h C.uintptr_t) C.NSRange { 667 w := windowFor(h) 668 state := w.w.EditorState() 669 rng := state.Selection 670 start, end := rng.Start, rng.End 671 if start > end { 672 start, end = end, start 673 } 674 u16start := state.UTF16Index(start) 675 return C.NSMakeRange( 676 C.NSUInteger(u16start), 677 C.NSUInteger(state.UTF16Index(end)-u16start), 678 ) 679 } 680 681 //export gio_unmarkText 682 func gio_unmarkText(h C.uintptr_t) { 683 w := windowFor(h) 684 w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) 685 } 686 687 //export gio_setMarkedText 688 func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) { 689 w := windowFor(h) 690 str := nsstringToString(cstr) 691 state := w.w.EditorState() 692 rng := state.compose 693 if rng.Start == -1 { 694 rng = state.Selection.Range 695 } 696 if replaceRange.location != C.NSNotFound { 697 // replaceRange is relative to marked (or selected) text. 698 offset := state.UTF16Index(rng.Start) 699 start := state.RunesIndex(int(replaceRange.location) + offset) 700 end := state.RunesIndex(int(replaceRange.location+replaceRange.length) + offset) 701 rng = key.Range{ 702 Start: start, 703 End: end, 704 } 705 } 706 w.w.EditorReplace(rng, str) 707 comp := key.Range{ 708 Start: rng.Start, 709 End: rng.Start + utf8.RuneCountInString(str), 710 } 711 w.w.SetComposingRegion(comp) 712 713 sel := key.Range{Start: comp.End, End: comp.End} 714 if selRange.location != C.NSNotFound { 715 // selRange is relative to inserted text. 716 offset := state.UTF16Index(rng.Start) 717 start := state.RunesIndex(int(selRange.location) + offset) 718 end := state.RunesIndex(int(selRange.location+selRange.length) + offset) 719 sel = key.Range{ 720 Start: start, 721 End: end, 722 } 723 } 724 w.w.SetEditorSelection(sel) 725 } 726 727 //export gio_substringForProposedRange 728 func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef { 729 w := windowFor(h) 730 state := w.w.EditorState() 731 start, end := state.Snippet.Start, state.Snippet.End 732 if start > end { 733 start, end = end, start 734 } 735 rng := key.Range{ 736 Start: state.RunesIndex(int(crng.location)), 737 End: state.RunesIndex(int(crng.location + crng.length)), 738 } 739 if rng.Start < start || end < rng.End { 740 w.w.SetEditorSnippet(rng) 741 } 742 u16start := state.UTF16Index(start) 743 actual.location = C.NSUInteger(u16start) 744 actual.length = C.NSUInteger(state.UTF16Index(end) - u16start) 745 return stringToNSString(state.Snippet.Text) 746 } 747 748 //export gio_insertText 749 func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) { 750 w := windowFor(h) 751 state := w.w.EditorState() 752 rng := state.compose 753 if rng.Start == -1 { 754 rng = state.Selection.Range 755 } 756 if crng.location != C.NSNotFound { 757 rng = key.Range{ 758 Start: state.RunesIndex(int(crng.location)), 759 End: state.RunesIndex(int(crng.location + crng.length)), 760 } 761 } 762 str := nsstringToString(cstr) 763 w.w.EditorReplace(rng, str) 764 w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) 765 start := rng.Start 766 if rng.End < start { 767 start = rng.End 768 } 769 pos := start + utf8.RuneCountInString(str) 770 w.w.SetEditorSelection(key.Range{Start: pos, End: pos}) 771 } 772 773 //export gio_characterIndexForPoint 774 func gio_characterIndexForPoint(h C.uintptr_t, p C.NSPoint) C.NSUInteger { 775 return C.NSNotFound 776 } 777 778 //export gio_firstRectForCharacterRange 779 func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.NSRect { 780 w := windowFor(h) 781 state := w.w.EditorState() 782 sel := state.Selection 783 u16start := state.UTF16Index(sel.Start) 784 actual.location = C.NSUInteger(u16start) 785 actual.length = 0 786 // Transform to NSView local coordinates (lower left origin, undo backing scale). 787 scale := 1. / float32(C.getViewBackingScale(w.view)) 788 height := float32(C.viewHeight(w.view)) 789 local := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(scale, -scale)).Offset(f32.Pt(0, height)) 790 t := local.Mul(sel.Transform) 791 bounds := f32.Rectangle{ 792 Min: t.Transform(sel.Pos.Sub(f32.Pt(0, sel.Ascent))), 793 Max: t.Transform(sel.Pos.Add(f32.Pt(0, sel.Descent))), 794 }.Canon() 795 sz := bounds.Size() 796 return C.NSMakeRect( 797 C.CGFloat(bounds.Min.X), C.CGFloat(bounds.Min.Y), 798 C.CGFloat(sz.X), C.CGFloat(sz.Y), 799 ) 800 } 801 802 func (w *window) draw() { 803 select { 804 case <-w.redraw: 805 default: 806 } 807 w.visible = true 808 if w.anim { 809 w.SetAnimating(w.anim) 810 } 811 w.scale = float32(C.getViewBackingScale(w.view)) 812 wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view)) 813 sz := image.Point{ 814 X: int(wf*w.scale + .5), 815 Y: int(hf*w.scale + .5), 816 } 817 if sz != w.config.Size { 818 w.config.Size = sz 819 w.ProcessEvent(ConfigEvent{Config: w.config}) 820 } 821 if sz.X == 0 || sz.Y == 0 { 822 return 823 } 824 cfg := configFor(w.scale) 825 w.ProcessEvent(frameEvent{ 826 FrameEvent: FrameEvent{ 827 Now: time.Now(), 828 Size: w.config.Size, 829 Metric: cfg, 830 }, 831 Sync: true, 832 }) 833 } 834 835 func (w *window) ProcessEvent(e event.Event) { 836 w.w.ProcessEvent(e) 837 if w.initialized { 838 w.loop.FlushEvents() 839 } 840 } 841 842 func (w *window) Event() event.Event { 843 return w.loop.Event() 844 } 845 846 func (w *window) Invalidate() { 847 w.loop.Invalidate() 848 } 849 850 func (w *window) Run(f func()) { 851 w.loop.Run(f) 852 } 853 854 func (w *window) Frame(frame *op.Ops) { 855 w.loop.Frame(frame) 856 } 857 858 func configFor(scale float32) unit.Metric { 859 return unit.Metric{ 860 PxPerDp: scale, 861 PxPerSp: scale, 862 } 863 } 864 865 //export gio_onAttached 866 func gio_onAttached(h C.uintptr_t, attached C.int) { 867 w := windowFor(h) 868 if attached != 0 { 869 layer := C.layerForView(w.view) 870 w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)}) 871 } else { 872 w.ProcessEvent(AppKitViewEvent{}) 873 w.visible = false 874 w.SetAnimating(w.anim) 875 } 876 } 877 878 //export gio_onDestroy 879 func gio_onDestroy(h C.uintptr_t) { 880 w := windowFor(h) 881 w.ProcessEvent(DestroyEvent{}) 882 w.displayLink.Close() 883 w.displayLink = nil 884 cgo.Handle(h).Delete() 885 w.view = 0 886 } 887 888 //export gio_onHide 889 func gio_onHide(h C.uintptr_t) { 890 w := windowFor(h) 891 w.visible = false 892 w.SetAnimating(w.anim) 893 } 894 895 //export gio_onShow 896 func gio_onShow(h C.uintptr_t) { 897 w := windowFor(h) 898 w.draw() 899 } 900 901 //export gio_onFullscreen 902 func gio_onFullscreen(h C.uintptr_t) { 903 w := windowFor(h) 904 w.config.Mode = Fullscreen 905 w.ProcessEvent(ConfigEvent{Config: w.config}) 906 } 907 908 //export gio_onWindowed 909 func gio_onWindowed(h C.uintptr_t) { 910 w := windowFor(h) 911 w.config.Mode = Windowed 912 w.ProcessEvent(ConfigEvent{Config: w.config}) 913 } 914 915 //export gio_onFinishLaunching 916 func gio_onFinishLaunching() { 917 close(launched) 918 } 919 920 func newWindow(win *callbacks, options []Option) { 921 <-launched 922 res := make(chan struct{}) 923 runOnMain(func() { 924 w := &window{ 925 redraw: make(chan struct{}, 1), 926 w: win, 927 } 928 w.loop = newEventLoop(w.w, w.wakeup) 929 win.SetDriver(w) 930 if err := w.init(); err != nil { 931 w.ProcessEvent(DestroyEvent{Err: err}) 932 return 933 } 934 window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0) 935 // Release our reference now that the NSWindow has it. 936 C.CFRelease(w.view) 937 w.updateWindowMode() 938 w.Configure(options) 939 if nextTopLeft.x == 0 && nextTopLeft.y == 0 { 940 // cascadeTopLeftFromPoint treats (0, 0) as a no-op, 941 // and just returns the offset we need for the first window. 942 nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft) 943 } 944 nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft) 945 // makeKeyAndOrderFront assumes ownership of our window reference. 946 C.makeKeyAndOrderFront(window) 947 w.initialized = true 948 res <- struct{}{} 949 w.loop.FlushEvents() 950 }) 951 <-res 952 } 953 954 func (w *window) init() error { 955 view := C.gio_createView() 956 if view == 0 { 957 return errors.New("newOSWindow: failed to create view") 958 } 959 scale := float32(C.getViewBackingScale(view)) 960 w.scale = scale 961 dl, err := newDisplayLink(func() { 962 select { 963 case w.redraw <- struct{}{}: 964 default: 965 return 966 } 967 w.runOnMain(func() { 968 if w.visible { 969 C.setNeedsDisplay(w.view) 970 } 971 }) 972 }) 973 w.displayLink = dl 974 if err != nil { 975 C.CFRelease(view) 976 return err 977 } 978 C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w))) 979 w.view = view 980 return nil 981 } 982 983 func osMain() { 984 C.gio_main() 985 } 986 987 func convertKey(k rune) (key.Name, bool) { 988 var n key.Name 989 switch k { 990 case 0x1b: 991 n = key.NameEscape 992 case C.NSLeftArrowFunctionKey: 993 n = key.NameLeftArrow 994 case C.NSRightArrowFunctionKey: 995 n = key.NameRightArrow 996 case C.NSUpArrowFunctionKey: 997 n = key.NameUpArrow 998 case C.NSDownArrowFunctionKey: 999 n = key.NameDownArrow 1000 case 0xd: 1001 n = key.NameReturn 1002 case 0x3: 1003 n = key.NameEnter 1004 case C.NSHomeFunctionKey: 1005 n = key.NameHome 1006 case C.NSEndFunctionKey: 1007 n = key.NameEnd 1008 case 0x7f: 1009 n = key.NameDeleteBackward 1010 case C.NSDeleteFunctionKey: 1011 n = key.NameDeleteForward 1012 case C.NSPageUpFunctionKey: 1013 n = key.NamePageUp 1014 case C.NSPageDownFunctionKey: 1015 n = key.NamePageDown 1016 case C.NSF1FunctionKey: 1017 n = key.NameF1 1018 case C.NSF2FunctionKey: 1019 n = key.NameF2 1020 case C.NSF3FunctionKey: 1021 n = key.NameF3 1022 case C.NSF4FunctionKey: 1023 n = key.NameF4 1024 case C.NSF5FunctionKey: 1025 n = key.NameF5 1026 case C.NSF6FunctionKey: 1027 n = key.NameF6 1028 case C.NSF7FunctionKey: 1029 n = key.NameF7 1030 case C.NSF8FunctionKey: 1031 n = key.NameF8 1032 case C.NSF9FunctionKey: 1033 n = key.NameF9 1034 case C.NSF10FunctionKey: 1035 n = key.NameF10 1036 case C.NSF11FunctionKey: 1037 n = key.NameF11 1038 case C.NSF12FunctionKey: 1039 n = key.NameF12 1040 case 0x09, 0x19: 1041 n = key.NameTab 1042 case 0x20: 1043 n = key.NameSpace 1044 default: 1045 k = unicode.ToUpper(k) 1046 if !unicode.IsPrint(k) { 1047 return "", false 1048 } 1049 n = key.Name(k) 1050 } 1051 return n, true 1052 } 1053 1054 func convertMods(mods C.NSUInteger) key.Modifiers { 1055 var kmods key.Modifiers 1056 if mods&C.NSAlternateKeyMask != 0 { 1057 kmods |= key.ModAlt 1058 } 1059 if mods&C.NSControlKeyMask != 0 { 1060 kmods |= key.ModCtrl 1061 } 1062 if mods&C.NSCommandKeyMask != 0 { 1063 kmods |= key.ModCommand 1064 } 1065 if mods&C.NSShiftKeyMask != 0 { 1066 kmods |= key.ModShift 1067 } 1068 return kmods 1069 } 1070 1071 func (AppKitViewEvent) implementsViewEvent() {} 1072 func (AppKitViewEvent) ImplementsEvent() {}