| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // +build darwin |
| // +build 386 amd64 |
| // +build !ios |
| |
| package gldriver |
| |
| /* |
| #cgo CFLAGS: -x objective-c -DGL_SILENCE_DEPRECATION |
| #cgo LDFLAGS: -framework Cocoa -framework OpenGL |
| #include <OpenGL/gl3.h> |
| #import <Carbon/Carbon.h> // for HIToolbox/Events.h |
| #import <Cocoa/Cocoa.h> |
| #include <pthread.h> |
| #include <stdint.h> |
| #include <stdlib.h> |
| |
| void startDriver(); |
| void stopDriver(); |
| void makeCurrentContext(uintptr_t ctx); |
| void flushContext(uintptr_t ctx); |
| uintptr_t doNewWindow(int width, int height, char* title); |
| void doShowWindow(uintptr_t id); |
| void doCloseWindow(uintptr_t id); |
| uint64_t threadID(); |
| */ |
| import "C" |
| |
| import ( |
| "errors" |
| "fmt" |
| "log" |
| "runtime" |
| "unsafe" |
| |
| "golang.org/x/exp/shiny/driver/internal/lifecycler" |
| "golang.org/x/exp/shiny/screen" |
| "golang.org/x/mobile/event/key" |
| "golang.org/x/mobile/event/mouse" |
| "golang.org/x/mobile/event/paint" |
| "golang.org/x/mobile/event/size" |
| "golang.org/x/mobile/geom" |
| "golang.org/x/mobile/gl" |
| ) |
| |
| const useLifecycler = true |
| |
| // TODO: change this to true, after manual testing on OS X. |
| const handleSizeEventsAtChannelReceive = false |
| |
| var initThreadID C.uint64_t |
| |
| func init() { |
| // Lock the goroutine responsible for initialization to an OS thread. |
| // This means the goroutine running main (and calling startDriver below) |
| // is locked to the OS thread that started the program. This is |
| // necessary for the correct delivery of Cocoa events to the process. |
| // |
| // A discussion on this topic: |
| // https://groups.google.com/forum/#!msg/golang-nuts/IiWZ2hUuLDA/SNKYYZBelsYJ |
| runtime.LockOSThread() |
| initThreadID = C.threadID() |
| } |
| |
| func newWindow(opts *screen.NewWindowOptions) (uintptr, error) { |
| width, height := optsSize(opts) |
| |
| title := C.CString(opts.GetTitle()) |
| defer C.free(unsafe.Pointer(title)) |
| |
| return uintptr(C.doNewWindow(C.int(width), C.int(height), title)), nil |
| } |
| |
| func initWindow(w *windowImpl) { |
| w.glctx, w.worker = gl.NewContext() |
| } |
| |
| func showWindow(w *windowImpl) { |
| C.doShowWindow(C.uintptr_t(w.id)) |
| } |
| |
| //export preparedOpenGL |
| func preparedOpenGL(id, ctx, vba uintptr) { |
| theScreen.mu.Lock() |
| w := theScreen.windows[id] |
| theScreen.mu.Unlock() |
| |
| w.ctx = ctx |
| go drawLoop(w, vba) |
| } |
| |
| func closeWindow(id uintptr) { |
| C.doCloseWindow(C.uintptr_t(id)) |
| } |
| |
| var mainCallback func(screen.Screen) |
| |
| func main(f func(screen.Screen)) error { |
| if tid := C.threadID(); tid != initThreadID { |
| log.Fatalf("gldriver.Main called on thread %d, but gldriver.init ran on %d", tid, initThreadID) |
| } |
| |
| mainCallback = f |
| C.startDriver() |
| return nil |
| } |
| |
| //export driverStarted |
| func driverStarted() { |
| go func() { |
| mainCallback(theScreen) |
| C.stopDriver() |
| }() |
| } |
| |
| //export drawgl |
| func drawgl(id uintptr) { |
| theScreen.mu.Lock() |
| w := theScreen.windows[id] |
| theScreen.mu.Unlock() |
| |
| if w == nil { |
| return // closing window |
| } |
| |
| // TODO: is this necessary? |
| w.lifecycler.SetVisible(true) |
| w.lifecycler.SendEvent(w, w.glctx) |
| |
| w.Send(paint.Event{External: true}) |
| <-w.drawDone |
| } |
| |
| // drawLoop is the primary drawing loop. |
| // |
| // After Cocoa has created an NSWindow and called prepareOpenGL, |
| // it starts drawLoop on a locked goroutine to handle OpenGL calls. |
| // |
| // The screen is drawn every time a paint.Event is received, which can be |
| // triggered either by the user or by Cocoa via drawgl (for example, when |
| // the window is resized). |
| func drawLoop(w *windowImpl, vba uintptr) { |
| runtime.LockOSThread() |
| C.makeCurrentContext(C.uintptr_t(w.ctx.(uintptr))) |
| |
| // Starting in OS X 10.11 (El Capitan), the vertex array is |
| // occasionally getting unbound when the context changes threads. |
| // |
| // Avoid this by binding it again. |
| C.glBindVertexArray(C.GLuint(vba)) |
| if errno := C.glGetError(); errno != 0 { |
| panic(fmt.Sprintf("gldriver: glBindVertexArray failed: %d", errno)) |
| } |
| |
| workAvailable := w.worker.WorkAvailable() |
| |
| // TODO(crawshaw): exit this goroutine on Release. |
| for { |
| select { |
| case <-workAvailable: |
| w.worker.DoWork() |
| case <-w.publish: |
| loop: |
| for { |
| select { |
| case <-workAvailable: |
| w.worker.DoWork() |
| default: |
| break loop |
| } |
| } |
| C.flushContext(C.uintptr_t(w.ctx.(uintptr))) |
| w.publishDone <- screen.PublishResult{} |
| } |
| } |
| } |
| |
| //export setGeom |
| func setGeom(id uintptr, ppp float32, widthPx, heightPx int) { |
| theScreen.mu.Lock() |
| w := theScreen.windows[id] |
| theScreen.mu.Unlock() |
| |
| if w == nil { |
| return // closing window |
| } |
| |
| sz := size.Event{ |
| WidthPx: widthPx, |
| HeightPx: heightPx, |
| WidthPt: geom.Pt(float32(widthPx) / ppp), |
| HeightPt: geom.Pt(float32(heightPx) / ppp), |
| PixelsPerPt: ppp, |
| } |
| |
| if !handleSizeEventsAtChannelReceive { |
| w.szMu.Lock() |
| w.sz = sz |
| w.szMu.Unlock() |
| } |
| |
| w.Send(sz) |
| } |
| |
| //export windowClosing |
| func windowClosing(id uintptr) { |
| sendLifecycle(id, (*lifecycler.State).SetDead, true) |
| } |
| |
| func sendWindowEvent(id uintptr, e interface{}) { |
| theScreen.mu.Lock() |
| w := theScreen.windows[id] |
| theScreen.mu.Unlock() |
| |
| if w == nil { |
| return // closing window |
| } |
| w.Send(e) |
| } |
| |
| var mods = [...]struct { |
| flags uint32 |
| code uint16 |
| mod key.Modifiers |
| }{ |
| // Left and right variants of modifier keys have their own masks, |
| // but they are not documented. These were determined empirically. |
| {1<<17 | 0x102, C.kVK_Shift, key.ModShift}, |
| {1<<17 | 0x104, C.kVK_RightShift, key.ModShift}, |
| {1<<18 | 0x101, C.kVK_Control, key.ModControl}, |
| {33<<13 | 0x100, C.kVK_RightControl, key.ModControl}, |
| {1<<19 | 0x120, C.kVK_Option, key.ModAlt}, |
| {1<<19 | 0x140, C.kVK_RightOption, key.ModAlt}, |
| {1<<20 | 0x108, C.kVK_Command, key.ModMeta}, |
| {1<<20 | 0x110, 0x36 /* kVK_RightCommand */, key.ModMeta}, |
| } |
| |
| func cocoaMods(flags uint32) (m key.Modifiers) { |
| for _, mod := range mods { |
| if flags&mod.flags == mod.flags { |
| m |= mod.mod |
| } |
| } |
| return m |
| } |
| |
| func cocoaMouseDir(ty int32) mouse.Direction { |
| switch ty { |
| case C.NSLeftMouseDown, C.NSRightMouseDown, C.NSOtherMouseDown: |
| return mouse.DirPress |
| case C.NSLeftMouseUp, C.NSRightMouseUp, C.NSOtherMouseUp: |
| return mouse.DirRelease |
| default: // dragged |
| return mouse.DirNone |
| } |
| } |
| |
| func cocoaMouseButton(button int32) mouse.Button { |
| switch button { |
| case 0: |
| return mouse.ButtonLeft |
| case 1: |
| return mouse.ButtonRight |
| case 2: |
| return mouse.ButtonMiddle |
| default: |
| return mouse.ButtonNone |
| } |
| } |
| |
| //export mouseEvent |
| func mouseEvent(id uintptr, x, y, dx, dy float32, ty, button int32, flags uint32) { |
| cmButton := mouse.ButtonNone |
| switch ty { |
| default: |
| cmButton = cocoaMouseButton(button) |
| case C.NSMouseMoved, C.NSLeftMouseDragged, C.NSRightMouseDragged, C.NSOtherMouseDragged: |
| // No-op. |
| case C.NSScrollWheel: |
| // Note that the direction of scrolling is inverted by default |
| // on OS X by the "natural scrolling" setting. At the Cocoa |
| // level this inversion is applied to trackpads and mice behind |
| // the scenes, and the value of dy goes in the direction the OS |
| // wants scrolling to go. |
| // |
| // This means the same trackpad/mouse motion on OS X and Linux |
| // can produce wheel events in opposite directions, but the |
| // direction matches what other programs on the OS do. |
| // |
| // If we wanted to expose the phsyical device motion in the |
| // event we could use [NSEvent isDirectionInvertedFromDevice] |
| // to know if "natural scrolling" is enabled. |
| // |
| // TODO: On a trackpad, a scroll can be a drawn-out affair with a |
| // distinct beginning and end. Should the intermediate events be |
| // DirNone? |
| // |
| // TODO: handle horizontal scrolling |
| button := mouse.ButtonWheelUp |
| if dy < 0 { |
| dy = -dy |
| button = mouse.ButtonWheelDown |
| } |
| e := mouse.Event{ |
| X: x, |
| Y: y, |
| Button: button, |
| Direction: mouse.DirStep, |
| Modifiers: cocoaMods(flags), |
| } |
| for delta := int(dy); delta != 0; delta-- { |
| sendWindowEvent(id, e) |
| } |
| return |
| } |
| sendWindowEvent(id, mouse.Event{ |
| X: x, |
| Y: y, |
| Button: cmButton, |
| Direction: cocoaMouseDir(ty), |
| Modifiers: cocoaMods(flags), |
| }) |
| } |
| |
| //export keyEvent |
| func keyEvent(id uintptr, runeVal rune, dir uint8, code uint16, flags uint32) { |
| sendWindowEvent(id, key.Event{ |
| Rune: cocoaRune(runeVal), |
| Direction: key.Direction(dir), |
| Code: cocoaKeyCode(code), |
| Modifiers: cocoaMods(flags), |
| }) |
| } |
| |
| //export flagEvent |
| func flagEvent(id uintptr, flags uint32) { |
| for _, mod := range mods { |
| if flags&mod.flags == mod.flags && lastFlags&mod.flags != mod.flags { |
| keyEvent(id, -1, C.NSKeyDown, mod.code, flags) |
| } |
| if lastFlags&mod.flags == mod.flags && flags&mod.flags != mod.flags { |
| keyEvent(id, -1, C.NSKeyUp, mod.code, flags) |
| } |
| } |
| lastFlags = flags |
| } |
| |
| var lastFlags uint32 |
| |
| func sendLifecycle(id uintptr, setter func(*lifecycler.State, bool), val bool) { |
| theScreen.mu.Lock() |
| w := theScreen.windows[id] |
| theScreen.mu.Unlock() |
| |
| if w == nil { |
| return |
| } |
| setter(&w.lifecycler, val) |
| w.lifecycler.SendEvent(w, w.glctx) |
| } |
| |
| func sendLifecycleAll(dead bool) { |
| windows := []*windowImpl{} |
| |
| theScreen.mu.Lock() |
| for _, w := range theScreen.windows { |
| windows = append(windows, w) |
| } |
| theScreen.mu.Unlock() |
| |
| for _, w := range windows { |
| w.lifecycler.SetFocused(false) |
| w.lifecycler.SetVisible(false) |
| if dead { |
| w.lifecycler.SetDead(true) |
| } |
| w.lifecycler.SendEvent(w, w.glctx) |
| } |
| } |
| |
| //export lifecycleDeadAll |
| func lifecycleDeadAll() { sendLifecycleAll(true) } |
| |
| //export lifecycleHideAll |
| func lifecycleHideAll() { sendLifecycleAll(false) } |
| |
| //export lifecycleVisible |
| func lifecycleVisible(id uintptr, val bool) { |
| sendLifecycle(id, (*lifecycler.State).SetVisible, val) |
| } |
| |
| //export lifecycleFocused |
| func lifecycleFocused(id uintptr, val bool) { |
| sendLifecycle(id, (*lifecycler.State).SetFocused, val) |
| } |
| |
| // cocoaRune marks the Carbon/Cocoa private-range unicode rune representing |
| // a non-unicode key event to -1, used for Rune in the key package. |
| // |
| // http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT |
| func cocoaRune(r rune) rune { |
| if '\uE000' <= r && r <= '\uF8FF' { |
| return -1 |
| } |
| return r |
| } |
| |
| // cocoaKeyCode converts a Carbon/Cocoa virtual key code number |
| // into the standard keycodes used by the key package. |
| // |
| // To get a sense of the key map, see the diagram on |
| // http://boredzo.org/blog/archives/2007-05-22/virtual-key-codes |
| func cocoaKeyCode(vkcode uint16) key.Code { |
| switch vkcode { |
| case C.kVK_ANSI_A: |
| return key.CodeA |
| case C.kVK_ANSI_B: |
| return key.CodeB |
| case C.kVK_ANSI_C: |
| return key.CodeC |
| case C.kVK_ANSI_D: |
| return key.CodeD |
| case C.kVK_ANSI_E: |
| return key.CodeE |
| case C.kVK_ANSI_F: |
| return key.CodeF |
| case C.kVK_ANSI_G: |
| return key.CodeG |
| case C.kVK_ANSI_H: |
| return key.CodeH |
| case C.kVK_ANSI_I: |
| return key.CodeI |
| case C.kVK_ANSI_J: |
| return key.CodeJ |
| case C.kVK_ANSI_K: |
| return key.CodeK |
| case C.kVK_ANSI_L: |
| return key.CodeL |
| case C.kVK_ANSI_M: |
| return key.CodeM |
| case C.kVK_ANSI_N: |
| return key.CodeN |
| case C.kVK_ANSI_O: |
| return key.CodeO |
| case C.kVK_ANSI_P: |
| return key.CodeP |
| case C.kVK_ANSI_Q: |
| return key.CodeQ |
| case C.kVK_ANSI_R: |
| return key.CodeR |
| case C.kVK_ANSI_S: |
| return key.CodeS |
| case C.kVK_ANSI_T: |
| return key.CodeT |
| case C.kVK_ANSI_U: |
| return key.CodeU |
| case C.kVK_ANSI_V: |
| return key.CodeV |
| case C.kVK_ANSI_W: |
| return key.CodeW |
| case C.kVK_ANSI_X: |
| return key.CodeX |
| case C.kVK_ANSI_Y: |
| return key.CodeY |
| case C.kVK_ANSI_Z: |
| return key.CodeZ |
| case C.kVK_ANSI_1: |
| return key.Code1 |
| case C.kVK_ANSI_2: |
| return key.Code2 |
| case C.kVK_ANSI_3: |
| return key.Code3 |
| case C.kVK_ANSI_4: |
| return key.Code4 |
| case C.kVK_ANSI_5: |
| return key.Code5 |
| case C.kVK_ANSI_6: |
| return key.Code6 |
| case C.kVK_ANSI_7: |
| return key.Code7 |
| case C.kVK_ANSI_8: |
| return key.Code8 |
| case C.kVK_ANSI_9: |
| return key.Code9 |
| case C.kVK_ANSI_0: |
| return key.Code0 |
| // TODO: move the rest of these codes to constants in key.go |
| // if we are happy with them. |
| case C.kVK_Return: |
| return key.CodeReturnEnter |
| case C.kVK_Escape: |
| return key.CodeEscape |
| case C.kVK_Delete: |
| return key.CodeDeleteBackspace |
| case C.kVK_Tab: |
| return key.CodeTab |
| case C.kVK_Space: |
| return key.CodeSpacebar |
| case C.kVK_ANSI_Minus: |
| return key.CodeHyphenMinus |
| case C.kVK_ANSI_Equal: |
| return key.CodeEqualSign |
| case C.kVK_ANSI_LeftBracket: |
| return key.CodeLeftSquareBracket |
| case C.kVK_ANSI_RightBracket: |
| return key.CodeRightSquareBracket |
| case C.kVK_ANSI_Backslash: |
| return key.CodeBackslash |
| // 50: Keyboard Non-US "#" and ~ |
| case C.kVK_ANSI_Semicolon: |
| return key.CodeSemicolon |
| case C.kVK_ANSI_Quote: |
| return key.CodeApostrophe |
| case C.kVK_ANSI_Grave: |
| return key.CodeGraveAccent |
| case C.kVK_ANSI_Comma: |
| return key.CodeComma |
| case C.kVK_ANSI_Period: |
| return key.CodeFullStop |
| case C.kVK_ANSI_Slash: |
| return key.CodeSlash |
| case C.kVK_CapsLock: |
| return key.CodeCapsLock |
| case C.kVK_F1: |
| return key.CodeF1 |
| case C.kVK_F2: |
| return key.CodeF2 |
| case C.kVK_F3: |
| return key.CodeF3 |
| case C.kVK_F4: |
| return key.CodeF4 |
| case C.kVK_F5: |
| return key.CodeF5 |
| case C.kVK_F6: |
| return key.CodeF6 |
| case C.kVK_F7: |
| return key.CodeF7 |
| case C.kVK_F8: |
| return key.CodeF8 |
| case C.kVK_F9: |
| return key.CodeF9 |
| case C.kVK_F10: |
| return key.CodeF10 |
| case C.kVK_F11: |
| return key.CodeF11 |
| case C.kVK_F12: |
| return key.CodeF12 |
| // 70: PrintScreen |
| // 71: Scroll Lock |
| // 72: Pause |
| // 73: Insert |
| case C.kVK_Home: |
| return key.CodeHome |
| case C.kVK_PageUp: |
| return key.CodePageUp |
| case C.kVK_ForwardDelete: |
| return key.CodeDeleteForward |
| case C.kVK_End: |
| return key.CodeEnd |
| case C.kVK_PageDown: |
| return key.CodePageDown |
| case C.kVK_RightArrow: |
| return key.CodeRightArrow |
| case C.kVK_LeftArrow: |
| return key.CodeLeftArrow |
| case C.kVK_DownArrow: |
| return key.CodeDownArrow |
| case C.kVK_UpArrow: |
| return key.CodeUpArrow |
| case C.kVK_ANSI_KeypadClear: |
| return key.CodeKeypadNumLock |
| case C.kVK_ANSI_KeypadDivide: |
| return key.CodeKeypadSlash |
| case C.kVK_ANSI_KeypadMultiply: |
| return key.CodeKeypadAsterisk |
| case C.kVK_ANSI_KeypadMinus: |
| return key.CodeKeypadHyphenMinus |
| case C.kVK_ANSI_KeypadPlus: |
| return key.CodeKeypadPlusSign |
| case C.kVK_ANSI_KeypadEnter: |
| return key.CodeKeypadEnter |
| case C.kVK_ANSI_Keypad1: |
| return key.CodeKeypad1 |
| case C.kVK_ANSI_Keypad2: |
| return key.CodeKeypad2 |
| case C.kVK_ANSI_Keypad3: |
| return key.CodeKeypad3 |
| case C.kVK_ANSI_Keypad4: |
| return key.CodeKeypad4 |
| case C.kVK_ANSI_Keypad5: |
| return key.CodeKeypad5 |
| case C.kVK_ANSI_Keypad6: |
| return key.CodeKeypad6 |
| case C.kVK_ANSI_Keypad7: |
| return key.CodeKeypad7 |
| case C.kVK_ANSI_Keypad8: |
| return key.CodeKeypad8 |
| case C.kVK_ANSI_Keypad9: |
| return key.CodeKeypad9 |
| case C.kVK_ANSI_Keypad0: |
| return key.CodeKeypad0 |
| case C.kVK_ANSI_KeypadDecimal: |
| return key.CodeKeypadFullStop |
| case C.kVK_ANSI_KeypadEquals: |
| return key.CodeKeypadEqualSign |
| case C.kVK_F13: |
| return key.CodeF13 |
| case C.kVK_F14: |
| return key.CodeF14 |
| case C.kVK_F15: |
| return key.CodeF15 |
| case C.kVK_F16: |
| return key.CodeF16 |
| case C.kVK_F17: |
| return key.CodeF17 |
| case C.kVK_F18: |
| return key.CodeF18 |
| case C.kVK_F19: |
| return key.CodeF19 |
| case C.kVK_F20: |
| return key.CodeF20 |
| // 116: Keyboard Execute |
| case C.kVK_Help: |
| return key.CodeHelp |
| // 118: Keyboard Menu |
| // 119: Keyboard Select |
| // 120: Keyboard Stop |
| // 121: Keyboard Again |
| // 122: Keyboard Undo |
| // 123: Keyboard Cut |
| // 124: Keyboard Copy |
| // 125: Keyboard Paste |
| // 126: Keyboard Find |
| case C.kVK_Mute: |
| return key.CodeMute |
| case C.kVK_VolumeUp: |
| return key.CodeVolumeUp |
| case C.kVK_VolumeDown: |
| return key.CodeVolumeDown |
| // 130: Keyboard Locking Caps Lock |
| // 131: Keyboard Locking Num Lock |
| // 132: Keyboard Locking Scroll Lock |
| // 133: Keyboard Comma |
| // 134: Keyboard Equal Sign |
| // ...: Bunch of stuff |
| case C.kVK_Control: |
| return key.CodeLeftControl |
| case C.kVK_Shift: |
| return key.CodeLeftShift |
| case C.kVK_Option: |
| return key.CodeLeftAlt |
| case C.kVK_Command: |
| return key.CodeLeftGUI |
| case C.kVK_RightControl: |
| return key.CodeRightControl |
| case C.kVK_RightShift: |
| return key.CodeRightShift |
| case C.kVK_RightOption: |
| return key.CodeRightAlt |
| // TODO key.CodeRightGUI |
| default: |
| return key.CodeUnknown |
| } |
| } |
| |
| func surfaceCreate() error { |
| return errors.New("gldriver: surface creation not implemented on darwin") |
| } |