blob: 21062cbe395ea1b2ba26044d62fb2c8c837adc10 [file] [log] [blame]
// 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) }