Files
gio/app/internal/wm/os_macos.go
T
Elias Naur 3fc8f55350 app/internal/wm: [macOS] close display link after window close
The app.Window owner may run SetAnimating just before window close,
which in turn rely on an active display link. This change makes sure
the link is stopped after window close where no more driver calls
can occur.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2021-06-14 23:29:58 +02:00

529 lines
12 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
// +build darwin,!ios
package wm
import (
"errors"
"image"
"runtime"
"time"
"unicode"
"unicode/utf16"
"unsafe"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/unit"
_ "gioui.org/internal/cocoainit"
)
/*
#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
#include <AppKit/AppKit.h>
#define GIO_MOUSE_MOVE 1
#define GIO_MOUSE_UP 2
#define GIO_MOUSE_DOWN 3
#define GIO_MOUSE_SCROLL 4
__attribute__ ((visibility ("hidden"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) CGFloat gio_viewWidth(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CGFloat gio_getViewBackingScale(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CGFloat gio_getScreenBackingScale(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void);
__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length);
__attribute__ ((visibility ("hidden"))) void gio_setNeedsDisplay(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) void gio_toggleFullScreen(CFTypeRef windowRef);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
__attribute__ ((visibility ("hidden"))) void gio_makeKeyAndOrderFront(CFTypeRef windowRef);
__attribute__ ((visibility ("hidden"))) NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft);
__attribute__ ((visibility ("hidden"))) void gio_close(CFTypeRef windowRef);
__attribute__ ((visibility ("hidden"))) void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
__attribute__ ((visibility ("hidden"))) void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
__attribute__ ((visibility ("hidden"))) void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
__attribute__ ((visibility ("hidden"))) void gio_setTitle(CFTypeRef windowRef, const char *title);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_layerForView(CFTypeRef viewRef);
*/
import "C"
func init() {
// Darwin requires that UI operations happen on the main thread only.
runtime.LockOSThread()
}
// ViewEvent notified the client of changes to the window AppKit handles.
// The handles are retained until another ViewEvent is sent.
type ViewEvent struct {
// View is a CFTypeRef for the NSView for the window.
View uintptr
// Layer is a CFTypeRef of the CALayer of View.
Layer uintptr
}
type window struct {
view C.CFTypeRef
window C.CFTypeRef
w Callbacks
stage system.Stage
displayLink *displayLink
cursor pointer.CursorName
scale float32
mode WindowMode
}
// viewMap is the mapping from Cocoa NSViews to Go windows.
var viewMap = make(map[C.CFTypeRef]*window)
// launched is closed when applicationDidFinishLaunching is called.
var launched = make(chan struct{})
// nextTopLeft is the offset to use for the next window's call to
// cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint
// mustView is like lookupView, except that it panics
// if the view isn't mapped.
func mustView(view C.CFTypeRef) *window {
w, ok := lookupView(view)
if !ok {
panic("no window for view")
}
return w
}
func lookupView(view C.CFTypeRef) (*window, bool) {
w, exists := viewMap[view]
if !exists {
return nil, false
}
return w, true
}
func deleteView(view C.CFTypeRef) {
delete(viewMap, view)
}
func insertView(view C.CFTypeRef, w *window) {
viewMap[view] = w
}
func (w *window) contextView() C.CFTypeRef {
return w.view
}
func (w *window) ReadClipboard() {
content := nsstringToString(C.gio_readClipboard())
go w.w.Event(clipboard.Event{Text: content})
}
func (w *window) WriteClipboard(s string) {
u16 := utf16.Encode([]rune(s))
var chars *C.unichar
if len(u16) > 0 {
chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
}
C.gio_writeClipboard(chars, C.NSUInteger(len(u16)))
}
func (w *window) Option(opts *Options) {
screenScale := float32(C.gio_getScreenBackingScale())
cfg := configFor(screenScale)
val := func(v unit.Value) float32 {
return float32(cfg.Px(v)) / screenScale
}
if o := opts.Size; o != nil {
width := val(o.Width)
height := val(o.Height)
if width > 0 || height > 0 {
C.gio_setSize(w.window, C.CGFloat(width), C.CGFloat(height))
}
}
if o := opts.MinSize; o != nil {
width := val(o.Width)
height := val(o.Height)
if width > 0 || height > 0 {
C.gio_setMinSize(w.window, C.CGFloat(width), C.CGFloat(height))
}
}
if o := opts.MaxSize; o != nil {
width := val(o.Width)
height := val(o.Height)
if width > 0 || height > 0 {
C.gio_setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height))
}
}
if o := opts.Title; o != nil {
title := C.CString(*o)
defer C.free(unsafe.Pointer(title))
C.gio_setTitle(w.window, title)
}
if o := opts.WindowMode; o != nil {
w.SetWindowMode(*o)
}
}
func (w *window) SetWindowMode(mode WindowMode) {
switch mode {
case w.mode:
case Windowed, Fullscreen:
C.gio_toggleFullScreen(w.window)
w.mode = mode
}
}
func (w *window) SetCursor(name pointer.CursorName) {
w.cursor = windowSetCursor(w.cursor, name)
}
func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) SetAnimating(anim bool) {
if anim {
w.displayLink.Start()
} else {
w.displayLink.Stop()
}
}
func (w *window) runOnMain(f func()) {
runOnMain(func() {
// Make sure the view is still valid. The window might've been closed
// during the switch to the main thread.
if w.view != 0 {
f()
}
})
}
func (w *window) Close() {
// gio_close immediately calls gio_onClose which sends events
// causing a deadlock because Close is called during an event.
// Break the deadlock by deferring the close, making Close more
// akin to a message like the other platforms.
go runOnMain(func() {
C.gio_close(w.window)
})
}
func (w *window) setStage(stage system.Stage) {
if stage == w.stage {
return
}
w.stage = stage
w.w.Event(system.StageEvent{Stage: stage})
}
//export gio_onKeys
func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger, keyDown C.bool) {
str := C.GoString(cstr)
kmods := convertMods(mods)
ks := key.Release
if keyDown {
ks = key.Press
}
w := mustView(view)
for _, k := range str {
if n, ok := convertKey(k); ok {
w.w.Event(key.Event{
Name: n,
Modifiers: kmods,
State: ks,
})
}
}
}
//export gio_onText
func gio_onText(view C.CFTypeRef, cstr *C.char) {
str := C.GoString(cstr)
w := mustView(view)
w.w.Event(key.EditEvent{Text: str})
}
//export gio_onMouse
func gio_onMouse(view C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
var typ pointer.Type
switch cdir {
case C.GIO_MOUSE_MOVE:
typ = pointer.Move
case C.GIO_MOUSE_UP:
typ = pointer.Release
case C.GIO_MOUSE_DOWN:
typ = pointer.Press
case C.GIO_MOUSE_SCROLL:
typ = pointer.Scroll
default:
panic("invalid direction")
}
var btns pointer.Buttons
if cbtns&(1<<0) != 0 {
btns |= pointer.ButtonPrimary
}
if cbtns&(1<<1) != 0 {
btns |= pointer.ButtonSecondary
}
if cbtns&(1<<2) != 0 {
btns |= pointer.ButtonTertiary
}
t := time.Duration(float64(ti)*float64(time.Second) + .5)
w := mustView(view)
xf, yf := float32(x)*w.scale, float32(y)*w.scale
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
w.w.Event(pointer.Event{
Type: typ,
Source: pointer.Mouse,
Time: t,
Buttons: btns,
Position: f32.Point{X: xf, Y: yf},
Scroll: f32.Point{X: dxf, Y: dyf},
Modifiers: convertMods(mods),
})
}
//export gio_onDraw
func gio_onDraw(view C.CFTypeRef) {
w := mustView(view)
w.draw()
}
//export gio_onFocus
func gio_onFocus(view C.CFTypeRef, focus C.int) {
w := mustView(view)
w.w.Event(key.FocusEvent{Focus: focus == 1})
w.SetCursor(w.cursor)
}
//export gio_onChangeScreen
func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
w := mustView(view)
w.displayLink.SetDisplayID(did)
}
func (w *window) draw() {
w.scale = float32(C.gio_getViewBackingScale(w.view))
wf, hf := float32(C.gio_viewWidth(w.view)), float32(C.gio_viewHeight(w.view))
if wf == 0 || hf == 0 {
return
}
width := int(wf*w.scale + .5)
height := int(hf*w.scale + .5)
cfg := configFor(w.scale)
w.setStage(system.StageRunning)
w.w.Event(FrameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: image.Point{
X: width,
Y: height,
},
Metric: cfg,
},
Sync: true,
})
}
func configFor(scale float32) unit.Metric {
return unit.Metric{
PxPerDp: scale,
PxPerSp: scale,
}
}
//export gio_onClose
func gio_onClose(view C.CFTypeRef) {
w := mustView(view)
w.w.Event(ViewEvent{})
deleteView(view)
w.w.Event(system.DestroyEvent{})
w.displayLink.Close()
w.displayLink = nil
C.CFRelease(w.view)
w.view = 0
C.CFRelease(w.window)
w.window = 0
}
//export gio_onHide
func gio_onHide(view C.CFTypeRef) {
w := mustView(view)
w.setStage(system.StagePaused)
}
//export gio_onShow
func gio_onShow(view C.CFTypeRef) {
w := mustView(view)
w.setStage(system.StageRunning)
}
//export gio_onAppHide
func gio_onAppHide() {
for _, w := range viewMap {
w.setStage(system.StagePaused)
}
}
//export gio_onAppShow
func gio_onAppShow() {
for _, w := range viewMap {
w.setStage(system.StageRunning)
}
}
//export gio_onFinishLaunching
func gio_onFinishLaunching() {
close(launched)
}
func NewWindow(win Callbacks, opts *Options) error {
<-launched
errch := make(chan error)
runOnMain(func() {
w, err := newWindow(opts)
if err != nil {
errch <- err
return
}
errch <- nil
w.w = win
w.window = C.gio_createWindow(w.view, nil, 0, 0, 0, 0, 0, 0)
win.SetDriver(w)
w.Option(opts)
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
// and just returns the offset we need for the first window.
nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft)
}
nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft)
C.gio_makeKeyAndOrderFront(w.window)
layer := C.gio_layerForView(w.view)
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
})
return <-errch
}
func newWindow(opts *Options) (*window, error) {
view := C.gio_createView()
if view == 0 {
return nil, errors.New("CreateWindow: failed to create view")
}
scale := float32(C.gio_getViewBackingScale(view))
w := &window{
view: view,
scale: scale,
}
dl, err := NewDisplayLink(func() {
w.runOnMain(func() {
C.gio_setNeedsDisplay(w.view)
})
})
w.displayLink = dl
if err != nil {
C.CFRelease(view)
return nil, err
}
insertView(view, w)
return w, nil
}
func Main() {
C.gio_main()
}
func convertKey(k rune) (string, bool) {
var n string
switch k {
case 0x1b:
n = key.NameEscape
case C.NSLeftArrowFunctionKey:
n = key.NameLeftArrow
case C.NSRightArrowFunctionKey:
n = key.NameRightArrow
case C.NSUpArrowFunctionKey:
n = key.NameUpArrow
case C.NSDownArrowFunctionKey:
n = key.NameDownArrow
case 0xd:
n = key.NameReturn
case 0x3:
n = key.NameEnter
case C.NSHomeFunctionKey:
n = key.NameHome
case C.NSEndFunctionKey:
n = key.NameEnd
case 0x7f:
n = key.NameDeleteBackward
case C.NSDeleteFunctionKey:
n = key.NameDeleteForward
case C.NSPageUpFunctionKey:
n = key.NamePageUp
case C.NSPageDownFunctionKey:
n = key.NamePageDown
case C.NSF1FunctionKey:
n = "F1"
case C.NSF2FunctionKey:
n = "F2"
case C.NSF3FunctionKey:
n = "F3"
case C.NSF4FunctionKey:
n = "F4"
case C.NSF5FunctionKey:
n = "F5"
case C.NSF6FunctionKey:
n = "F6"
case C.NSF7FunctionKey:
n = "F7"
case C.NSF8FunctionKey:
n = "F8"
case C.NSF9FunctionKey:
n = "F9"
case C.NSF10FunctionKey:
n = "F10"
case C.NSF11FunctionKey:
n = "F11"
case C.NSF12FunctionKey:
n = "F12"
case 0x09, 0x19:
n = key.NameTab
case 0x20:
n = key.NameSpace
default:
k = unicode.ToUpper(k)
if !unicode.IsPrint(k) {
return "", false
}
n = string(k)
}
return n, true
}
func convertMods(mods C.NSUInteger) key.Modifiers {
var kmods key.Modifiers
if mods&C.NSAlternateKeyMask != 0 {
kmods |= key.ModAlt
}
if mods&C.NSControlKeyMask != 0 {
kmods |= key.ModCtrl
}
if mods&C.NSCommandKeyMask != 0 {
kmods |= key.ModCommand
}
if mods&C.NSShiftKeyMask != 0 {
kmods |= key.ModShift
}
return kmods
}
func (_ ViewEvent) ImplementsEvent() {}