github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/app/internal/wm/os_macos.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 // +build darwin,!ios 4 5 package wm 6 7 import ( 8 "errors" 9 "image" 10 "runtime" 11 "time" 12 "unicode" 13 "unicode/utf16" 14 "unsafe" 15 16 "github.com/cybriq/giocore/f32" 17 "github.com/cybriq/giocore/io/clipboard" 18 "github.com/cybriq/giocore/io/key" 19 "github.com/cybriq/giocore/io/pointer" 20 "github.com/cybriq/giocore/io/system" 21 "github.com/cybriq/giocore/unit" 22 23 _ "github.com/cybriq/giocore/internal/cocoainit" 24 ) 25 26 /* 27 #cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c 28 29 #include <AppKit/AppKit.h> 30 31 #define GIO_MOUSE_MOVE 1 32 #define GIO_MOUSE_UP 2 33 #define GIO_MOUSE_DOWN 3 34 #define GIO_MOUSE_SCROLL 4 35 36 __attribute__ ((visibility ("hidden"))) void gio_main(void); 37 __attribute__ ((visibility ("hidden"))) CGFloat gio_viewWidth(CFTypeRef viewRef); 38 __attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef); 39 __attribute__ ((visibility ("hidden"))) CGFloat gio_getViewBackingScale(CFTypeRef viewRef); 40 __attribute__ ((visibility ("hidden"))) CGFloat gio_getScreenBackingScale(void); 41 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void); 42 __attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length); 43 __attribute__ ((visibility ("hidden"))) void gio_setNeedsDisplay(CFTypeRef viewRef); 44 __attribute__ ((visibility ("hidden"))) void gio_toggleFullScreen(CFTypeRef windowRef); 45 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void); 46 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); 47 __attribute__ ((visibility ("hidden"))) void gio_makeKeyAndOrderFront(CFTypeRef windowRef); 48 __attribute__ ((visibility ("hidden"))) NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft); 49 __attribute__ ((visibility ("hidden"))) void gio_close(CFTypeRef windowRef); 50 __attribute__ ((visibility ("hidden"))) void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height); 51 __attribute__ ((visibility ("hidden"))) void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height); 52 __attribute__ ((visibility ("hidden"))) void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height); 53 __attribute__ ((visibility ("hidden"))) void gio_setTitle(CFTypeRef windowRef, const char *title); 54 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_layerForView(CFTypeRef viewRef); 55 */ 56 import "C" 57 58 func init() { 59 // Darwin requires that UI operations happen on the main thread only. 60 runtime.LockOSThread() 61 } 62 63 // ViewEvent notified the client of changes to the window AppKit handles. 64 // The handles are retained until another ViewEvent is sent. 65 type ViewEvent struct { 66 // View is a CFTypeRef for the NSView for the window. 67 View uintptr 68 // Layer is a CFTypeRef of the CALayer of View. 69 Layer uintptr 70 } 71 72 type window struct { 73 view C.CFTypeRef 74 window C.CFTypeRef 75 w Callbacks 76 stage system.Stage 77 displayLink *displayLink 78 cursor pointer.CursorName 79 80 scale float32 81 mode WindowMode 82 } 83 84 // viewMap is the mapping from Cocoa NSViews to Go windows. 85 var viewMap = make(map[C.CFTypeRef]*window) 86 87 // launched is closed when applicationDidFinishLaunching is called. 88 var launched = make(chan struct{}) 89 90 // nextTopLeft is the offset to use for the next window's call to 91 // cascadeTopLeftFromPoint. 92 var nextTopLeft C.NSPoint 93 94 // mustView is like lookupView, except that it panics 95 // if the view isn't mapped. 96 func mustView(view C.CFTypeRef) *window { 97 w, ok := lookupView(view) 98 if !ok { 99 panic("no window for view") 100 } 101 return w 102 } 103 104 func lookupView(view C.CFTypeRef) (*window, bool) { 105 w, exists := viewMap[view] 106 if !exists { 107 return nil, false 108 } 109 return w, true 110 } 111 112 func deleteView(view C.CFTypeRef) { 113 delete(viewMap, view) 114 } 115 116 func insertView(view C.CFTypeRef, w *window) { 117 viewMap[view] = w 118 } 119 120 func (w *window) contextView() C.CFTypeRef { 121 return w.view 122 } 123 124 func (w *window) ReadClipboard() { 125 content := nsstringToString(C.gio_readClipboard()) 126 go w.w.Event(clipboard.Event{Text: content}) 127 } 128 129 func (w *window) WriteClipboard(s string) { 130 u16 := utf16.Encode([]rune(s)) 131 var chars *C.unichar 132 if len(u16) > 0 { 133 chars = (*C.unichar)(unsafe.Pointer(&u16[0])) 134 } 135 C.gio_writeClipboard(chars, C.NSUInteger(len(u16))) 136 } 137 138 func (w *window) Option(opts *Options) { 139 screenScale := float32(C.gio_getScreenBackingScale()) 140 cfg := configFor(screenScale) 141 val := func(v unit.Value) float32 { 142 return float32(cfg.Px(v)) / screenScale 143 } 144 if o := opts.Size; o != nil { 145 width := val(o.Width) 146 height := val(o.Height) 147 if width > 0 || height > 0 { 148 C.gio_setSize(w.window, C.CGFloat(width), C.CGFloat(height)) 149 } 150 } 151 if o := opts.MinSize; o != nil { 152 width := val(o.Width) 153 height := val(o.Height) 154 if width > 0 || height > 0 { 155 C.gio_setMinSize(w.window, C.CGFloat(width), C.CGFloat(height)) 156 } 157 } 158 if o := opts.MaxSize; o != nil { 159 width := val(o.Width) 160 height := val(o.Height) 161 if width > 0 || height > 0 { 162 C.gio_setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height)) 163 } 164 } 165 if o := opts.Title; o != nil { 166 title := C.CString(*o) 167 defer C.free(unsafe.Pointer(title)) 168 C.gio_setTitle(w.window, title) 169 } 170 if o := opts.WindowMode; o != nil { 171 w.SetWindowMode(*o) 172 } 173 } 174 175 func (w *window) SetWindowMode(mode WindowMode) { 176 switch mode { 177 case w.mode: 178 case Windowed, Fullscreen: 179 C.gio_toggleFullScreen(w.window) 180 w.mode = mode 181 } 182 } 183 184 func (w *window) SetCursor(name pointer.CursorName) { 185 w.cursor = windowSetCursor(w.cursor, name) 186 } 187 188 func (w *window) ShowTextInput(show bool) {} 189 190 func (w *window) SetInputHint(_ key.InputHint) {} 191 192 func (w *window) SetAnimating(anim bool) { 193 if anim { 194 w.displayLink.Start() 195 } else { 196 w.displayLink.Stop() 197 } 198 } 199 200 func (w *window) runOnMain(f func()) { 201 runOnMain(func() { 202 // Make sure the view is still valid. The window might've been closed 203 // during the switch to the main thread. 204 if w.view != 0 { 205 f() 206 } 207 }) 208 } 209 210 func (w *window) Close() { 211 // gio_close immediately calls gio_onClose which sends events 212 // causing a deadlock because Close is called during an event. 213 // Break the deadlock by deferring the close, making Close more 214 // akin to a message like the other platforms. 215 go runOnMain(func() { 216 C.gio_close(w.window) 217 }) 218 } 219 220 func (w *window) setStage(stage system.Stage) { 221 if stage == w.stage { 222 return 223 } 224 w.stage = stage 225 w.w.Event(system.StageEvent{Stage: stage}) 226 } 227 228 //export gio_onKeys 229 func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger, keyDown C.bool) { 230 str := C.GoString(cstr) 231 kmods := convertMods(mods) 232 ks := key.Release 233 if keyDown { 234 ks = key.Press 235 } 236 w := mustView(view) 237 for _, k := range str { 238 if n, ok := convertKey(k); ok { 239 w.w.Event(key.Event{ 240 Name: n, 241 Modifiers: kmods, 242 State: ks, 243 }) 244 } 245 } 246 } 247 248 //export gio_onText 249 func gio_onText(view C.CFTypeRef, cstr *C.char) { 250 str := C.GoString(cstr) 251 w := mustView(view) 252 w.w.Event(key.EditEvent{Text: str}) 253 } 254 255 //export gio_onMouse 256 func gio_onMouse(view C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) { 257 var typ pointer.Type 258 switch cdir { 259 case C.GIO_MOUSE_MOVE: 260 typ = pointer.Move 261 case C.GIO_MOUSE_UP: 262 typ = pointer.Release 263 case C.GIO_MOUSE_DOWN: 264 typ = pointer.Press 265 case C.GIO_MOUSE_SCROLL: 266 typ = pointer.Scroll 267 default: 268 panic("invalid direction") 269 } 270 var btns pointer.Buttons 271 if cbtns&(1<<0) != 0 { 272 btns |= pointer.ButtonPrimary 273 } 274 if cbtns&(1<<1) != 0 { 275 btns |= pointer.ButtonSecondary 276 } 277 if cbtns&(1<<2) != 0 { 278 btns |= pointer.ButtonTertiary 279 } 280 t := time.Duration(float64(ti)*float64(time.Second) + .5) 281 w := mustView(view) 282 xf, yf := float32(x)*w.scale, float32(y)*w.scale 283 dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale 284 w.w.Event(pointer.Event{ 285 Type: typ, 286 Source: pointer.Mouse, 287 Time: t, 288 Buttons: btns, 289 Position: f32.Point{X: xf, Y: yf}, 290 Scroll: f32.Point{X: dxf, Y: dyf}, 291 Modifiers: convertMods(mods), 292 }) 293 } 294 295 //export gio_onDraw 296 func gio_onDraw(view C.CFTypeRef) { 297 w := mustView(view) 298 w.draw() 299 } 300 301 //export gio_onFocus 302 func gio_onFocus(view C.CFTypeRef, focus C.int) { 303 w := mustView(view) 304 w.w.Event(key.FocusEvent{Focus: focus == 1}) 305 w.SetCursor(w.cursor) 306 } 307 308 //export gio_onChangeScreen 309 func gio_onChangeScreen(view C.CFTypeRef, did uint64) { 310 w := mustView(view) 311 w.displayLink.SetDisplayID(did) 312 } 313 314 func (w *window) draw() { 315 w.scale = float32(C.gio_getViewBackingScale(w.view)) 316 wf, hf := float32(C.gio_viewWidth(w.view)), float32(C.gio_viewHeight(w.view)) 317 if wf == 0 || hf == 0 { 318 return 319 } 320 width := int(wf*w.scale + .5) 321 height := int(hf*w.scale + .5) 322 cfg := configFor(w.scale) 323 w.setStage(system.StageRunning) 324 w.w.Event(FrameEvent{ 325 FrameEvent: system.FrameEvent{ 326 Now: time.Now(), 327 Size: image.Point{ 328 X: width, 329 Y: height, 330 }, 331 Metric: cfg, 332 }, 333 Sync: true, 334 }) 335 } 336 337 func configFor(scale float32) unit.Metric { 338 return unit.Metric{ 339 PxPerDp: scale, 340 PxPerSp: scale, 341 } 342 } 343 344 //export gio_onClose 345 func gio_onClose(view C.CFTypeRef) { 346 w := mustView(view) 347 w.w.Event(ViewEvent{}) 348 deleteView(view) 349 w.w.Event(system.DestroyEvent{}) 350 w.displayLink.Close() 351 w.displayLink = nil 352 C.CFRelease(w.view) 353 w.view = 0 354 C.CFRelease(w.window) 355 w.window = 0 356 } 357 358 //export gio_onHide 359 func gio_onHide(view C.CFTypeRef) { 360 w := mustView(view) 361 w.setStage(system.StagePaused) 362 } 363 364 //export gio_onShow 365 func gio_onShow(view C.CFTypeRef) { 366 w := mustView(view) 367 w.setStage(system.StageRunning) 368 } 369 370 //export gio_onAppHide 371 func gio_onAppHide() { 372 for _, w := range viewMap { 373 w.setStage(system.StagePaused) 374 } 375 } 376 377 //export gio_onAppShow 378 func gio_onAppShow() { 379 for _, w := range viewMap { 380 w.setStage(system.StageRunning) 381 } 382 } 383 384 //export gio_onFinishLaunching 385 func gio_onFinishLaunching() { 386 close(launched) 387 } 388 389 func NewWindow(win Callbacks, opts *Options) error { 390 <-launched 391 errch := make(chan error) 392 runOnMain(func() { 393 w, err := newWindow(opts) 394 if err != nil { 395 errch <- err 396 return 397 } 398 errch <- nil 399 w.w = win 400 w.window = C.gio_createWindow(w.view, nil, 0, 0, 0, 0, 0, 0) 401 win.SetDriver(w) 402 w.Option(opts) 403 if nextTopLeft.x == 0 && nextTopLeft.y == 0 { 404 // cascadeTopLeftFromPoint treats (0, 0) as a no-op, 405 // and just returns the offset we need for the first window. 406 nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft) 407 } 408 nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft) 409 C.gio_makeKeyAndOrderFront(w.window) 410 layer := C.gio_layerForView(w.view) 411 w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)}) 412 }) 413 return <-errch 414 } 415 416 func newWindow(opts *Options) (*window, error) { 417 view := C.gio_createView() 418 if view == 0 { 419 return nil, errors.New("CreateWindow: failed to create view") 420 } 421 scale := float32(C.gio_getViewBackingScale(view)) 422 w := &window{ 423 view: view, 424 scale: scale, 425 } 426 dl, err := NewDisplayLink(func() { 427 w.runOnMain(func() { 428 C.gio_setNeedsDisplay(w.view) 429 }) 430 }) 431 w.displayLink = dl 432 if err != nil { 433 C.CFRelease(view) 434 return nil, err 435 } 436 insertView(view, w) 437 return w, nil 438 } 439 440 func Main() { 441 C.gio_main() 442 } 443 444 func convertKey(k rune) (string, bool) { 445 var n string 446 switch k { 447 case 0x1b: 448 n = key.NameEscape 449 case C.NSLeftArrowFunctionKey: 450 n = key.NameLeftArrow 451 case C.NSRightArrowFunctionKey: 452 n = key.NameRightArrow 453 case C.NSUpArrowFunctionKey: 454 n = key.NameUpArrow 455 case C.NSDownArrowFunctionKey: 456 n = key.NameDownArrow 457 case 0xd: 458 n = key.NameReturn 459 case 0x3: 460 n = key.NameEnter 461 case C.NSHomeFunctionKey: 462 n = key.NameHome 463 case C.NSEndFunctionKey: 464 n = key.NameEnd 465 case 0x7f: 466 n = key.NameDeleteBackward 467 case C.NSDeleteFunctionKey: 468 n = key.NameDeleteForward 469 case C.NSPageUpFunctionKey: 470 n = key.NamePageUp 471 case C.NSPageDownFunctionKey: 472 n = key.NamePageDown 473 case C.NSF1FunctionKey: 474 n = "F1" 475 case C.NSF2FunctionKey: 476 n = "F2" 477 case C.NSF3FunctionKey: 478 n = "F3" 479 case C.NSF4FunctionKey: 480 n = "F4" 481 case C.NSF5FunctionKey: 482 n = "F5" 483 case C.NSF6FunctionKey: 484 n = "F6" 485 case C.NSF7FunctionKey: 486 n = "F7" 487 case C.NSF8FunctionKey: 488 n = "F8" 489 case C.NSF9FunctionKey: 490 n = "F9" 491 case C.NSF10FunctionKey: 492 n = "F10" 493 case C.NSF11FunctionKey: 494 n = "F11" 495 case C.NSF12FunctionKey: 496 n = "F12" 497 case 0x09, 0x19: 498 n = key.NameTab 499 case 0x20: 500 n = key.NameSpace 501 default: 502 k = unicode.ToUpper(k) 503 if !unicode.IsPrint(k) { 504 return "", false 505 } 506 n = string(k) 507 } 508 return n, true 509 } 510 511 func convertMods(mods C.NSUInteger) key.Modifiers { 512 var kmods key.Modifiers 513 if mods&C.NSAlternateKeyMask != 0 { 514 kmods |= key.ModAlt 515 } 516 if mods&C.NSControlKeyMask != 0 { 517 kmods |= key.ModCtrl 518 } 519 if mods&C.NSCommandKeyMask != 0 { 520 kmods |= key.ModCommand 521 } 522 if mods&C.NSShiftKeyMask != 0 { 523 kmods |= key.ModShift 524 } 525 return kmods 526 } 527 528 func (_ ViewEvent) ImplementsEvent() {}