| // 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 |
| #cgo LDFLAGS: -framework Cocoa -framework OpenGL -framework QuartzCore |
| #import <Carbon/Carbon.h> // for HIToolbox/Events.h |
| #import <Cocoa/Cocoa.h> |
| #include <pthread.h> |
| #include <stdint.h> |
| |
| void startDriver(); |
| void stopDriver(); |
| void makeCurrentContext(uintptr_t ctx); |
| uintptr_t doNewWindow(int width, int height); |
| uintptr_t doShowWindow(uintptr_t id); |
| void doCloseWindow(uintptr_t id); |
| uint64_t threadID(); |
| */ |
| import "C" |
| |
| import ( |
| "log" |
| "runtime" |
| |
| "golang.org/x/exp/shiny/screen" |
| "golang.org/x/mobile/event/key" |
| "golang.org/x/mobile/event/lifecycle" |
| "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" |
| ) |
| |
| 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(width, height int32) uintptr { |
| return uintptr(C.doNewWindow(C.int(width), C.int(height))) |
| } |
| |
| func showWindow(w *windowImpl) { |
| w.glctxMu.Lock() |
| w.glctx, w.worker = gl.NewContext() |
| w.glctxMu.Unlock() |
| |
| w.ctx = uintptr(C.doShowWindow(C.uintptr_t(w.id))) |
| } |
| |
| 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 |
| } |
| w.draw <- struct{}{} |
| <-w.drawDone |
| } |
| |
| // drawLoop is the primary drawing loop. |
| // |
| // After Cocoa has created an NSWindow on the initial OS thread for |
| // processing Cocoa events in doNewWindow, it starts drawLoop on another |
| // goroutine. It is locked to an OS thread for its OpenGL context. |
| // |
| // Two Cocoa threads deliver draw signals to drawLoop. The primary |
| // source of draw events is the CVDisplayLink timer, which is tied to |
| // the display vsync. Secondary draw events come from [NSView drawRect:] |
| // when the window is resized. |
| func drawLoop(w *windowImpl) { |
| runtime.LockOSThread() |
| // TODO(crawshaw): there are several problematic issues around having |
| // a draw loop per window, but resolving them requires some thought. |
| // Firstly, nothing should race on gl.DoWork, so only one person can |
| // do that at a time. Secondly, which GL ctx we use matters. A ctx |
| // carries window-specific state (for example, the current glViewport |
| // value), so we only want to run GL commands on the right context |
| // between a <-w.draw and a <-w.drawDone. Thirdly, some GL functions |
| // can be legitimately called outside of a window draw cycle, for |
| // example, gl.CreateTexture. It doesn't matter which GL ctx we use |
| // for that, but we have to use a valid one. So if a window gets |
| // closed, it's important we swap the default ctx. More work needed. |
| // |
| // Similarly, should each window have its own endPaint channel, or should |
| // the single dedicated draw loop have a single dedicated channel? |
| C.makeCurrentContext(C.uintptr_t(w.ctx)) |
| |
| workAvailable := w.worker.WorkAvailable() |
| |
| // TODO(crawshaw): exit this goroutine on Release. |
| for { |
| select { |
| case <-workAvailable: |
| w.worker.DoWork() |
| case <-w.draw: |
| // TODO(crawshaw): don't send a paint.Event unconditionally. Only |
| // send one if the window actually needs redrawing. |
| w.Send(paint.Event{}) |
| loop: |
| for { |
| select { |
| case <-workAvailable: |
| w.worker.DoWork() |
| case <-w.publish: |
| C.CGLFlushDrawable(C.CGLGetCurrentContext()) |
| break loop |
| } |
| } |
| w.drawDone <- struct{}{} |
| } |
| } |
| } |
| |
| //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, |
| } |
| |
| w.szMu.Lock() |
| w.sz = sz |
| w.szMu.Unlock() |
| |
| w.Send(sz) |
| } |
| |
| //export windowClosing |
| func windowClosing(id uintptr) { |
| theScreen.mu.Lock() |
| w := theScreen.windows[id] |
| delete(theScreen.windows, id) |
| theScreen.mu.Unlock() |
| w.releaseCleanup() |
| } |
| |
| 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}, |
| // TODO key.ControlRight |
| {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, C.kVK_Command, key.ModMeta}, // TODO: missing kVK_RightCommand |
| } |
| |
| 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 float32, ty, button int32, flags uint32) { |
| sendWindowEvent(id, mouse.Event{ |
| X: x, |
| Y: y, |
| Button: cocoaMouseButton(button), |
| Direction: cocoaMouseDir(ty), |
| Modifiers: cocoaMods(flags), |
| }) |
| } |
| |
| func sendLifecycle(to lifecycle.Stage) { |
| log.Printf("sendLifecycle: %v", to) // TODO |
| } |
| |
| //export lifecycleDead |
| func lifecycleDead() { sendLifecycle(lifecycle.StageDead) } |
| |
| //export lifecycleAlive |
| func lifecycleAlive() { sendLifecycle(lifecycle.StageAlive) } |
| |
| //export lifecycleVisible |
| func lifecycleVisible() { sendLifecycle(lifecycle.StageVisible) } |
| |
| //export lifecycleFocused |
| func lifecycleFocused() { sendLifecycle(lifecycle.StageFocused) } |