app,app/internal/wm: introduce app.Window.Run and use it internally

app.Window implements a method for safely running functions against the
underlying native window through the driverFuncs channel. However, the
functions still run in a different goroutine than the one driving the
native event loop, which forces the implementations in package wm to do
complicated synchronization.

A previous change added a mechanism to run functions in the native event
loop thread. The macOS port needed this functionality, but with some
care it can be generalized. That's what this change does through the
new Run method.

The advantage is that the thread switch dance is now confined to
app.Window, with the help of a generic wm.Driver.Wakeup method. All
other Driver methods can then assume they run on their event loop
threads.

Run is exported because it is also needed for programs that use
Windows configured with CustomRenderer to control their own rendering.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-05-18 12:34:58 +01:00
parent c914935169
commit 8611894b4b
14 changed files with 283 additions and 380 deletions
+4 -1
View File
@@ -72,7 +72,10 @@ func (c *d3d11Context) Present() error {
} }
func (c *d3d11Context) MakeCurrent() error { func (c *d3d11Context) MakeCurrent() error {
_, width, height := c.win.HWND() var width, height int
c.win.w.Run(func() {
_, width, height = c.win.HWND()
})
if c.renderTarget != nil && width == c.width && height == c.height { if c.renderTarget != nil && width == c.width && height == c.height {
c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView) c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView)
return nil return nil
+10 -1
View File
@@ -3,6 +3,7 @@
package wm package wm
/* /*
#include <android/native_window_jni.h>
#include <EGL/egl.h> #include <EGL/egl.h>
*/ */
import "C" import "C"
@@ -35,7 +36,15 @@ func (c *context) Release() {
func (c *context) MakeCurrent() error { func (c *context) MakeCurrent() error {
c.Context.ReleaseSurface() c.Context.ReleaseSurface()
win, width, height := c.win.nativeWindow(c.Context.VisualID()) var (
win *C.ANativeWindow
width, height int
)
// Run on main thread. Deadlock is avoided because MakeCurrent is only
// called during a FrameEvent.
c.win.callbacks.Run(func() {
win, width, height = c.win.nativeWindow(c.Context.VisualID())
})
if win == nil { if win == nil {
return nil return nil
} }
+9 -1
View File
@@ -3,6 +3,8 @@
package wm package wm
import ( import (
"golang.org/x/sys/windows"
"gioui.org/internal/egl" "gioui.org/internal/egl"
) )
@@ -34,7 +36,13 @@ func (c *glContext) Release() {
func (c *glContext) MakeCurrent() error { func (c *glContext) MakeCurrent() error {
c.Context.ReleaseSurface() c.Context.ReleaseSurface()
win, width, height := c.win.HWND() var (
win windows.Handle
width, height int
)
c.win.w.Run(func() {
win, width, height = c.win.HWND()
})
eglSurf := egl.NativeWindowType(win) eglSurf := egl.NativeWindowType(win)
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err return err
+1 -1
View File
@@ -43,7 +43,7 @@ func newContext(w *window) (*context, error) {
} }
// [NSOpenGLContext setView] must run on the main thread. Fortunately, // [NSOpenGLContext setView] must run on the main thread. Fortunately,
// newContext is only called during a [NSView draw] on the main thread. // newContext is only called during a [NSView draw] on the main thread.
w.w.Func(func() { w.w.Run(func() {
C.gio_setContextView(ctx, view) C.gio_setContextView(ctx, view)
}) })
c := &context{ c := &context{
+33 -98
View File
@@ -42,7 +42,6 @@ import "C"
import ( import (
"errors" "errors"
"fmt" "fmt"
"gioui.org/internal/f32color"
"image" "image"
"image/color" "image/color"
"reflect" "reflect"
@@ -53,6 +52,8 @@ import (
"unicode/utf16" "unicode/utf16"
"unsafe" "unsafe"
"gioui.org/internal/f32color"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard" "gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
@@ -70,24 +71,11 @@ type window struct {
fontScale float32 fontScale float32
insets system.Insets insets system.Insets
stage system.Stage stage system.Stage
started bool started bool
state, newState windowState
// mu protects the fields following it.
mu sync.Mutex
win *C.ANativeWindow
animating bool animating bool
}
// windowState tracks the View or Activity specific state lost when Android win *C.ANativeWindow
// re-creates our Activity.
type windowState struct {
cursor *pointer.CursorName
orientation *Orientation
navigationColor *color.NRGBA
statusColor *color.NRGBA
} }
// gioView hold cached JNI methods for GioView. // gioView hold cached JNI methods for GioView.
@@ -247,7 +235,6 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
views[handle] = w views[handle] = w
w.loadConfig(env, class) w.loadConfig(env, class)
w.Option(wopts.opts) w.Option(wopts.opts)
applyStateDiff(env, view, windowState{}, w.state)
w.setStage(system.StagePaused) w.setStage(system.StagePaused)
w.callbacks.Event(ViewEvent{View: uintptr(view)}) w.callbacks.Event(ViewEvent{View: uintptr(view)})
return handle return handle
@@ -274,7 +261,7 @@ func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, handle C.j
func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.jlong) { func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := views[handle] w := views[handle]
w.started = true w.started = true
if w.aNativeWindow() != nil { if w.win != nil {
w.setVisible() w.setVisible()
} }
} }
@@ -282,18 +269,14 @@ func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.
//export Java_org_gioui_GioView_onSurfaceDestroyed //export Java_org_gioui_GioView_onSurfaceDestroyed
func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) { func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := views[handle] w := views[handle]
w.mu.Lock()
w.win = nil w.win = nil
w.mu.Unlock()
w.setStage(system.StagePaused) w.setStage(system.StagePaused)
} }
//export Java_org_gioui_GioView_onSurfaceChanged //export Java_org_gioui_GioView_onSurfaceChanged
func Java_org_gioui_GioView_onSurfaceChanged(env *C.JNIEnv, class C.jclass, handle C.jlong, surf C.jobject) { func Java_org_gioui_GioView_onSurfaceChanged(env *C.JNIEnv, class C.jclass, handle C.jlong, surf C.jobject) {
w := views[handle] w := views[handle]
w.mu.Lock()
w.win = C.ANativeWindow_fromSurface(env, surf) w.win = C.ANativeWindow_fromSurface(env, surf)
w.mu.Unlock()
if w.started { if w.started {
w.setVisible() w.setVisible()
} }
@@ -323,9 +306,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if w.stage < system.StageRunning { if w.stage < system.StageRunning {
return return
} }
w.mu.Lock()
anim := w.animating anim := w.animating
w.mu.Unlock()
if anim { if anim {
runInJVM(javaVM(), func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
callVoidMethod(env, w.view, gioView.postFrameCallback) callVoidMethod(env, w.view, gioView.postFrameCallback)
@@ -348,7 +329,7 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
//export Java_org_gioui_GioView_onFocusChange //export Java_org_gioui_GioView_onFocusChange
func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) { func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) {
w := views[view] w := views[view]
w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE}) go w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
} }
//export Java_org_gioui_GioView_onWindowInsets //export Java_org_gioui_GioView_onWindowInsets
@@ -366,8 +347,7 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C
} }
func (w *window) setVisible() { func (w *window) setVisible() {
win := w.aNativeWindow() width, height := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win)
width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win)
if width == 0 || height == 0 { if width == 0 || height == 0 {
return return
} }
@@ -384,22 +364,15 @@ func (w *window) setStage(stage system.Stage) {
} }
func (w *window) nativeWindow(visID int) (*C.ANativeWindow, int, int) { func (w *window) nativeWindow(visID int) (*C.ANativeWindow, int, int) {
win := w.aNativeWindow()
var width, height int var width, height int
if win != nil { if w.win != nil {
if C.ANativeWindow_setBuffersGeometry(win, 0, 0, C.int32_t(visID)) != 0 { if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 {
panic(errors.New("ANativeWindow_setBuffersGeometry failed")) panic(errors.New("ANativeWindow_setBuffersGeometry failed"))
} }
w, h := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) w, h := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win)
width, height = int(w), int(h) width, height = int(w), int(h)
} }
return win, width, height return w.win, width, height
}
func (w *window) aNativeWindow() *C.ANativeWindow {
w.mu.Lock()
defer w.mu.Unlock()
return w.win
} }
func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) { func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) {
@@ -417,23 +390,16 @@ func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) {
} }
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
w.mu.Lock()
w.animating = anim w.animating = anim
w.mu.Unlock()
if anim { if anim {
runOnMain(func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
if w.view == 0 {
// View was destroyed while switching to main thread.
return
}
callVoidMethod(env, w.view, gioView.postFrameCallback) callVoidMethod(env, w.view, gioView.postFrameCallback)
}) })
} }
} }
func (w *window) draw(sync bool) { func (w *window) draw(sync bool) {
win := w.aNativeWindow() width, height := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win)
width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win)
if width == 0 || height == 0 { if width == 0 || height == 0 {
return return
} }
@@ -567,10 +533,7 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
} }
func (w *window) ShowTextInput(show bool) { func (w *window) ShowTextInput(show bool) {
runOnMain(func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
if w.view == 0 {
return
}
if show { if show {
callVoidMethod(env, w.view, gioView.showTextInput) callVoidMethod(env, w.view, gioView.showTextInput)
} else { } else {
@@ -669,7 +632,7 @@ func NewWindow(window Callbacks, opts *Options) error {
} }
func (w *window) WriteClipboard(s string) { func (w *window) WriteClipboard(s string) {
runOnMain(func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
jstr := javaString(env, s) jstr := javaString(env, s)
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
jvalue(android.appCtx), jvalue(jstr)) jvalue(android.appCtx), jvalue(jstr))
@@ -677,71 +640,43 @@ func (w *window) WriteClipboard(s string) {
} }
func (w *window) ReadClipboard() { func (w *window) ReadClipboard() {
runOnMain(func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
c, err := callStaticObjectMethod(env, android.gioCls, android.mreadClipboard, c, err := callStaticObjectMethod(env, android.gioCls, android.mreadClipboard,
jvalue(android.appCtx)) jvalue(android.appCtx))
if err != nil { if err != nil {
return return
} }
content := goString(env, C.jstring(c)) content := goString(env, C.jstring(c))
w.callbacks.Event(clipboard.Event{Text: content}) go w.callbacks.Event(clipboard.Event{Text: content})
}) })
} }
func (w *window) Option(opts *Options) { func (w *window) Option(opts *Options) {
if o := opts.Orientation; o != nil { runInJVM(javaVM(), func(env *C.JNIEnv) {
w.setState(func(state *windowState) { if o := opts.Orientation; o != nil {
state.orientation = o setOrientation(env, w.view, *o)
}) }
} if o := opts.NavigationColor; o != nil {
if o := opts.NavigationColor; o != nil { setNavigationColor(env, w.view, *o)
w.setState(func(state *windowState) { }
state.navigationColor = o if o := opts.StatusColor; o != nil {
}) setStatusColor(env, w.view, *o)
} }
if o := opts.StatusColor; o != nil { })
w.setState(func(state *windowState) {
state.statusColor = o
})
}
} }
func (w *window) SetCursor(name pointer.CursorName) { func (w *window) SetCursor(name pointer.CursorName) {
w.setState(func(state *windowState) { runInJVM(javaVM(), func(env *C.JNIEnv) {
state.cursor = &name setCursor(env, w.view, name)
}) })
} }
// setState adjust the window state on the main thread. func (w *window) Wakeup() {
func (w *window) setState(f func(state *windowState)) {
runOnMain(func(env *C.JNIEnv) { runOnMain(func(env *C.JNIEnv) {
f(&w.newState) w.callbacks.Event(WakeupEvent{})
if w.view == 0 {
// No View attached. The state will be applied at next onCreateView.
return
}
old := w.state
state := w.newState
applyStateDiff(env, w.view, old, state)
w.state = state
}) })
} }
func applyStateDiff(env *C.JNIEnv, view C.jobject, old, state windowState) {
if state.cursor != nil && old.cursor != state.cursor {
setCursor(env, view, *state.cursor)
}
if state.orientation != nil && old.orientation != state.orientation {
setOrientation(env, view, *state.orientation)
}
if state.navigationColor != nil && old.navigationColor != state.navigationColor {
setNavigationColor(env, view, *state.navigationColor)
}
if state.statusColor != nil && old.statusColor != state.statusColor {
setStatusColor(env, view, *state.statusColor)
}
}
func setCursor(env *C.JNIEnv, view C.jobject, name pointer.CursorName) { func setCursor(env *C.JNIEnv, view C.jobject, name pointer.CursorName) {
var curID int var curID int
switch name { switch name {
+6
View File
@@ -222,3 +222,9 @@ func windowSetCursor(from, to pointer.CursorName) pointer.CursorName {
}) })
return to return to
} }
func (w *window) Wakeup() {
runOnMain(func() {
w.w.Event(WakeupEvent{})
})
}
+11 -23
View File
@@ -224,21 +224,17 @@ func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.C
} }
func (w *window) ReadClipboard() { func (w *window) ReadClipboard() {
runOnMain(func() { content := nsstringToString(C.gio_readClipboard())
content := nsstringToString(C.gio_readClipboard()) go w.w.Event(clipboard.Event{Text: content})
w.w.Event(clipboard.Event{Text: content})
})
} }
func (w *window) WriteClipboard(s string) { func (w *window) WriteClipboard(s string) {
u16 := utf16.Encode([]rune(s)) u16 := utf16.Encode([]rune(s))
runOnMain(func() { var chars *C.unichar
var chars *C.unichar if len(u16) > 0 {
if len(u16) > 0 { chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
chars = (*C.unichar)(unsafe.Pointer(&u16[0])) }
} C.gio_writeClipboard(chars, C.NSUInteger(len(u16)))
C.gio_writeClipboard(chars, C.NSUInteger(len(u16)))
})
} }
func (w *window) Option(opts *Options) {} func (w *window) Option(opts *Options) {}
@@ -294,19 +290,11 @@ func (w *window) isVisible() bool {
} }
func (w *window) ShowTextInput(show bool) { func (w *window) ShowTextInput(show bool) {
v := w.view if show {
if v == 0 { C.gio_showTextInput(w.view)
return } else {
C.gio_hideTextInput(w.view)
} }
C.CFRetain(v)
runOnMain(func() {
defer C.CFRelease(v)
if show {
C.gio_showTextInput(w.view)
} else {
C.gio_hideTextInput(w.view)
}
})
} }
// Close the window. Not implemented for iOS. // Close the window. Not implemented for iOS.
+12 -17
View File
@@ -7,7 +7,6 @@ import (
"image" "image"
"image/color" "image/color"
"strings" "strings"
"sync"
"syscall/js" "syscall/js"
"time" "time"
"unicode" "unicode"
@@ -47,7 +46,6 @@ type window struct {
chanAnimation chan struct{} chanAnimation chan struct{}
chanRedraw chan struct{} chanRedraw chan struct{}
mu sync.Mutex
size f32.Point size f32.Point
inset f32.Point inset f32.Point
scale float32 scale float32
@@ -55,6 +53,7 @@ type window struct {
// animRequested tracks whether a requestAnimationFrame callback // animRequested tracks whether a requestAnimationFrame callback
// is pending. // is pending.
animRequested bool animRequested bool
wakeups chan struct{}
} }
func NewWindow(win Callbacks, opts *Options) error { func NewWindow(win Callbacks, opts *Options) error {
@@ -71,6 +70,7 @@ func NewWindow(win Callbacks, opts *Options) error {
window: js.Global().Get("window"), window: js.Global().Get("window"),
head: doc.Get("head"), head: doc.Get("head"),
clipboard: js.Global().Get("navigator").Get("clipboard"), clipboard: js.Global().Get("navigator").Get("clipboard"),
wakeups: make(chan struct{}, 1),
} }
w.requestAnimationFrame = w.window.Get("requestAnimationFrame") w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
w.browserHistory = w.window.Get("history") w.browserHistory = w.window.Get("history")
@@ -89,7 +89,7 @@ func NewWindow(win Callbacks, opts *Options) error {
}) })
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} { w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
content := args[0].String() content := args[0].String()
win.Event(clipboard.Event{Text: content}) go win.Event(clipboard.Event{Text: content})
return nil return nil
}) })
w.addEventListeners() w.addEventListeners()
@@ -106,6 +106,8 @@ func NewWindow(win Callbacks, opts *Options) error {
w.draw(true) w.draw(true)
for { for {
select { select {
case <-w.wakeups:
w.w.Event(WakeupEvent{})
case <-w.chanAnimation: case <-w.chanAnimation:
w.animCallback() w.animCallback()
case <-w.chanRedraw: case <-w.chanRedraw:
@@ -349,9 +351,7 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
changedTouches := e.Get("changedTouches") changedTouches := e.Get("changedTouches")
n := changedTouches.Length() n := changedTouches.Length()
rect := w.cnv.Call("getBoundingClientRect") rect := w.cnv.Call("getBoundingClientRect")
w.mu.Lock()
scale := w.scale scale := w.scale
w.mu.Unlock()
var mods key.Modifiers var mods key.Modifiers
if e.Get("shiftKey").Bool() { if e.Get("shiftKey").Bool() {
mods |= key.ModShift mods |= key.ModShift
@@ -401,9 +401,7 @@ func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
rect := w.cnv.Call("getBoundingClientRect") rect := w.cnv.Call("getBoundingClientRect")
x -= rect.Get("left").Float() x -= rect.Get("left").Float()
y -= rect.Get("top").Float() y -= rect.Get("top").Float()
w.mu.Lock()
scale := w.scale scale := w.scale
w.mu.Unlock()
pos := f32.Point{ pos := f32.Point{
X: float32(x) * scale, X: float32(x) * scale,
Y: float32(y) * scale, Y: float32(y) * scale,
@@ -452,21 +450,17 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
} }
func (w *window) animCallback() { func (w *window) animCallback() {
w.mu.Lock()
anim := w.animating anim := w.animating
w.animRequested = anim w.animRequested = anim
if anim { if anim {
w.requestAnimationFrame.Invoke(w.redraw) w.requestAnimationFrame.Invoke(w.redraw)
} }
w.mu.Unlock()
if anim { if anim {
w.draw(false) w.draw(false)
} }
} }
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
w.mu.Lock()
defer w.mu.Unlock()
w.animating = anim w.animating = anim
if anim && !w.animRequested { if anim && !w.animRequested {
w.animRequested = true w.animRequested = true
@@ -511,6 +505,13 @@ func (w *window) SetCursor(name pointer.CursorName) {
style.Set("cursor", string(name)) style.Set("cursor", string(name))
} }
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
}
func (w *window) ShowTextInput(show bool) { func (w *window) ShowTextInput(show bool) {
// Run in a goroutine to avoid a deadlock if the // Run in a goroutine to avoid a deadlock if the
// focus change result in an event. // focus change result in an event.
@@ -527,9 +528,6 @@ func (w *window) ShowTextInput(show bool) {
func (w *window) Close() {} func (w *window) Close() {}
func (w *window) resize() { func (w *window) resize() {
w.mu.Lock()
defer w.mu.Unlock()
w.scale = float32(w.window.Get("devicePixelRatio").Float()) w.scale = float32(w.window.Get("devicePixelRatio").Float())
rect := w.cnv.Call("getBoundingClientRect") rect := w.cnv.Call("getBoundingClientRect")
@@ -570,9 +568,6 @@ func (w *window) draw(sync bool) {
} }
func (w *window) config() (int, int, system.Insets, unit.Metric) { func (w *window) config() (int, int, system.Insets, unit.Metric) {
w.mu.Lock()
defer w.mu.Unlock()
return int(w.size.X + .5), int(w.size.Y + .5), system.Insets{ return int(w.size.X + .5), int(w.size.Y + .5), system.Insets{
Bottom: unit.Px(w.inset.Y), Bottom: unit.Px(w.inset.Y),
Right: unit.Px(w.inset.X), Right: unit.Px(w.inset.X),
+39 -47
View File
@@ -122,60 +122,54 @@ func (w *window) contextView() C.CFTypeRef {
} }
func (w *window) ReadClipboard() { func (w *window) ReadClipboard() {
runOnMain(func() { content := nsstringToString(C.gio_readClipboard())
content := nsstringToString(C.gio_readClipboard()) go w.w.Event(clipboard.Event{Text: content})
w.w.Event(clipboard.Event{Text: content})
})
} }
func (w *window) WriteClipboard(s string) { func (w *window) WriteClipboard(s string) {
u16 := utf16.Encode([]rune(s)) u16 := utf16.Encode([]rune(s))
runOnMain(func() { var chars *C.unichar
var chars *C.unichar if len(u16) > 0 {
if len(u16) > 0 { chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
chars = (*C.unichar)(unsafe.Pointer(&u16[0])) }
} C.gio_writeClipboard(chars, C.NSUInteger(len(u16)))
C.gio_writeClipboard(chars, C.NSUInteger(len(u16)))
})
} }
func (w *window) Option(opts *Options) { func (w *window) Option(opts *Options) {
w.runOnMain(func() { screenScale := float32(C.gio_getScreenBackingScale())
screenScale := float32(C.gio_getScreenBackingScale()) cfg := configFor(screenScale)
cfg := configFor(screenScale) val := func(v unit.Value) float32 {
val := func(v unit.Value) float32 { return float32(cfg.Px(v)) / screenScale
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.Size; o != nil { }
width := val(o.Width) if o := opts.MinSize; o != nil {
height := val(o.Height) width := val(o.Width)
if width > 0 || height > 0 { height := val(o.Height)
C.gio_setSize(w.window, C.CGFloat(width), C.CGFloat(height)) if width > 0 || height > 0 {
} C.gio_setMinSize(w.window, C.CGFloat(width), C.CGFloat(height))
} }
if o := opts.MinSize; o != nil { }
width := val(o.Width) if o := opts.MaxSize; o != nil {
height := val(o.Height) width := val(o.Width)
if width > 0 || height > 0 { height := val(o.Height)
C.gio_setMinSize(w.window, C.CGFloat(width), C.CGFloat(height)) if width > 0 || height > 0 {
} C.gio_setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height))
} }
if o := opts.MaxSize; o != nil { }
width := val(o.Width) if o := opts.Title; o != nil {
height := val(o.Height) title := C.CString(*o)
if width > 0 || height > 0 { defer C.free(unsafe.Pointer(title))
C.gio_setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height)) C.gio_setTitle(w.window, title)
} }
} if o := opts.WindowMode; o != nil {
if o := opts.Title; o != nil { w.SetWindowMode(*o)
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) { func (w *window) SetWindowMode(mode WindowMode) {
@@ -212,9 +206,7 @@ func (w *window) runOnMain(f func()) {
} }
func (w *window) Close() { func (w *window) Close() {
w.runOnMain(func() { C.gio_close(w.window)
C.gio_close(w.window)
})
} }
func (w *window) setStage(stage system.Stage) { func (w *window) setStage(stage system.Stage) {
+33 -66
View File
@@ -179,9 +179,7 @@ type window struct {
dead bool dead bool
lastFrameCallback *C.struct_wl_callback lastFrameCallback *C.struct_wl_callback
mu sync.Mutex
animating bool animating bool
opts *Options
needAck bool needAck bool
// The most recent configure serial waiting to be ack'ed. // The most recent configure serial waiting to be ack'ed.
serial C.uint32_t serial C.uint32_t
@@ -189,10 +187,8 @@ type window struct {
height int height int
newScale bool newScale bool
scale int scale int
// readClipboard tracks whether a ClipboardEvent is requested.
readClipboard bool wakeups chan struct{}
// writeClipboard is set whenever a clipboard write is requested.
writeClipboard *string
} }
type poller struct { type poller struct {
@@ -319,6 +315,7 @@ func (d *wlDisplay) createNativeWindow(opts *Options) (*window, error) {
newScale: scale != 1, newScale: scale != 1,
ppdp: ppdp, ppdp: ppdp,
ppsp: ppdp, ppsp: ppdp,
wakeups: make(chan struct{}, 1),
} }
w.surf = C.wl_compositor_create_surface(d.compositor) w.surf = C.wl_compositor_create_surface(d.compositor)
if w.surf == nil { if w.surf == nil {
@@ -473,10 +470,8 @@ func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
//export gio_onXdgSurfaceConfigure //export gio_onXdgSurfaceConfigure
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) { func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
w := callbackLoad(data).(*window) w := callbackLoad(data).(*window)
w.mu.Lock()
w.serial = serial w.serial = serial
w.needAck = true w.needAck = true
w.mu.Unlock()
w.setStage(system.StageRunning) w.setStage(system.StageRunning)
w.draw(true) w.draw(true)
} }
@@ -491,8 +486,6 @@ func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, width, height C.int32_t, states *C.struct_wl_array) { func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, width, height C.int32_t, states *C.struct_wl_array) {
w := callbackLoad(data).(*window) w := callbackLoad(data).(*window)
if width != 0 && height != 0 { if width != 0 && height != 0 {
w.mu.Lock()
defer w.mu.Unlock()
w.width = int(width) w.width = int(width)
w.height = int(height) w.height = int(height)
w.updateOpaqueRegion() w.updateOpaqueRegion()
@@ -868,8 +861,6 @@ func (w *window) flushFling() {
invDist := 1 / vel invDist := 1 / vel
w.fling.dir.X = estx.Velocity * invDist w.fling.dir.X = estx.Velocity * invDist
w.fling.dir.Y = esty.Velocity * invDist w.fling.dir.Y = esty.Velocity * invDist
// Wake up the window loop.
w.disp.wakeup()
} }
//export gio_onPointerAxisSource //export gio_onPointerAxisSource
@@ -897,24 +888,26 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis
} }
func (w *window) ReadClipboard() { func (w *window) ReadClipboard() {
w.mu.Lock() r, err := w.disp.readClipboard()
w.readClipboard = true // Send empty responses on unavailable clipboards or errors.
w.mu.Unlock() if r == nil || err != nil {
w.disp.wakeup() w.w.Event(clipboard.Event{})
return
}
// Don't let slow clipboard transfers block event loop.
go func() {
defer r.Close()
data, _ := ioutil.ReadAll(r)
w.w.Event(clipboard.Event{Text: string(data)})
}()
} }
func (w *window) WriteClipboard(s string) { func (w *window) WriteClipboard(s string) {
w.mu.Lock() w.disp.writeClipboard([]byte(s))
w.writeClipboard = &s
w.mu.Unlock()
w.disp.wakeup()
} }
func (w *window) Option(opts *Options) { func (w *window) Option(opts *Options) {
w.mu.Lock() w.setOptions(opts)
w.opts = opts
w.mu.Unlock()
w.disp.wakeup()
} }
func (w *window) setOptions(opts *Options) { func (w *window) setOptions(opts *Options) {
@@ -1138,48 +1131,21 @@ func (w *window) loop() error {
if err := w.disp.dispatch(&p); err != nil { if err := w.disp.dispatch(&p); err != nil {
return err return err
} }
select {
case <-w.wakeups:
w.w.Event(WakeupEvent{})
default:
}
if w.dead { if w.dead {
w.w.Event(system.DestroyEvent{}) w.w.Event(system.DestroyEvent{})
break break
} }
w.process() // pass false to skip unnecessary drawing.
w.draw(false)
} }
return nil return nil
} }
func (w *window) process() {
w.mu.Lock()
readClipboard := w.readClipboard
writeClipboard := w.writeClipboard
opts := w.opts
w.readClipboard = false
w.writeClipboard = nil
w.opts = nil
w.mu.Unlock()
if readClipboard {
r, err := w.disp.readClipboard()
// Send empty responses on unavailable clipboards or errors.
if r == nil || err != nil {
w.w.Event(clipboard.Event{})
return
}
// Don't let slow clipboard transfers block event loop.
go func() {
defer r.Close()
data, _ := ioutil.ReadAll(r)
w.w.Event(clipboard.Event{Text: string(data)})
}()
}
if writeClipboard != nil {
w.disp.writeClipboard([]byte(*writeClipboard))
}
if opts != nil {
w.setOptions(opts)
}
// pass false to skip unnecessary drawing.
w.draw(false)
}
func (d *wlDisplay) dispatch(p *poller) error { func (d *wlDisplay) dispatch(p *poller) error {
dispfd := C.wl_display_get_fd(d.disp) dispfd := C.wl_display_get_fd(d.disp)
// Poll for events and notifications. // Poll for events and notifications.
@@ -1222,13 +1188,18 @@ func (d *wlDisplay) dispatch(p *poller) error {
return nil return nil
} }
func (w *window) SetAnimating(anim bool) { func (w *window) Wakeup() {
w.mu.Lock() select {
w.animating = anim case w.wakeups <- struct{}{}:
w.mu.Unlock() default:
}
w.disp.wakeup() w.disp.wakeup()
} }
func (w *window) SetAnimating(anim bool) {
w.animating = anim
}
// Wakeup wakes up the event loop through the notification pipe. // Wakeup wakes up the event loop through the notification pipe.
func (d *wlDisplay) wakeup() { func (d *wlDisplay) wakeup() {
oneByte := make([]byte, 1) oneByte := make([]byte, 1)
@@ -1415,12 +1386,10 @@ func (w *window) updateOutputs() {
} }
} }
} }
w.mu.Lock()
if found && scale != w.scale { if found && scale != w.scale {
w.scale = scale w.scale = scale
w.newScale = true w.newScale = true
} }
w.mu.Unlock()
if !found { if !found {
w.setStage(system.StagePaused) w.setStage(system.StagePaused)
} else { } else {
@@ -1439,10 +1408,8 @@ func (w *window) config() (int, int, unit.Metric) {
func (w *window) draw(sync bool) { func (w *window) draw(sync bool) {
w.flushScroll() w.flushScroll()
w.mu.Lock()
anim := w.animating || w.fling.anim.Active() anim := w.animating || w.fling.anim.Active()
dead := w.dead dead := w.dead
w.mu.Unlock()
if dead || (!anim && !sync) { if dead || (!anim && !sync) {
return return
} }
+7 -32
View File
@@ -59,7 +59,6 @@ type window struct {
// placement saves the previous window position when in full screen mode. // placement saves the previous window position when in full screen mode.
placement *windows.WindowPlacement placement *windows.WindowPlacement
mu sync.Mutex
animating bool animating bool
minmax winConstraints minmax winConstraints
@@ -67,11 +66,7 @@ type window struct {
opts *Options opts *Options
} }
const ( const _WM_WAKEUP = windows.WM_USER + iota
_WM_REDRAW = windows.WM_USER + iota
_WM_CURSOR
_WM_OPTION
)
type gpuAPI struct { type gpuAPI struct {
priority int priority int
@@ -330,14 +325,12 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
} }
case windows.WM_SETCURSOR: case windows.WM_SETCURSOR:
w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
fallthrough
case _WM_CURSOR:
if w.cursorIn { if w.cursorIn {
windows.SetCursor(w.cursor) windows.SetCursor(w.cursor)
return windows.TRUE return windows.TRUE
} }
case _WM_OPTION: case _WM_WAKEUP:
w.setOptions() w.w.Event(WakeupEvent{})
} }
return windows.DefWindowProc(hwnd, msg, wParam, lParam) return windows.DefWindowProc(hwnd, msg, wParam, lParam)
@@ -422,9 +415,7 @@ func (w *window) loop() error {
msg := new(windows.Msg) msg := new(windows.Msg)
loop: loop:
for { for {
w.mu.Lock()
anim := w.animating anim := w.animating
w.mu.Unlock()
if anim && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) { if anim && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) {
w.draw(false) w.draw(false)
continue continue
@@ -443,16 +434,11 @@ loop:
} }
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
w.mu.Lock()
w.animating = anim w.animating = anim
w.mu.Unlock()
if anim {
w.postRedraw()
}
} }
func (w *window) postRedraw() { func (w *window) Wakeup() {
if err := windows.PostMessage(w.hwnd, _WM_REDRAW, 0, 0); err != nil { if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err) panic(err)
} }
} }
@@ -530,18 +516,7 @@ func (w *window) readClipboard() error {
} }
func (w *window) Option(opts *Options) { func (w *window) Option(opts *Options) {
w.mu.Lock()
w.opts = opts w.opts = opts
w.mu.Unlock()
if err := windows.PostMessage(w.hwnd, _WM_OPTION, 0, 0); err != nil {
panic(err)
}
}
func (w *window) setOptions() {
w.mu.Lock()
opts := w.opts
w.mu.Unlock()
if o := opts.Size; o != nil { if o := opts.Size; o != nil {
dpi := windows.GetSystemDPI() dpi := windows.GetSystemDPI()
cfg := configForDPI(dpi) cfg := configForDPI(dpi)
@@ -658,8 +633,8 @@ func (w *window) SetCursor(name pointer.CursorName) {
c = resources.cursor c = resources.cursor
} }
w.cursor = c w.cursor = c
if err := windows.PostMessage(w.hwnd, _WM_CURSOR, 0, 0); err != nil { if w.cursorIn {
panic(err) windows.SetCursor(w.cursor)
} }
} }
+17 -52
View File
@@ -87,59 +87,34 @@ type x11Window struct {
} }
dead bool dead bool
mu sync.Mutex
animating bool animating bool
opts *Options
pointerBtns pointer.Buttons pointerBtns pointer.Buttons
clipboard struct { clipboard struct {
read bool
write *string
content []byte content []byte
} }
cursor pointer.CursorName cursor pointer.CursorName
mode WindowMode mode WindowMode
wakeups chan struct{}
} }
func (w *x11Window) SetAnimating(anim bool) { func (w *x11Window) SetAnimating(anim bool) {
w.mu.Lock()
w.animating = anim w.animating = anim
w.mu.Unlock()
if anim {
w.wakeup()
}
} }
func (w *x11Window) ReadClipboard() { func (w *x11Window) ReadClipboard() {
w.mu.Lock() C.XDeleteProperty(w.x, w.xw, w.atoms.clipboardContent)
w.clipboard.read = true C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
w.mu.Unlock()
w.wakeup()
} }
func (w *x11Window) WriteClipboard(s string) { func (w *x11Window) WriteClipboard(s string) {
w.mu.Lock() w.clipboard.content = []byte(s)
w.clipboard.write = &s C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
w.mu.Unlock()
w.wakeup()
} }
func (w *x11Window) Option(opts *Options) { func (w *x11Window) Option(opts *Options) {
w.mu.Lock()
w.opts = opts
w.mu.Unlock()
w.wakeup()
}
func (w *x11Window) setOptions() {
w.mu.Lock()
opts := w.opts
w.opts = nil
w.mu.Unlock()
if opts == nil {
return
}
var shints C.XSizeHints var shints C.XSizeHints
if o := opts.MinSize; o != nil { if o := opts.MinSize; o != nil {
shints.min_width = C.int(w.cfg.Px(o.Width)) shints.min_width = C.int(w.cfg.Px(o.Width))
@@ -250,9 +225,6 @@ func (w *x11Window) ShowTextInput(show bool) {}
// Close the window. // Close the window.
func (w *x11Window) Close() { func (w *x11Window) Close() {
w.mu.Lock()
defer w.mu.Unlock()
var xev C.XEvent var xev C.XEvent
ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
*ev = C.XClientMessageEvent{ *ev = C.XClientMessageEvent{
@@ -270,7 +242,11 @@ func (w *x11Window) Close() {
var x11OneByte = make([]byte, 1) var x11OneByte = make([]byte, 1)
func (w *x11Window) wakeup() { func (w *x11Window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN { if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err)) panic(fmt.Errorf("failed to write to pipe: %v", err))
} }
@@ -312,9 +288,7 @@ loop:
// This fixes an issue on Xephyr where on startup XPending() > 0 but // This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll. // poll will still block. This also prevents no-op calls to poll.
if syn = h.handleEvents(); !syn { if syn = h.handleEvents(); !syn {
w.mu.Lock()
anim = w.animating anim = w.animating
w.mu.Unlock()
if !anim { if !anim {
// Clear poll events. // Clear poll events.
*xEvents = 0 *xEvents = 0
@@ -333,7 +307,6 @@ loop:
} }
} }
} }
w.setOptions()
// Clear notifications. // Clear notifications.
for { for {
_, err := syscall.Read(w.notify.read, buf) _, err := syscall.Read(w.notify.read, buf)
@@ -344,6 +317,11 @@ loop:
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err)) panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
} }
} }
select {
case <-w.wakeups:
w.w.Event(WakeupEvent{})
default:
}
if anim || syn { if anim || syn {
w.w.Event(FrameEvent{ w.w.Event(FrameEvent{
@@ -358,20 +336,6 @@ loop:
Sync: syn, Sync: syn,
}) })
} }
w.mu.Lock()
readClipboard := w.clipboard.read
writeClipboard := w.clipboard.write
w.clipboard.read = false
w.clipboard.write = nil
w.mu.Unlock()
if readClipboard {
C.XDeleteProperty(w.x, w.xw, w.atoms.clipboardContent)
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
}
if writeClipboard != nil {
w.clipboard.content = []byte(*writeClipboard)
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
}
} }
w.w.Event(system.DestroyEvent{Err: nil}) w.w.Event(system.DestroyEvent{Err: nil})
} }
@@ -681,6 +645,7 @@ func newX11Window(gioWin Callbacks, opts *Options) error {
cfg: cfg, cfg: cfg,
xkb: xkb, xkb: xkb,
xkbEventBase: xkbEventBase, xkbEventBase: xkbEventBase,
wakeups: make(chan struct{}, 1),
} }
w.notify.read = pipe[0] w.notify.read = pipe[0]
w.notify.write = pipe[1] w.notify.write = pipe[1]
+7 -1
View File
@@ -32,6 +32,8 @@ type Options struct {
CustomRenderer bool CustomRenderer bool
} }
type WakeupEvent struct{}
type WindowMode uint8 type WindowMode uint8
const ( const (
@@ -59,7 +61,7 @@ type Callbacks interface {
// Func runs a function during an Event. This is required for platforms // Func runs a function during an Event. This is required for platforms
// that require coordination between the rendering goroutine and the system // that require coordination between the rendering goroutine and the system
// main thread. // main thread.
Func(f func()) Run(f func())
} }
type Context interface { type Context interface {
@@ -99,6 +101,8 @@ type Driver interface {
// Close the window. // Close the window.
Close() Close()
// Wakeup wakes up the event loop and sends a WakeupEvent.
Wakeup()
} }
type windowRendezvous struct { type windowRendezvous struct {
@@ -137,3 +141,5 @@ func newWindowRendezvous() *windowRendezvous {
}() }()
return wr return wr
} }
func (_ WakeupEvent) ImplementsEvent() {}
+94 -40
View File
@@ -24,7 +24,7 @@ import (
// WindowOption configures a wm. // WindowOption configures a wm.
type Option func(opts *wm.Options) type Option func(opts *wm.Options)
// Window represents an operating system wm. // Window represents an operating system window.
type Window struct { type Window struct {
driver wm.Driver driver wm.Driver
ctx wm.Context ctx wm.Context
@@ -33,6 +33,9 @@ type Window struct {
// driverFuncs is a channel of functions to run when // driverFuncs is a channel of functions to run when
// the Window has a valid driver. // the Window has a valid driver.
driverFuncs chan func() driverFuncs chan func()
// wakeups wakes up the native event loop to send a
// wm.WakeupEvent that flushes driverFuncs.
wakeups chan struct{}
out chan event.Event out chan event.Event
in chan event.Event in chan event.Event
@@ -41,7 +44,8 @@ type Window struct {
frames chan *op.Ops frames chan *op.Ops
frameAck chan struct{} frameAck chan struct{}
// dead is closed when the window is destroyed. // dead is closed when the window is destroyed.
dead chan struct{} dead chan struct{}
notifyAnimate chan struct{}
stage system.Stage stage system.Stage
animating bool animating bool
@@ -58,8 +62,7 @@ type Window struct {
} }
type callbacks struct { type callbacks struct {
w *Window w *Window
funcs chan func()
} }
// queue is an event.Queue implementation that distributes system events // queue is an event.Queue implementation that distributes system events
@@ -98,17 +101,18 @@ func NewWindow(options ...Option) *Window {
} }
w := &Window{ w := &Window{
in: make(chan event.Event), in: make(chan event.Event),
out: make(chan event.Event), out: make(chan event.Event),
ack: make(chan struct{}), ack: make(chan struct{}),
invalidates: make(chan struct{}, 1), invalidates: make(chan struct{}, 1),
frames: make(chan *op.Ops), frames: make(chan *op.Ops),
frameAck: make(chan struct{}), frameAck: make(chan struct{}),
driverFuncs: make(chan func()), driverFuncs: make(chan func(), 1),
dead: make(chan struct{}), wakeups: make(chan struct{}, 1),
nocontext: opts.CustomRenderer, dead: make(chan struct{}),
notifyAnimate: make(chan struct{}, 1),
nocontext: opts.CustomRenderer,
} }
w.callbacks.funcs = make(chan func())
w.callbacks.w = w w.callbacks.w = w
go w.run(opts) go w.run(opts)
return w return w
@@ -177,9 +181,9 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.
w.queue.q.Frame(frame) w.queue.q.Frame(frame)
switch w.queue.q.TextInputState() { switch w.queue.q.TextInputState() {
case router.TextInputOpen: case router.TextInputOpen:
w.driver.ShowTextInput(true) go w.Run(func() { w.driver.ShowTextInput(true) })
case router.TextInputClose: case router.TextInputClose:
w.driver.ShowTextInput(false) go w.Run(func() { w.driver.ShowTextInput(false) })
} }
if txt, ok := w.queue.q.WriteClipboard(); ok { if txt, ok := w.queue.q.WriteClipboard(); ok {
go w.WriteClipboard(txt) go w.WriteClipboard(txt)
@@ -226,7 +230,7 @@ func (w *Window) Invalidate() {
// Option applies the options to the window. // Option applies the options to the window.
func (w *Window) Option(opts ...Option) { func (w *Window) Option(opts ...Option) {
go w.driverDo(func() { go w.Run(func() {
o := new(wm.Options) o := new(wm.Options)
for _, opt := range opts { for _, opt := range opts {
opt(o) opt(o)
@@ -239,21 +243,21 @@ func (w *Window) Option(opts ...Option) {
// of a clipboard.Event. Multiple reads may be coalesced // of a clipboard.Event. Multiple reads may be coalesced
// to a single event. // to a single event.
func (w *Window) ReadClipboard() { func (w *Window) ReadClipboard() {
go w.driverDo(func() { go w.Run(func() {
w.driver.ReadClipboard() w.driver.ReadClipboard()
}) })
} }
// WriteClipboard writes a string to the clipboard. // WriteClipboard writes a string to the clipboard.
func (w *Window) WriteClipboard(s string) { func (w *Window) WriteClipboard(s string) {
go w.driverDo(func() { go w.Run(func() {
w.driver.WriteClipboard(s) w.driver.WriteClipboard(s)
}) })
} }
// SetCursorName changes the current window cursor to name. // SetCursorName changes the current window cursor to name.
func (w *Window) SetCursorName(name pointer.CursorName) { func (w *Window) SetCursorName(name pointer.CursorName) {
go w.driverDo(func() { go w.Run(func() {
w.driver.SetCursor(name) w.driver.SetCursor(name)
}) })
} }
@@ -264,16 +268,32 @@ func (w *Window) SetCursorName(name pointer.CursorName) {
// Currently, only macOS, Windows and X11 drivers implement this functionality, // Currently, only macOS, Windows and X11 drivers implement this functionality,
// all others are stubbed. // all others are stubbed.
func (w *Window) Close() { func (w *Window) Close() {
go w.driverDo(func() { go w.Run(func() {
w.driver.Close() w.driver.Close()
}) })
} }
// driverDo waits for the window to have a valid driver attached and calls f. // Run f in the same thread as the native window event loop, and wait for f to
// It does nothing if the if the window was destroyed while waiting. // return or the window to close. Run is guaranteed not to deadlock if it is
func (w *Window) driverDo(f func()) { // invoked during the handling of a ViewEvent, system.FrameEvent,
// system.StageEvent; call Run in a separate goroutine to avoid deadlock in all
// other cases.
//
// Note that most programs should not call Run; configuring a Window with
// CustomRenderer is a notable exception.
func (w *Window) Run(f func()) {
done := make(chan struct{})
wrapper := func() {
f()
close(done)
}
select { select {
case w.driverFuncs <- f: case w.driverFuncs <- wrapper:
w.wakeup()
select {
case <-done:
case <-w.dead:
}
case <-w.dead: case <-w.dead:
} }
} }
@@ -293,7 +313,18 @@ func (w *Window) updateAnimation() {
} }
if animate != w.animating { if animate != w.animating {
w.animating = animate w.animating = animate
w.driver.SetAnimating(animate) select {
case w.notifyAnimate <- struct{}{}:
w.wakeup()
default:
}
}
}
func (w *Window) wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
} }
} }
@@ -311,20 +342,39 @@ func (c *callbacks) SetDriver(d wm.Driver) {
func (c *callbacks) Event(e event.Event) { func (c *callbacks) Event(e event.Event) {
select { select {
case c.w.in <- e: case c.w.in <- e:
for { c.w.runFuncs()
select {
case <-c.w.ack:
return
case f := <-c.funcs:
f()
}
}
case <-c.w.dead: case <-c.w.dead:
} }
} }
func (c *callbacks) Func(f func()) { func (w *Window) runFuncs() {
c.funcs <- f // Flush pending runnnables.
loop:
for {
select {
case <-w.notifyAnimate:
w.driver.SetAnimating(w.animating)
case f := <-w.driverFuncs:
f()
default:
break loop
}
}
// Wait for ack while running incoming runnables.
for {
select {
case <-w.notifyAnimate:
w.driver.SetAnimating(w.animating)
case f := <-w.driverFuncs:
f()
case <-w.ack:
return
}
}
}
func (c *callbacks) Run(f func()) {
c.w.Run(f)
} }
func (w *Window) waitAck() { func (w *Window) waitAck() {
@@ -383,9 +433,9 @@ func (w *Window) run(opts *wm.Options) {
return return
} }
for { for {
var driverFuncs chan func() var wakeups chan struct{}
if w.driver != nil { if w.driver != nil {
driverFuncs = w.driverFuncs wakeups = w.wakeups
} }
var timer <-chan time.Time var timer <-chan time.Time
if w.delayedDraw != nil { if w.delayedDraw != nil {
@@ -398,8 +448,8 @@ func (w *Window) run(opts *wm.Options) {
case <-w.invalidates: case <-w.invalidates:
w.setNextFrame(time.Time{}) w.setNextFrame(time.Time{})
w.updateAnimation() w.updateAnimation()
case f := <-driverFuncs: case <-wakeups:
f() w.driver.Wakeup()
case e := <-w.in: case e := <-w.in:
switch e2 := e.(type) { switch e2 := e.(type) {
case system.StageEvent: case system.StageEvent:
@@ -454,6 +504,10 @@ func (w *Window) run(opts *wm.Options) {
w.out <- e2 w.out <- e2
w.ack <- struct{}{} w.ack <- struct{}{}
return return
case ViewEvent:
w.out <- e2
w.waitAck()
case wm.WakeupEvent:
case event.Event: case event.Event:
if w.queue.q.Queue(e2) { if w.queue.q.Queue(e2) {
w.setNextFrame(time.Time{}) w.setNextFrame(time.Time{})