diff --git a/app/internal/wm/d3d11_windows.go b/app/internal/wm/d3d11_windows.go index 1dae0056..41f5cc5b 100644 --- a/app/internal/wm/d3d11_windows.go +++ b/app/internal/wm/d3d11_windows.go @@ -72,7 +72,10 @@ func (c *d3d11Context) Present() 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 { c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView) return nil diff --git a/app/internal/wm/egl_android.go b/app/internal/wm/egl_android.go index bee47fd3..06b97211 100644 --- a/app/internal/wm/egl_android.go +++ b/app/internal/wm/egl_android.go @@ -3,6 +3,7 @@ package wm /* +#include #include */ import "C" @@ -35,7 +36,15 @@ func (c *context) Release() { func (c *context) MakeCurrent() error { 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 { return nil } diff --git a/app/internal/wm/egl_windows.go b/app/internal/wm/egl_windows.go index b796c33b..261934b9 100644 --- a/app/internal/wm/egl_windows.go +++ b/app/internal/wm/egl_windows.go @@ -3,6 +3,8 @@ package wm import ( + "golang.org/x/sys/windows" + "gioui.org/internal/egl" ) @@ -34,7 +36,13 @@ func (c *glContext) Release() { func (c *glContext) MakeCurrent() error { 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) if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { return err diff --git a/app/internal/wm/gl_macos.go b/app/internal/wm/gl_macos.go index 7c231ae1..0bede035 100644 --- a/app/internal/wm/gl_macos.go +++ b/app/internal/wm/gl_macos.go @@ -43,7 +43,7 @@ func newContext(w *window) (*context, error) { } // [NSOpenGLContext setView] must run on the main thread. Fortunately, // 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 := &context{ diff --git a/app/internal/wm/os_android.go b/app/internal/wm/os_android.go index 59a53e72..080f67b2 100644 --- a/app/internal/wm/os_android.go +++ b/app/internal/wm/os_android.go @@ -42,7 +42,6 @@ import "C" import ( "errors" "fmt" - "gioui.org/internal/f32color" "image" "image/color" "reflect" @@ -53,6 +52,8 @@ import ( "unicode/utf16" "unsafe" + "gioui.org/internal/f32color" + "gioui.org/f32" "gioui.org/io/clipboard" "gioui.org/io/key" @@ -70,24 +71,11 @@ type window struct { fontScale float32 insets system.Insets - stage system.Stage - started bool - - state, newState windowState - - // mu protects the fields following it. - mu sync.Mutex - win *C.ANativeWindow + stage system.Stage + started bool animating bool -} -// windowState tracks the View or Activity specific state lost when Android -// re-creates our Activity. -type windowState struct { - cursor *pointer.CursorName - orientation *Orientation - navigationColor *color.NRGBA - statusColor *color.NRGBA + win *C.ANativeWindow } // 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 w.loadConfig(env, class) w.Option(wopts.opts) - applyStateDiff(env, view, windowState{}, w.state) w.setStage(system.StagePaused) w.callbacks.Event(ViewEvent{View: uintptr(view)}) 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) { w := views[handle] w.started = true - if w.aNativeWindow() != nil { + if w.win != nil { 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 func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) { w := views[handle] - w.mu.Lock() w.win = nil - w.mu.Unlock() w.setStage(system.StagePaused) } //export Java_org_gioui_GioView_onSurfaceChanged func Java_org_gioui_GioView_onSurfaceChanged(env *C.JNIEnv, class C.jclass, handle C.jlong, surf C.jobject) { w := views[handle] - w.mu.Lock() w.win = C.ANativeWindow_fromSurface(env, surf) - w.mu.Unlock() if w.started { w.setVisible() } @@ -323,9 +306,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view if w.stage < system.StageRunning { return } - w.mu.Lock() anim := w.animating - w.mu.Unlock() if anim { runInJVM(javaVM(), func(env *C.JNIEnv) { 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 func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) { 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 @@ -366,8 +347,7 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C } func (w *window) setVisible() { - win := w.aNativeWindow() - width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + width, height := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win) if width == 0 || height == 0 { return } @@ -384,22 +364,15 @@ func (w *window) setStage(stage system.Stage) { } func (w *window) nativeWindow(visID int) (*C.ANativeWindow, int, int) { - win := w.aNativeWindow() var width, height int - if win != nil { - if C.ANativeWindow_setBuffersGeometry(win, 0, 0, C.int32_t(visID)) != 0 { + if w.win != nil { + if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 { 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) } - return win, width, height -} - -func (w *window) aNativeWindow() *C.ANativeWindow { - w.mu.Lock() - defer w.mu.Unlock() - return w.win + return w.win, width, height } 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) { - w.mu.Lock() w.animating = anim - w.mu.Unlock() if anim { - runOnMain(func(env *C.JNIEnv) { - if w.view == 0 { - // View was destroyed while switching to main thread. - return - } + runInJVM(javaVM(), func(env *C.JNIEnv) { callVoidMethod(env, w.view, gioView.postFrameCallback) }) } } func (w *window) draw(sync bool) { - win := w.aNativeWindow() - width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + width, height := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win) if width == 0 || height == 0 { 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) { - runOnMain(func(env *C.JNIEnv) { - if w.view == 0 { - return - } + runInJVM(javaVM(), func(env *C.JNIEnv) { if show { callVoidMethod(env, w.view, gioView.showTextInput) } else { @@ -669,7 +632,7 @@ func NewWindow(window Callbacks, opts *Options) error { } func (w *window) WriteClipboard(s string) { - runOnMain(func(env *C.JNIEnv) { + runInJVM(javaVM(), func(env *C.JNIEnv) { jstr := javaString(env, s) callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, jvalue(android.appCtx), jvalue(jstr)) @@ -677,71 +640,43 @@ func (w *window) WriteClipboard(s string) { } func (w *window) ReadClipboard() { - runOnMain(func(env *C.JNIEnv) { + runInJVM(javaVM(), func(env *C.JNIEnv) { c, err := callStaticObjectMethod(env, android.gioCls, android.mreadClipboard, jvalue(android.appCtx)) if err != nil { return } 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) { - if o := opts.Orientation; o != nil { - w.setState(func(state *windowState) { - state.orientation = o - }) - } - if o := opts.NavigationColor; o != nil { - w.setState(func(state *windowState) { - state.navigationColor = o - }) - } - if o := opts.StatusColor; o != nil { - w.setState(func(state *windowState) { - state.statusColor = o - }) - } + runInJVM(javaVM(), func(env *C.JNIEnv) { + if o := opts.Orientation; o != nil { + setOrientation(env, w.view, *o) + } + if o := opts.NavigationColor; o != nil { + setNavigationColor(env, w.view, *o) + } + if o := opts.StatusColor; o != nil { + setStatusColor(env, w.view, *o) + } + }) } func (w *window) SetCursor(name pointer.CursorName) { - w.setState(func(state *windowState) { - state.cursor = &name + runInJVM(javaVM(), func(env *C.JNIEnv) { + setCursor(env, w.view, name) }) } -// setState adjust the window state on the main thread. -func (w *window) setState(f func(state *windowState)) { +func (w *window) Wakeup() { runOnMain(func(env *C.JNIEnv) { - f(&w.newState) - 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 + w.callbacks.Event(WakeupEvent{}) }) } -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) { var curID int switch name { diff --git a/app/internal/wm/os_darwin.go b/app/internal/wm/os_darwin.go index 1b6c6a3a..9ef3c096 100644 --- a/app/internal/wm/os_darwin.go +++ b/app/internal/wm/os_darwin.go @@ -222,3 +222,9 @@ func windowSetCursor(from, to pointer.CursorName) pointer.CursorName { }) return to } + +func (w *window) Wakeup() { + runOnMain(func() { + w.w.Event(WakeupEvent{}) + }) +} diff --git a/app/internal/wm/os_ios.go b/app/internal/wm/os_ios.go index 660119b0..63dc8e0d 100644 --- a/app/internal/wm/os_ios.go +++ b/app/internal/wm/os_ios.go @@ -224,21 +224,17 @@ func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.C } func (w *window) ReadClipboard() { - runOnMain(func() { - content := nsstringToString(C.gio_readClipboard()) - w.w.Event(clipboard.Event{Text: content}) - }) + content := nsstringToString(C.gio_readClipboard()) + go w.w.Event(clipboard.Event{Text: content}) } func (w *window) WriteClipboard(s string) { u16 := utf16.Encode([]rune(s)) - runOnMain(func() { - var chars *C.unichar - if len(u16) > 0 { - chars = (*C.unichar)(unsafe.Pointer(&u16[0])) - } - C.gio_writeClipboard(chars, C.NSUInteger(len(u16))) - }) + 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) {} @@ -294,19 +290,11 @@ func (w *window) isVisible() bool { } func (w *window) ShowTextInput(show bool) { - v := w.view - if v == 0 { - return + if show { + C.gio_showTextInput(w.view) + } 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. diff --git a/app/internal/wm/os_js.go b/app/internal/wm/os_js.go index 427be44d..fb466b6e 100644 --- a/app/internal/wm/os_js.go +++ b/app/internal/wm/os_js.go @@ -7,7 +7,6 @@ import ( "image" "image/color" "strings" - "sync" "syscall/js" "time" "unicode" @@ -47,7 +46,6 @@ type window struct { chanAnimation chan struct{} chanRedraw chan struct{} - mu sync.Mutex size f32.Point inset f32.Point scale float32 @@ -55,6 +53,7 @@ type window struct { // animRequested tracks whether a requestAnimationFrame callback // is pending. animRequested bool + wakeups chan struct{} } func NewWindow(win Callbacks, opts *Options) error { @@ -71,6 +70,7 @@ func NewWindow(win Callbacks, opts *Options) error { window: js.Global().Get("window"), head: doc.Get("head"), clipboard: js.Global().Get("navigator").Get("clipboard"), + wakeups: make(chan struct{}, 1), } w.requestAnimationFrame = w.window.Get("requestAnimationFrame") 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{} { content := args[0].String() - win.Event(clipboard.Event{Text: content}) + go win.Event(clipboard.Event{Text: content}) return nil }) w.addEventListeners() @@ -106,6 +106,8 @@ func NewWindow(win Callbacks, opts *Options) error { w.draw(true) for { select { + case <-w.wakeups: + w.w.Event(WakeupEvent{}) case <-w.chanAnimation: w.animCallback() case <-w.chanRedraw: @@ -349,9 +351,7 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) { changedTouches := e.Get("changedTouches") n := changedTouches.Length() rect := w.cnv.Call("getBoundingClientRect") - w.mu.Lock() scale := w.scale - w.mu.Unlock() var mods key.Modifiers if e.Get("shiftKey").Bool() { 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") x -= rect.Get("left").Float() y -= rect.Get("top").Float() - w.mu.Lock() scale := w.scale - w.mu.Unlock() pos := f32.Point{ X: float32(x) * 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() { - w.mu.Lock() anim := w.animating w.animRequested = anim if anim { w.requestAnimationFrame.Invoke(w.redraw) } - w.mu.Unlock() if anim { w.draw(false) } } func (w *window) SetAnimating(anim bool) { - w.mu.Lock() - defer w.mu.Unlock() w.animating = anim if anim && !w.animRequested { w.animRequested = true @@ -511,6 +505,13 @@ func (w *window) SetCursor(name pointer.CursorName) { style.Set("cursor", string(name)) } +func (w *window) Wakeup() { + select { + case w.wakeups <- struct{}{}: + default: + } +} + func (w *window) ShowTextInput(show bool) { // Run in a goroutine to avoid a deadlock if the // focus change result in an event. @@ -527,9 +528,6 @@ func (w *window) ShowTextInput(show bool) { func (w *window) Close() {} func (w *window) resize() { - w.mu.Lock() - defer w.mu.Unlock() - w.scale = float32(w.window.Get("devicePixelRatio").Float()) 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) { - w.mu.Lock() - defer w.mu.Unlock() - return int(w.size.X + .5), int(w.size.Y + .5), system.Insets{ Bottom: unit.Px(w.inset.Y), Right: unit.Px(w.inset.X), diff --git a/app/internal/wm/os_macos.go b/app/internal/wm/os_macos.go index 83f6edac..c34da229 100644 --- a/app/internal/wm/os_macos.go +++ b/app/internal/wm/os_macos.go @@ -122,60 +122,54 @@ func (w *window) contextView() C.CFTypeRef { } func (w *window) ReadClipboard() { - runOnMain(func() { - content := nsstringToString(C.gio_readClipboard()) - w.w.Event(clipboard.Event{Text: content}) - }) + content := nsstringToString(C.gio_readClipboard()) + go w.w.Event(clipboard.Event{Text: content}) } func (w *window) WriteClipboard(s string) { u16 := utf16.Encode([]rune(s)) - runOnMain(func() { - var chars *C.unichar - if len(u16) > 0 { - chars = (*C.unichar)(unsafe.Pointer(&u16[0])) - } - C.gio_writeClipboard(chars, C.NSUInteger(len(u16))) - }) + 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) { - w.runOnMain(func() { - screenScale := float32(C.gio_getScreenBackingScale()) - cfg := configFor(screenScale) - val := func(v unit.Value) float32 { - return float32(cfg.Px(v)) / screenScale + 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.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.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.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) - } - }) + } + 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) { @@ -212,9 +206,7 @@ func (w *window) runOnMain(f func()) { } func (w *window) Close() { - w.runOnMain(func() { - C.gio_close(w.window) - }) + C.gio_close(w.window) } func (w *window) setStage(stage system.Stage) { diff --git a/app/internal/wm/os_wayland.go b/app/internal/wm/os_wayland.go index 03790959..ef162d4c 100644 --- a/app/internal/wm/os_wayland.go +++ b/app/internal/wm/os_wayland.go @@ -179,9 +179,7 @@ type window struct { dead bool lastFrameCallback *C.struct_wl_callback - mu sync.Mutex animating bool - opts *Options needAck bool // The most recent configure serial waiting to be ack'ed. serial C.uint32_t @@ -189,10 +187,8 @@ type window struct { height int newScale bool scale int - // readClipboard tracks whether a ClipboardEvent is requested. - readClipboard bool - // writeClipboard is set whenever a clipboard write is requested. - writeClipboard *string + + wakeups chan struct{} } type poller struct { @@ -319,6 +315,7 @@ func (d *wlDisplay) createNativeWindow(opts *Options) (*window, error) { newScale: scale != 1, ppdp: ppdp, ppsp: ppdp, + wakeups: make(chan struct{}, 1), } w.surf = C.wl_compositor_create_surface(d.compositor) 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 func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) { w := callbackLoad(data).(*window) - w.mu.Lock() w.serial = serial w.needAck = true - w.mu.Unlock() w.setStage(system.StageRunning) 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) { w := callbackLoad(data).(*window) if width != 0 && height != 0 { - w.mu.Lock() - defer w.mu.Unlock() w.width = int(width) w.height = int(height) w.updateOpaqueRegion() @@ -868,8 +861,6 @@ func (w *window) flushFling() { invDist := 1 / vel w.fling.dir.X = estx.Velocity * invDist w.fling.dir.Y = esty.Velocity * invDist - // Wake up the window loop. - w.disp.wakeup() } //export gio_onPointerAxisSource @@ -897,24 +888,26 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis } func (w *window) ReadClipboard() { - w.mu.Lock() - w.readClipboard = true - w.mu.Unlock() - w.disp.wakeup() + 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)}) + }() } func (w *window) WriteClipboard(s string) { - w.mu.Lock() - w.writeClipboard = &s - w.mu.Unlock() - w.disp.wakeup() + w.disp.writeClipboard([]byte(s)) } func (w *window) Option(opts *Options) { - w.mu.Lock() - w.opts = opts - w.mu.Unlock() - w.disp.wakeup() + w.setOptions(opts) } func (w *window) setOptions(opts *Options) { @@ -1138,48 +1131,21 @@ func (w *window) loop() error { if err := w.disp.dispatch(&p); err != nil { return err } + select { + case <-w.wakeups: + w.w.Event(WakeupEvent{}) + default: + } if w.dead { w.w.Event(system.DestroyEvent{}) break } - w.process() + // pass false to skip unnecessary drawing. + w.draw(false) } 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 { dispfd := C.wl_display_get_fd(d.disp) // Poll for events and notifications. @@ -1222,13 +1188,18 @@ func (d *wlDisplay) dispatch(p *poller) error { return nil } -func (w *window) SetAnimating(anim bool) { - w.mu.Lock() - w.animating = anim - w.mu.Unlock() +func (w *window) Wakeup() { + select { + case w.wakeups <- struct{}{}: + default: + } w.disp.wakeup() } +func (w *window) SetAnimating(anim bool) { + w.animating = anim +} + // Wakeup wakes up the event loop through the notification pipe. func (d *wlDisplay) wakeup() { oneByte := make([]byte, 1) @@ -1415,12 +1386,10 @@ func (w *window) updateOutputs() { } } } - w.mu.Lock() if found && scale != w.scale { w.scale = scale w.newScale = true } - w.mu.Unlock() if !found { w.setStage(system.StagePaused) } else { @@ -1439,10 +1408,8 @@ func (w *window) config() (int, int, unit.Metric) { func (w *window) draw(sync bool) { w.flushScroll() - w.mu.Lock() anim := w.animating || w.fling.anim.Active() dead := w.dead - w.mu.Unlock() if dead || (!anim && !sync) { return } diff --git a/app/internal/wm/os_windows.go b/app/internal/wm/os_windows.go index ede985d6..ceebbe64 100644 --- a/app/internal/wm/os_windows.go +++ b/app/internal/wm/os_windows.go @@ -59,7 +59,6 @@ type window struct { // placement saves the previous window position when in full screen mode. placement *windows.WindowPlacement - mu sync.Mutex animating bool minmax winConstraints @@ -67,11 +66,7 @@ type window struct { opts *Options } -const ( - _WM_REDRAW = windows.WM_USER + iota - _WM_CURSOR - _WM_OPTION -) +const _WM_WAKEUP = windows.WM_USER + iota type gpuAPI struct { priority int @@ -330,14 +325,12 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr } case windows.WM_SETCURSOR: w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT - fallthrough - case _WM_CURSOR: if w.cursorIn { windows.SetCursor(w.cursor) return windows.TRUE } - case _WM_OPTION: - w.setOptions() + case _WM_WAKEUP: + w.w.Event(WakeupEvent{}) } return windows.DefWindowProc(hwnd, msg, wParam, lParam) @@ -422,9 +415,7 @@ func (w *window) loop() error { msg := new(windows.Msg) loop: for { - w.mu.Lock() anim := w.animating - w.mu.Unlock() if anim && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) { w.draw(false) continue @@ -443,16 +434,11 @@ loop: } func (w *window) SetAnimating(anim bool) { - w.mu.Lock() w.animating = anim - w.mu.Unlock() - if anim { - w.postRedraw() - } } -func (w *window) postRedraw() { - if err := windows.PostMessage(w.hwnd, _WM_REDRAW, 0, 0); err != nil { +func (w *window) Wakeup() { + if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil { panic(err) } } @@ -530,18 +516,7 @@ func (w *window) readClipboard() error { } func (w *window) Option(opts *Options) { - w.mu.Lock() 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 { dpi := windows.GetSystemDPI() cfg := configForDPI(dpi) @@ -658,8 +633,8 @@ func (w *window) SetCursor(name pointer.CursorName) { c = resources.cursor } w.cursor = c - if err := windows.PostMessage(w.hwnd, _WM_CURSOR, 0, 0); err != nil { - panic(err) + if w.cursorIn { + windows.SetCursor(w.cursor) } } diff --git a/app/internal/wm/os_x11.go b/app/internal/wm/os_x11.go index 51ab7983..f664de02 100644 --- a/app/internal/wm/os_x11.go +++ b/app/internal/wm/os_x11.go @@ -87,59 +87,34 @@ type x11Window struct { } dead bool - mu sync.Mutex animating bool - opts *Options pointerBtns pointer.Buttons clipboard struct { - read bool - write *string content []byte } cursor pointer.CursorName mode WindowMode + + wakeups chan struct{} } func (w *x11Window) SetAnimating(anim bool) { - w.mu.Lock() w.animating = anim - w.mu.Unlock() - if anim { - w.wakeup() - } } func (w *x11Window) ReadClipboard() { - w.mu.Lock() - w.clipboard.read = true - w.mu.Unlock() - w.wakeup() + 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) } func (w *x11Window) WriteClipboard(s string) { - w.mu.Lock() - w.clipboard.write = &s - w.mu.Unlock() - w.wakeup() + w.clipboard.content = []byte(s) + C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime) } 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 if o := opts.MinSize; o != nil { shints.min_width = C.int(w.cfg.Px(o.Width)) @@ -250,9 +225,6 @@ func (w *x11Window) ShowTextInput(show bool) {} // Close the window. func (w *x11Window) Close() { - w.mu.Lock() - defer w.mu.Unlock() - var xev C.XEvent ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) *ev = C.XClientMessageEvent{ @@ -270,7 +242,11 @@ func (w *x11Window) Close() { 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 { 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 // poll will still block. This also prevents no-op calls to poll. if syn = h.handleEvents(); !syn { - w.mu.Lock() anim = w.animating - w.mu.Unlock() if !anim { // Clear poll events. *xEvents = 0 @@ -333,7 +307,6 @@ loop: } } } - w.setOptions() // Clear notifications. for { _, err := syscall.Read(w.notify.read, buf) @@ -344,6 +317,11 @@ loop: panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err)) } } + select { + case <-w.wakeups: + w.w.Event(WakeupEvent{}) + default: + } if anim || syn { w.w.Event(FrameEvent{ @@ -358,20 +336,6 @@ loop: 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}) } @@ -681,6 +645,7 @@ func newX11Window(gioWin Callbacks, opts *Options) error { cfg: cfg, xkb: xkb, xkbEventBase: xkbEventBase, + wakeups: make(chan struct{}, 1), } w.notify.read = pipe[0] w.notify.write = pipe[1] diff --git a/app/internal/wm/window.go b/app/internal/wm/window.go index 93b7f93f..475d7a4c 100644 --- a/app/internal/wm/window.go +++ b/app/internal/wm/window.go @@ -32,6 +32,8 @@ type Options struct { CustomRenderer bool } +type WakeupEvent struct{} + type WindowMode uint8 const ( @@ -59,7 +61,7 @@ type Callbacks interface { // Func runs a function during an Event. This is required for platforms // that require coordination between the rendering goroutine and the system // main thread. - Func(f func()) + Run(f func()) } type Context interface { @@ -99,6 +101,8 @@ type Driver interface { // Close the window. Close() + // Wakeup wakes up the event loop and sends a WakeupEvent. + Wakeup() } type windowRendezvous struct { @@ -137,3 +141,5 @@ func newWindowRendezvous() *windowRendezvous { }() return wr } + +func (_ WakeupEvent) ImplementsEvent() {} diff --git a/app/window.go b/app/window.go index 3eb53942..ac0b97cb 100644 --- a/app/window.go +++ b/app/window.go @@ -24,7 +24,7 @@ import ( // WindowOption configures a wm. type Option func(opts *wm.Options) -// Window represents an operating system wm. +// Window represents an operating system window. type Window struct { driver wm.Driver ctx wm.Context @@ -33,6 +33,9 @@ type Window struct { // driverFuncs is a channel of functions to run when // the Window has a valid driver. 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 in chan event.Event @@ -41,7 +44,8 @@ type Window struct { frames chan *op.Ops frameAck chan struct{} // dead is closed when the window is destroyed. - dead chan struct{} + dead chan struct{} + notifyAnimate chan struct{} stage system.Stage animating bool @@ -58,8 +62,7 @@ type Window struct { } type callbacks struct { - w *Window - funcs chan func() + w *Window } // queue is an event.Queue implementation that distributes system events @@ -98,17 +101,18 @@ func NewWindow(options ...Option) *Window { } w := &Window{ - in: make(chan event.Event), - out: make(chan event.Event), - ack: make(chan struct{}), - invalidates: make(chan struct{}, 1), - frames: make(chan *op.Ops), - frameAck: make(chan struct{}), - driverFuncs: make(chan func()), - dead: make(chan struct{}), - nocontext: opts.CustomRenderer, + in: make(chan event.Event), + out: make(chan event.Event), + ack: make(chan struct{}), + invalidates: make(chan struct{}, 1), + frames: make(chan *op.Ops), + frameAck: make(chan struct{}), + driverFuncs: make(chan func(), 1), + wakeups: make(chan struct{}, 1), + dead: make(chan struct{}), + notifyAnimate: make(chan struct{}, 1), + nocontext: opts.CustomRenderer, } - w.callbacks.funcs = make(chan func()) w.callbacks.w = w go w.run(opts) return w @@ -177,9 +181,9 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op. w.queue.q.Frame(frame) switch w.queue.q.TextInputState() { case router.TextInputOpen: - w.driver.ShowTextInput(true) + go w.Run(func() { w.driver.ShowTextInput(true) }) case router.TextInputClose: - w.driver.ShowTextInput(false) + go w.Run(func() { w.driver.ShowTextInput(false) }) } if txt, ok := w.queue.q.WriteClipboard(); ok { go w.WriteClipboard(txt) @@ -226,7 +230,7 @@ func (w *Window) Invalidate() { // Option applies the options to the window. func (w *Window) Option(opts ...Option) { - go w.driverDo(func() { + go w.Run(func() { o := new(wm.Options) for _, opt := range opts { opt(o) @@ -239,21 +243,21 @@ func (w *Window) Option(opts ...Option) { // of a clipboard.Event. Multiple reads may be coalesced // to a single event. func (w *Window) ReadClipboard() { - go w.driverDo(func() { + go w.Run(func() { w.driver.ReadClipboard() }) } // WriteClipboard writes a string to the clipboard. func (w *Window) WriteClipboard(s string) { - go w.driverDo(func() { + go w.Run(func() { w.driver.WriteClipboard(s) }) } // SetCursorName changes the current window cursor to name. func (w *Window) SetCursorName(name pointer.CursorName) { - go w.driverDo(func() { + go w.Run(func() { 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, // all others are stubbed. func (w *Window) Close() { - go w.driverDo(func() { + go w.Run(func() { w.driver.Close() }) } -// driverDo waits for the window to have a valid driver attached and calls f. -// It does nothing if the if the window was destroyed while waiting. -func (w *Window) driverDo(f func()) { +// Run f in the same thread as the native window event loop, and wait for f to +// return or the window to close. Run is guaranteed not to deadlock if it is +// 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 { - case w.driverFuncs <- f: + case w.driverFuncs <- wrapper: + w.wakeup() + select { + case <-done: + case <-w.dead: + } case <-w.dead: } } @@ -293,7 +313,18 @@ func (w *Window) updateAnimation() { } if animate != w.animating { 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) { select { case c.w.in <- e: - for { - select { - case <-c.w.ack: - return - case f := <-c.funcs: - f() - } - } + c.w.runFuncs() case <-c.w.dead: } } -func (c *callbacks) Func(f func()) { - c.funcs <- f +func (w *Window) runFuncs() { + // 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() { @@ -383,9 +433,9 @@ func (w *Window) run(opts *wm.Options) { return } for { - var driverFuncs chan func() + var wakeups chan struct{} if w.driver != nil { - driverFuncs = w.driverFuncs + wakeups = w.wakeups } var timer <-chan time.Time if w.delayedDraw != nil { @@ -398,8 +448,8 @@ func (w *Window) run(opts *wm.Options) { case <-w.invalidates: w.setNextFrame(time.Time{}) w.updateAnimation() - case f := <-driverFuncs: - f() + case <-wakeups: + w.driver.Wakeup() case e := <-w.in: switch e2 := e.(type) { case system.StageEvent: @@ -454,6 +504,10 @@ func (w *Window) run(opts *wm.Options) { w.out <- e2 w.ack <- struct{}{} return + case ViewEvent: + w.out <- e2 + w.waitAck() + case wm.WakeupEvent: case event.Event: if w.queue.q.Queue(e2) { w.setNextFrame(time.Time{})