1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
|
// Copyright 2019 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.
//go:build darwin
// +build darwin
// Package mtldriver provides a Metal driver for accessing a screen.
//
// At this time, the Metal API is used only to present the final pixels
// to the screen. All rendering is performed on the CPU via the image/draw
// algorithms. Future work is to use mtl.Buffer, mtl.Texture, etc., and
// do more of the rendering work on the GPU.
package mtldriver
import (
"runtime"
"unsafe"
"dmitri.shuralyov.com/gpu/mtl"
"github.com/go-gl/glfw/v3.3/glfw"
"golang.org/x/exp/shiny/driver/internal/errscreen"
"golang.org/x/exp/shiny/driver/mtldriver/internal/appkit"
"golang.org/x/exp/shiny/driver/mtldriver/internal/coreanim"
"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"
)
func init() {
runtime.LockOSThread()
}
// Main is called by the program's main function to run the graphical
// application.
//
// It calls f on the Screen, possibly in a separate goroutine, as some OS-
// specific libraries require being on 'the main thread'. It returns when f
// returns.
func Main(f func(screen.Screen)) {
if err := main(f); err != nil {
f(errscreen.Stub(err))
}
}
func main(f func(screen.Screen)) error {
device, err := mtl.CreateSystemDefaultDevice()
if err != nil {
return err
}
err = glfw.Init()
if err != nil {
return err
}
defer glfw.Terminate()
glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI)
{
// TODO(dmitshur): Delete this when https://github.com/go-gl/glfw/issues/272 is resolved.
// Post an empty event from the main thread before it can happen in a non-main thread,
// to work around https://github.com/glfw/glfw/issues/1649.
glfw.PostEmptyEvent()
}
var (
done = make(chan struct{})
newWindowCh = make(chan newWindowReq, 1)
releaseWindowCh = make(chan releaseWindowReq, 1)
)
go func() {
f(&screenImpl{
newWindowCh: newWindowCh,
})
close(done)
glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on done.
}()
for {
select {
case <-done:
return nil
case req := <-newWindowCh:
w, err := newWindow(device, releaseWindowCh, req.opts)
req.respCh <- newWindowResp{w, err}
case req := <-releaseWindowCh:
req.window.Destroy()
req.respCh <- struct{}{}
default:
glfw.WaitEvents()
}
}
}
type newWindowReq struct {
opts *screen.NewWindowOptions
respCh chan newWindowResp
}
type newWindowResp struct {
w screen.Window
err error
}
type releaseWindowReq struct {
window *glfw.Window
respCh chan struct{}
}
// newWindow creates a new GLFW window.
// It must be called on the main thread.
func newWindow(device mtl.Device, releaseWindowCh chan releaseWindowReq, opts *screen.NewWindowOptions) (screen.Window, error) {
width, height := optsSize(opts)
window, err := glfw.CreateWindow(width, height, opts.GetTitle(), nil, nil)
if err != nil {
return nil, err
}
ml := coreanim.MakeMetalLayer()
ml.SetDevice(device)
ml.SetPixelFormat(mtl.PixelFormatBGRA8UNorm)
ml.SetMaximumDrawableCount(3)
ml.SetDisplaySyncEnabled(true)
cv := appkit.NewWindow(unsafe.Pointer(window.GetCocoaWindow())).ContentView()
cv.SetLayer(ml)
cv.SetWantsLayer(true)
w := &windowImpl{
device: device,
window: window,
releaseWindowCh: releaseWindowCh,
ml: ml,
cq: device.MakeCommandQueue(),
}
// Set callbacks.
framebufferSizeCallback := func(_ *glfw.Window, width, height int) {
w.Send(size.Event{
WidthPx: width,
HeightPx: height,
// TODO(dmitshur): ppp,
})
w.Send(paint.Event{External: true})
}
window.SetFramebufferSizeCallback(framebufferSizeCallback)
window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
const scale = 2 // TODO(dmitshur): compute dynamically
w.Send(mouse.Event{X: float32(x * scale), Y: float32(y * scale)})
})
window.SetMouseButtonCallback(func(_ *glfw.Window, b glfw.MouseButton, a glfw.Action, mods glfw.ModifierKey) {
btn := glfwMouseButton(b)
if btn == mouse.ButtonNone {
return
}
const scale = 2 // TODO(dmitshur): compute dynamically
x, y := window.GetCursorPos()
w.Send(mouse.Event{
X: float32(x * scale), Y: float32(y * scale),
Button: btn,
Direction: glfwMouseDirection(a),
// TODO(dmitshur): set Modifiers
})
})
window.SetKeyCallback(func(_ *glfw.Window, k glfw.Key, _ int, a glfw.Action, mods glfw.ModifierKey) {
code := glfwKeyCode(k)
if code == key.CodeUnknown {
return
}
w.Send(key.Event{
Code: code,
Direction: glfwKeyDirection(a),
// TODO(dmitshur): set Modifiers
})
})
// TODO(dmitshur): set CharModsCallback to catch text (runes) that are typed,
// and perhaps try to unify key pressed + character typed into single event
window.SetCloseCallback(func(*glfw.Window) {
w.lifecycler.SetDead(true)
w.lifecycler.SendEvent(w, nil)
})
// TODO(dmitshur): more fine-grained tracking of whether window is visible and/or focused
w.lifecycler.SetDead(false)
w.lifecycler.SetVisible(true)
w.lifecycler.SetFocused(true)
w.lifecycler.SendEvent(w, nil)
// Send the initial size and paint events.
width, height = window.GetFramebufferSize()
framebufferSizeCallback(window, width, height)
return w, nil
}
func optsSize(opts *screen.NewWindowOptions) (width, height int) {
width, height = 1024/2, 768/2
if opts != nil {
if opts.Width > 0 {
width = opts.Width
}
if opts.Height > 0 {
height = opts.Height
}
}
return width, height
}
func glfwMouseButton(button glfw.MouseButton) mouse.Button {
switch button {
case glfw.MouseButtonLeft:
return mouse.ButtonLeft
case glfw.MouseButtonRight:
return mouse.ButtonRight
case glfw.MouseButtonMiddle:
return mouse.ButtonMiddle
default:
return mouse.ButtonNone
}
}
func glfwMouseDirection(action glfw.Action) mouse.Direction {
switch action {
case glfw.Press:
return mouse.DirPress
case glfw.Release:
return mouse.DirRelease
default:
panic("unreachable")
}
}
func glfwKeyCode(k glfw.Key) key.Code {
// TODO(dmitshur): support more keys
switch k {
case glfw.KeyEnter:
return key.CodeReturnEnter
case glfw.KeyEscape:
return key.CodeEscape
default:
return key.CodeUnknown
}
}
func glfwKeyDirection(action glfw.Action) key.Direction {
switch action {
case glfw.Press:
return key.DirPress
case glfw.Release:
return key.DirRelease
case glfw.Repeat:
return key.DirNone
default:
panic("unreachable")
}
}
|