mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
533bf953f9
According to https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html#//apple_ref/doc/uid/20000738-125024 Cocoa is by default not multithread-safe for programs that use the posix api for creating threads: " For multithreaded applications, Cocoa frameworks use locks and other forms of internal synchronization to ensure they behave correctly. To prevent these locks from degrading performance in the single-threaded case, however, Cocoa does not create them until the application spawns its first new thread using the NSThread class. If you spawn threads using only POSIX thread routines, Cocoa does not receive the notifications it needs to know that your application is now multithreaded. When that happens, operations involving the Cocoa frameworks may destabilize or crash your application. " That includes Go programs. The fix, as discovered by Steeve Morin, is to create and launch an empty NSThread. Add a package that does that, and use it everywhere Cocoa is used. Sigh. Signed-off-by: Elias Naur <mail@eliasnaur.com>
381 lines
7.6 KiB
Go
381 lines
7.6 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
// +build darwin,!ios
|
|
|
|
package window
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
"unicode"
|
|
"unsafe"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/io/system"
|
|
|
|
_ "gioui.org/app/internal/cocoainit"
|
|
)
|
|
|
|
/*
|
|
#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
|
|
|
|
|
|
#include <AppKit/AppKit.h>
|
|
#include "os_macos.h"
|
|
*/
|
|
import "C"
|
|
|
|
func init() {
|
|
// Darwin requires that UI operations happen on the main thread only.
|
|
runtime.LockOSThread()
|
|
}
|
|
|
|
type window struct {
|
|
view C.CFTypeRef
|
|
w Callbacks
|
|
stage system.Stage
|
|
scale float32
|
|
}
|
|
|
|
type viewCmd struct {
|
|
view C.CFTypeRef
|
|
f viewFunc
|
|
}
|
|
|
|
type viewFunc func(views viewMap, view C.CFTypeRef)
|
|
|
|
type viewMap map[C.CFTypeRef]*window
|
|
|
|
var (
|
|
viewOnce sync.Once
|
|
viewCmds = make(chan viewCmd)
|
|
viewAcks = make(chan struct{})
|
|
)
|
|
|
|
var mainWindow = newWindowRendezvous()
|
|
|
|
var viewFactory func() C.CFTypeRef
|
|
|
|
func viewDo(view C.CFTypeRef, f viewFunc) {
|
|
viewOnce.Do(func() {
|
|
go runViewCmdLoop()
|
|
})
|
|
viewCmds <- viewCmd{view, f}
|
|
<-viewAcks
|
|
}
|
|
|
|
func runViewCmdLoop() {
|
|
views := make(viewMap)
|
|
for {
|
|
select {
|
|
case cmd := <-viewCmds:
|
|
cmd.f(views, cmd.view)
|
|
viewAcks <- struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *window) contextView() C.CFTypeRef {
|
|
return w.view
|
|
}
|
|
|
|
func (w *window) ShowTextInput(show bool) {}
|
|
|
|
func (w *window) SetAnimating(anim bool) {
|
|
var animb C.BOOL
|
|
if anim {
|
|
animb = 1
|
|
}
|
|
C.gio_setAnimating(w.view, animb)
|
|
}
|
|
|
|
func (w *window) setStage(stage system.Stage) {
|
|
if stage == w.stage {
|
|
return
|
|
}
|
|
w.stage = stage
|
|
w.w.Event(system.StageEvent{Stage: stage})
|
|
}
|
|
|
|
// Use a top level func for onFrameCallback to avoid
|
|
// garbage from viewDo.
|
|
func onFrameCmd(views viewMap, view C.CFTypeRef) {
|
|
// CVDisplayLink does not run on the main thread,
|
|
// so we have to ignore requests to windows being
|
|
// deleted.
|
|
if w, exists := views[view]; exists {
|
|
w.draw(false)
|
|
}
|
|
}
|
|
|
|
//export gio_onFrameCallback
|
|
func gio_onFrameCallback(view C.CFTypeRef) {
|
|
viewDo(view, onFrameCmd)
|
|
}
|
|
|
|
//export gio_onKeys
|
|
func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger) {
|
|
str := C.GoString(cstr)
|
|
kmods := convertMods(mods)
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
w := views[view]
|
|
for _, k := range str {
|
|
if n, ok := convertKey(k); ok {
|
|
w.w.Event(key.Event{
|
|
Name: n,
|
|
Modifiers: kmods,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
//export gio_onText
|
|
func gio_onText(view C.CFTypeRef, cstr *C.char) {
|
|
str := C.GoString(cstr)
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
w := views[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
|
|
default:
|
|
panic("invalid direction")
|
|
}
|
|
var btns pointer.Buttons
|
|
if cbtns&(1<<0) != 0 {
|
|
btns |= pointer.ButtonLeft
|
|
}
|
|
if cbtns&(1<<1) != 0 {
|
|
btns |= pointer.ButtonRight
|
|
}
|
|
if cbtns&(1<<2) != 0 {
|
|
btns |= pointer.ButtonMiddle
|
|
}
|
|
t := time.Duration(float64(ti)*float64(time.Second) + .5)
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
w := views[view]
|
|
x, y := float32(x)*w.scale, float32(y)*w.scale
|
|
dx, dy := 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: x, Y: y},
|
|
Scroll: f32.Point{X: dx, Y: dy},
|
|
Modifiers: convertMods(mods),
|
|
})
|
|
})
|
|
}
|
|
|
|
//export gio_onDraw
|
|
func gio_onDraw(view C.CFTypeRef) {
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
if w, exists := views[view]; exists {
|
|
w.draw(true)
|
|
}
|
|
})
|
|
}
|
|
|
|
//export gio_onFocus
|
|
func gio_onFocus(view C.CFTypeRef, focus C.BOOL) {
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
w := views[view]
|
|
w.w.Event(key.FocusEvent{Focus: focus == C.YES})
|
|
})
|
|
}
|
|
|
|
func (w *window) draw(sync bool) {
|
|
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)
|
|
cfg.now = time.Now()
|
|
w.setStage(system.StageRunning)
|
|
w.w.Event(FrameEvent{
|
|
FrameEvent: system.FrameEvent{
|
|
Size: image.Point{
|
|
X: width,
|
|
Y: height,
|
|
},
|
|
Config: &cfg,
|
|
},
|
|
Sync: sync,
|
|
})
|
|
}
|
|
|
|
func configFor(scale float32) config {
|
|
return config{
|
|
pxPerDp: scale,
|
|
pxPerSp: scale,
|
|
}
|
|
}
|
|
|
|
//export gio_onTerminate
|
|
func gio_onTerminate(view C.CFTypeRef) {
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
w := views[view]
|
|
delete(views, view)
|
|
w.w.Event(system.DestroyEvent{})
|
|
})
|
|
}
|
|
|
|
//export gio_onHide
|
|
func gio_onHide(view C.CFTypeRef) {
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
w := views[view]
|
|
w.setStage(system.StagePaused)
|
|
})
|
|
}
|
|
|
|
//export gio_onShow
|
|
func gio_onShow(view C.CFTypeRef) {
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
w := views[view]
|
|
w.setStage(system.StageRunning)
|
|
})
|
|
}
|
|
|
|
//export gio_onCreate
|
|
func gio_onCreate(view C.CFTypeRef) {
|
|
viewDo(view, func(views viewMap, view C.CFTypeRef) {
|
|
scale := float32(C.gio_getViewBackingScale(view))
|
|
w := &window{
|
|
view: view,
|
|
scale: scale,
|
|
}
|
|
wopts := <-mainWindow.out
|
|
w.w = wopts.window
|
|
w.w.SetDriver(w)
|
|
views[view] = w
|
|
})
|
|
}
|
|
|
|
func NewWindow(win Callbacks, opts *Options) error {
|
|
mainWindow.in <- windowAndOptions{win, opts}
|
|
return <-mainWindow.errs
|
|
}
|
|
|
|
func Main() {
|
|
wopts := <-mainWindow.out
|
|
view := viewFactory()
|
|
if view == 0 {
|
|
// TODO: return this error from CreateWindow.
|
|
panic(errors.New("CreateWindow: failed to create view"))
|
|
}
|
|
// Window sizes is in unscaled screen coordinates, not device pixels.
|
|
cfg := configFor(1.0)
|
|
opts := wopts.opts
|
|
w := cfg.Px(opts.Width)
|
|
h := cfg.Px(opts.Height)
|
|
w = int(float32(w))
|
|
h = int(float32(h))
|
|
title := C.CString(opts.Title)
|
|
defer C.free(unsafe.Pointer(title))
|
|
C.gio_main(view, title, C.CGFloat(w), C.CGFloat(h))
|
|
}
|
|
|
|
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 = "Space"
|
|
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
|
|
}
|