diff --git a/app/GioView.java b/app/GioView.java index 9d223d00..e7bfbc32 100644 --- a/app/GioView.java +++ b/app/GioView.java @@ -65,8 +65,8 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal private final InputMethodManager imm; private final float scrollXScale; private final float scrollYScale; + private final AccessibilityManager accessManager; private int keyboardHint; - private AccessibilityManager accessManager; private long nhandle; diff --git a/app/egl_wayland.go b/app/egl_wayland.go index f0933364..04aa60d8 100644 --- a/app/egl_wayland.go +++ b/app/egl_wayland.go @@ -69,7 +69,17 @@ func (c *wlContext) Refresh() error { } c.eglWin = eglWin eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin))) - return c.Context.CreateSurface(eglSurf, width, height) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + if err := c.Context.MakeCurrent(); err != nil { + return err + } + defer c.Context.ReleaseCurrent() + // We're in charge of the frame callbacks, don't let eglSwapBuffers + // wait for callbacks that may never arrive. + c.Context.EnableVSync(false) + return nil } func (c *wlContext) Lock() error { diff --git a/app/egl_x11.go b/app/egl_x11.go index 7186cc0b..c60f06de 100644 --- a/app/egl_x11.go +++ b/app/egl_x11.go @@ -46,8 +46,8 @@ func (c *x11Context) Refresh() error { if err := c.Context.MakeCurrent(); err != nil { return err } + defer c.Context.ReleaseCurrent() c.Context.EnableVSync(true) - c.Context.ReleaseCurrent() return nil } diff --git a/app/os.go b/app/os.go index 39595d8f..373210b1 100644 --- a/app/os.go +++ b/app/os.go @@ -7,7 +7,9 @@ import ( "image" "image/color" + "gioui.org/io/event" "gioui.org/io/key" + "gioui.org/op" "gioui.org/gpu" "gioui.org/io/pointer" @@ -131,6 +133,28 @@ func (o Orientation) String() string { return "" } +// eventLoop implements the functionality required for drivers where +// window event loops must run on a separate thread. +type eventLoop struct { + win *callbacks + // wakeup is the callback to wake up the event loop. + wakeup func() + // driverFuncs is a channel of functions to run the next + // time the window loop waits for events. + driverFuncs chan func() + // invalidates is notified when an invalidate is requested by the client. + invalidates chan struct{} + // immediateInvalidates is an optimistic invalidates that doesn't require a wakeup. + immediateInvalidates chan struct{} + // events is where the platform backend delivers events bound for the + // user program. + events chan event.Event + frames chan *op.Ops + frameAck chan struct{} + // delivering avoids re-entrant event delivery. + delivering bool +} + type frameEvent struct { FrameEvent @@ -147,9 +171,19 @@ type context interface { Unlock() } -// Driver is the interface for the platform implementation +// basicDriver is the subset of [driver] that may be called even after +// a window is destroyed. +type basicDriver interface { + // Event blocks until an even is available and returns it. + Event() event.Event + // Invalidate requests a FrameEvent. + Invalidate() +} + +// driver is the interface for the platform implementation // of a window. type driver interface { + basicDriver // SetAnimating sets the animation flag. When the window is animating, // FrameEvents are delivered as fast as the display can handle them. SetAnimating(anim bool) @@ -166,17 +200,23 @@ type driver interface { // SetCursor updates the current cursor to name. SetCursor(cursor pointer.Cursor) // Wakeup wakes up the event loop and sends a WakeupEvent. - Wakeup() + // Wakeup() // Perform actions on the window. Perform(system.Action) // EditorStateChanged notifies the driver that the editor state changed. EditorStateChanged(old, new editorState) + // Run a function on the window thread. + Run(f func()) + // Frame receives a frame. + Frame(frame *op.Ops) + // ProcessEvent processes an event. + ProcessEvent(e event.Event) } type windowRendezvous struct { - in chan windowAndConfig - out chan windowAndConfig - errs chan error + in chan windowAndConfig + out chan windowAndConfig + windows chan struct{} } type windowAndConfig struct { @@ -186,32 +226,137 @@ type windowAndConfig struct { func newWindowRendezvous() *windowRendezvous { wr := &windowRendezvous{ - in: make(chan windowAndConfig), - out: make(chan windowAndConfig), - errs: make(chan error), + in: make(chan windowAndConfig), + out: make(chan windowAndConfig), + windows: make(chan struct{}), } go func() { - var main windowAndConfig + in := wr.in + var window windowAndConfig var out chan windowAndConfig for { select { - case w := <-wr.in: - var err error - if main.window != nil { - err = errors.New("multiple windows are not supported") - } - wr.errs <- err - main = w + case w := <-in: + window = w out = wr.out - case out <- main: + case out <- window: } } }() return wr } -func (wakeupEvent) ImplementsEvent() {} -func (ConfigEvent) ImplementsEvent() {} +func newEventLoop(w *callbacks, wakeup func()) *eventLoop { + return &eventLoop{ + win: w, + wakeup: wakeup, + events: make(chan event.Event), + invalidates: make(chan struct{}, 1), + immediateInvalidates: make(chan struct{}), + frames: make(chan *op.Ops), + frameAck: make(chan struct{}), + driverFuncs: make(chan func(), 1), + } +} + +// Frame receives a frame and waits for its processing. It is called by +// the client goroutine. +func (e *eventLoop) Frame(frame *op.Ops) { + e.frames <- frame + <-e.frameAck +} + +// Event returns the next available event. It is called by the client +// goroutine. +func (e *eventLoop) Event() event.Event { + for { + evt := <-e.events + // Receiving a flushEvent indicates to the platform backend that + // all previous events have been processed by the user program. + if _, ok := evt.(flushEvent); ok { + continue + } + return evt + } +} + +// Invalidate requests invalidation of the window. It is called by the client +// goroutine. +func (e *eventLoop) Invalidate() { + select { + case e.immediateInvalidates <- struct{}{}: + // The event loop was waiting, no need for a wakeup. + case e.invalidates <- struct{}{}: + // The event loop is sleeping, wake it up. + e.wakeup() + default: + // A redraw is pending. + } +} + +// Run f in the window loop thread. It is called by the client goroutine. +func (e *eventLoop) Run(f func()) { + e.driverFuncs <- f + e.wakeup() +} + +// FlushEvents delivers pending events to the client. +func (e *eventLoop) FlushEvents() { + if e.delivering { + return + } + e.delivering = true + defer func() { e.delivering = false }() + for { + evt, ok := e.win.nextEvent() + if !ok { + break + } + e.deliverEvent(evt) + } +} + +func (e *eventLoop) deliverEvent(evt event.Event) { + var frames <-chan *op.Ops + for { + select { + case f := <-e.driverFuncs: + f() + case frame := <-frames: + // The client called FrameEvent.Frame. + frames = nil + e.win.ProcessFrame(frame, e.frameAck) + case e.events <- evt: + switch evt.(type) { + case flushEvent, DestroyEvent: + // DestroyEvents are not flushed. + return + case FrameEvent: + frames = e.frames + } + evt = theFlushEvent + case <-e.invalidates: + e.win.Invalidate() + case <-e.immediateInvalidates: + e.win.Invalidate() + } + } +} + +func (e *eventLoop) Wakeup() { + for { + select { + case f := <-e.driverFuncs: + f() + case <-e.invalidates: + e.win.Invalidate() + case <-e.immediateInvalidates: + e.win.Invalidate() + default: + return + } + } +} func walkActions(actions system.Action, do func(system.Action)) { for a := system.Action(1); actions != 0; a <<= 1 { @@ -221,3 +366,6 @@ func walkActions(actions system.Action, do func(system.Action)) { } } } + +func (wakeupEvent) ImplementsEvent() {} +func (ConfigEvent) ImplementsEvent() {} diff --git a/app/os_android.go b/app/os_android.go index 079c70ab..1a2c3034 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -137,8 +137,10 @@ import ( "unsafe" "gioui.org/internal/f32color" + "gioui.org/op" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/input" "gioui.org/io/key" "gioui.org/io/pointer" @@ -150,6 +152,7 @@ import ( type window struct { callbacks *callbacks + loop *eventLoop view C.jobject handle cgo.Handle @@ -162,8 +165,9 @@ type window struct { started bool animating bool - win *C.ANativeWindow - config Config + win *C.ANativeWindow + config Config + inputHint key.InputHint semantic struct { hoverID input.SemanticID @@ -487,24 +491,30 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j }) view = C.jni_NewGlobalRef(env, view) wopts := <-mainWindow.out + var cnf Config w, ok := windows[wopts.window] if !ok { w = &window{ callbacks: wopts.window, } + w.loop = newEventLoop(w.callbacks, w.wakeup) + w.callbacks.SetDriver(w) + cnf.apply(unit.Metric{}, wopts.options) windows[wopts.window] = w + } else { + cnf = w.config } + mainWindow.windows <- struct{}{} if w.view != 0 { w.detach(env) } w.view = view w.handle = cgo.NewHandle(w) - w.callbacks.SetDriver(w) w.loadConfig(env, class) - w.Configure(wopts.options) - w.SetInputHint(key.HintAny) + w.setConfig(env, cnf) + w.SetInputHint(w.inputHint) w.setStage(StagePaused) - w.callbacks.Event(ViewEvent{View: uintptr(view)}) + w.processEvent(ViewEvent{View: uintptr(view)}) return C.jlong(w.handle) } @@ -579,7 +589,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view //export Java_org_gioui_GioView_onBack func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean { w := cgo.Handle(view).Value().(*window) - if w.callbacks.Event(key.Event{Name: key.NameBack}) { + if w.processEvent(key.Event{Name: key.NameBack}) { return C.JNI_TRUE } return C.JNI_FALSE @@ -588,7 +598,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 := cgo.Handle(view).Value().(*window) - w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE}) + w.processEvent(key.FocusEvent{Focus: focus == C.JNI_TRUE}) } //export Java_org_gioui_GioView_onWindowInsets @@ -663,6 +673,34 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view } } +func (w *window) ProcessEvent(e event.Event) { + w.processEvent(e) +} + +func (w *window) processEvent(e event.Event) bool { + if !w.callbacks.ProcessEvent(e) { + return false + } + w.loop.FlushEvents() + return true +} + +func (w *window) Event() event.Event { + return w.loop.Event() +} + +func (w *window) Invalidate() { + w.loop.Invalidate() +} + +func (w *window) Run(f func()) { + w.loop.Run(f) +} + +func (w *window) Frame(frame *op.Ops) { + w.loop.Frame(frame) +} + func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode, off image.Point, info C.jobject) error { for _, ch := range sem.Children { err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID))) @@ -767,8 +805,7 @@ func (w *window) semIDFor(virtID C.jint) input.SemanticID { func (w *window) detach(env *C.JNIEnv) { callVoidMethod(env, w.view, gioView.unregister) - w.callbacks.Event(ViewEvent{}) - w.callbacks.SetDriver(nil) + w.processEvent(ViewEvent{}) w.handle.Delete() C.jni_DeleteGlobalRef(env, w.view) w.view = 0 @@ -788,7 +825,7 @@ func (w *window) setStage(stage Stage) { return } w.stage = stage - w.callbacks.Event(StageEvent{stage}) + w.processEvent(StageEvent{stage}) } func (w *window) setVisual(visID int) error { @@ -830,7 +867,7 @@ func (w *window) draw(env *C.JNIEnv, sync bool) { size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win))) if size != w.config.Size { w.config.Size = size - w.callbacks.Event(ConfigEvent{Config: w.config}) + w.processEvent(ConfigEvent{Config: w.config}) } if size.X == 0 || size.Y == 0 { return @@ -844,7 +881,7 @@ func (w *window) draw(env *C.JNIEnv, sync bool) { Left: unit.Dp(w.insets.left) * dppp, Right: unit.Dp(w.insets.right) * dppp, } - w.callbacks.Event(frameEvent{ + w.processEvent(frameEvent{ FrameEvent: FrameEvent{ Now: time.Now(), Size: w.config.Size, @@ -944,7 +981,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j if pressed == C.JNI_TRUE { state = key.Press } - w.callbacks.Event(key.Event{Name: n, State: state}) + w.processEvent(key.Event{Name: n, State: state}) } if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224). w.callbacks.EditorInsert(string(rune(r))) @@ -994,7 +1031,7 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C default: return } - w.callbacks.Event(pointer.Event{ + w.processEvent(pointer.Event{ Kind: kind, Source: src, Buttons: btns, @@ -1146,6 +1183,8 @@ func (w *window) ShowTextInput(show bool) { } func (w *window) SetInputHint(mode key.InputHint) { + w.inputHint = mode + // Constants defined at https://developer.android.com/reference/android/text/InputType. const ( TYPE_NULL = 0 @@ -1292,9 +1331,9 @@ func findClass(env *C.JNIEnv, name string) C.jclass { func osMain() { } -func newWindow(window *callbacks, options []Option) error { +func newWindow(window *callbacks, options []Option) { mainWindow.in <- windowAndConfig{window, options} - return <-mainWindow.errs + <-mainWindow.windows } func (w *window) WriteClipboard(mime string, s []byte) { @@ -1313,7 +1352,7 @@ func (w *window) ReadClipboard() { return } content := goString(env, C.jstring(c)) - w.callbacks.Event(transfer.DataEvent{ + w.processEvent(transfer.DataEvent{ Type: "application/text", Open: func() io.ReadCloser { return io.NopCloser(strings.NewReader(content)) @@ -1323,42 +1362,46 @@ func (w *window) ReadClipboard() { } func (w *window) Configure(options []Option) { + cnf := w.config + cnf.apply(unit.Metric{}, options) runInJVM(javaVM(), func(env *C.JNIEnv) { - prev := w.config - cnf := w.config - cnf.apply(unit.Metric{}, options) - // Decorations are never disabled. - cnf.Decorated = true - - if prev.Orientation != cnf.Orientation { - w.config.Orientation = cnf.Orientation - setOrientation(env, w.view, cnf.Orientation) - } - if prev.NavigationColor != cnf.NavigationColor { - w.config.NavigationColor = cnf.NavigationColor - setNavigationColor(env, w.view, cnf.NavigationColor) - } - if prev.StatusColor != cnf.StatusColor { - w.config.StatusColor = cnf.StatusColor - setStatusColor(env, w.view, cnf.StatusColor) - } - if prev.Mode != cnf.Mode { - switch cnf.Mode { - case Fullscreen: - callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE) - w.config.Mode = Fullscreen - case Windowed: - callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE) - w.config.Mode = Windowed - } - } - if cnf.Decorated != prev.Decorated { - w.config.Decorated = cnf.Decorated - } - w.callbacks.Event(ConfigEvent{Config: w.config}) + w.setConfig(env, cnf) }) } +func (w *window) setConfig(env *C.JNIEnv, cnf Config) { + prev := w.config + // Decorations are never disabled. + cnf.Decorated = true + + if prev.Orientation != cnf.Orientation { + w.config.Orientation = cnf.Orientation + setOrientation(env, w.view, cnf.Orientation) + } + if prev.NavigationColor != cnf.NavigationColor { + w.config.NavigationColor = cnf.NavigationColor + setNavigationColor(env, w.view, cnf.NavigationColor) + } + if prev.StatusColor != cnf.StatusColor { + w.config.StatusColor = cnf.StatusColor + setStatusColor(env, w.view, cnf.StatusColor) + } + if prev.Mode != cnf.Mode { + switch cnf.Mode { + case Fullscreen: + callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE) + w.config.Mode = Fullscreen + case Windowed: + callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE) + w.config.Mode = Windowed + } + } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } + w.processEvent(ConfigEvent{Config: w.config}) +} + func (w *window) Perform(system.Action) {} func (w *window) SetCursor(cursor pointer.Cursor) { @@ -1367,9 +1410,10 @@ func (w *window) SetCursor(cursor pointer.Cursor) { }) } -func (w *window) Wakeup() { +func (w *window) wakeup() { runOnMain(func(env *C.JNIEnv) { - w.callbacks.Event(wakeupEvent{}) + w.loop.Wakeup() + w.loop.FlushEvents() }) } diff --git a/app/os_darwin.go b/app/os_darwin.go index fb8491a9..8f82cc94 100644 --- a/app/os_darwin.go +++ b/app/os_darwin.go @@ -260,8 +260,9 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor { return to } -func (w *window) Wakeup() { +func (w *window) wakeup() { runOnMain(func() { - w.w.Event(wakeupEvent{}) + w.loop.Wakeup() + w.loop.FlushEvents() }) } diff --git a/app/os_ios.go b/app/os_ios.go index 55ee8faa..0adfab29 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -81,10 +81,12 @@ import ( "unsafe" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/io/transfer" + "gioui.org/op" "gioui.org/unit" ) @@ -97,10 +99,11 @@ type window struct { view C.CFTypeRef w *callbacks displayLink *displayLink + loop *eventLoop - visible bool - cursor pointer.Cursor - config Config + hidden bool + cursor pointer.Cursor + config Config pointerMap []C.CFTypeRef } @@ -116,23 +119,26 @@ func init() { //export onCreate func onCreate(view, controller C.CFTypeRef) { + wopts := <-mainWindow.out w := &window{ view: view, + w: wopts.window, } + w.loop = newEventLoop(w.w, w.wakeup) + w.w.SetDriver(w) + mainWindow.windows <- struct{}{} dl, err := newDisplayLink(func() { w.draw(false) }) if err != nil { - panic(err) + w.w.ProcessEvent(DestroyEvent{Err: err}) + return } w.displayLink = dl - wopts := <-mainWindow.out - w.w = wopts.window - w.w.SetDriver(w) views[view] = w w.Configure(wopts.options) - w.w.Event(StageEvent{Stage: StagePaused}) - w.w.Event(ViewEvent{ViewController: uintptr(controller)}) + w.ProcessEvent(StageEvent{Stage: StageRunning}) + w.ProcessEvent(ViewEvent{ViewController: uintptr(controller)}) } //export gio_onDraw @@ -142,22 +148,20 @@ func gio_onDraw(view C.CFTypeRef) { } func (w *window) draw(sync bool) { + if w.hidden { + return + } params := C.viewDrawParams(w.view) if params.width == 0 || params.height == 0 { return } - wasVisible := w.visible - w.visible = true - if !wasVisible { - w.w.Event(StageEvent{Stage: StageRunning}) - } const inchPrDp = 1.0 / 163 m := unit.Metric{ PxPerDp: float32(params.dpi) * inchPrDp, PxPerSp: float32(params.sdpi) * inchPrDp, } dppp := unit.Dp(1. / m.PxPerDp) - w.w.Event(frameEvent{ + w.ProcessEvent(frameEvent{ FrameEvent: FrameEvent{ Now: time.Now(), Size: image.Point{ @@ -179,24 +183,33 @@ func (w *window) draw(sync bool) { //export onStop func onStop(view C.CFTypeRef) { w := views[view] - w.visible = false - w.w.Event(StageEvent{Stage: StagePaused}) + w.hidden = true + w.ProcessEvent(StageEvent{Stage: StagePaused}) +} + +//export onStart +func onStart(view C.CFTypeRef) { + w := views[view] + w.hidden = false + w.ProcessEvent(StageEvent{Stage: StageRunning}) + w.draw(true) } //export onDestroy func onDestroy(view C.CFTypeRef) { w := views[view] - delete(views, view) - w.w.Event(ViewEvent{}) - w.w.Event(DestroyEvent{}) + w.ProcessEvent(ViewEvent{}) + w.ProcessEvent(DestroyEvent{}) w.displayLink.Close() + w.displayLink = nil + delete(views, view) w.view = 0 } //export onFocus func onFocus(view C.CFTypeRef, focus int) { w := views[view] - w.w.Event(key.FocusEvent{Focus: focus != 0}) + w.ProcessEvent(key.FocusEvent{Focus: focus != 0}) } //export onLowMemory @@ -254,7 +267,7 @@ func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.C w := views[view] t := time.Duration(float64(ti) * float64(time.Second)) p := f32.Point{X: float32(x), Y: float32(y)} - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: kind, Source: pointer.Touch, PointerID: w.lookupTouch(last != 0, touchRef), @@ -267,7 +280,7 @@ func (w *window) ReadClipboard() { cstr := C.readClipboard() defer C.CFRelease(cstr) content := nsstringToString(cstr) - w.w.Event(transfer.DataEvent{ + w.ProcessEvent(transfer.DataEvent{ Type: "application/text", Open: func() io.ReadCloser { return io.NopCloser(strings.NewReader(content)) @@ -287,7 +300,7 @@ func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) Configure([]Option) { // Decorations are never disabled. w.config.Decorated = true - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } func (w *window) EditorStateChanged(old, new editorState) {} @@ -295,10 +308,6 @@ func (w *window) EditorStateChanged(old, new editorState) {} func (w *window) Perform(system.Action) {} func (w *window) SetAnimating(anim bool) { - v := w.view - if v == 0 { - return - } if anim { w.displayLink.Start() } else { @@ -311,7 +320,7 @@ func (w *window) SetCursor(cursor pointer.Cursor) { } func (w *window) onKeyCommand(name key.Name) { - w.w.Event(key.Event{ + w.ProcessEvent(key.Event{ Name: name, }) } @@ -350,9 +359,30 @@ func (w *window) ShowTextInput(show bool) { func (w *window) SetInputHint(_ key.InputHint) {} -func newWindow(win *callbacks, options []Option) error { +func (w *window) ProcessEvent(e event.Event) { + w.w.ProcessEvent(e) + w.loop.FlushEvents() +} + +func (w *window) Event() event.Event { + return w.loop.Event() +} + +func (w *window) Invalidate() { + w.loop.Invalidate() +} + +func (w *window) Run(f func()) { + w.loop.Run(f) +} + +func (w *window) Frame(frame *op.Ops) { + w.loop.Frame(frame) +} + +func newWindow(win *callbacks, options []Option) { mainWindow.in <- windowAndConfig{win, options} - return <-mainWindow.errs + <-mainWindow.windows } func osMain() { diff --git a/app/os_ios.m b/app/os_ios.m index 7bc2524d..c185a431 100644 --- a/app/os_ios.m +++ b/app/os_ios.m @@ -56,7 +56,7 @@ CGFloat _keyboardHeight; - (void)applicationWillEnterForeground:(UIApplication *)application { UIView *drawView = self.view.subviews[0]; if (drawView != nil) { - gio_onDraw((__bridge CFTypeRef)drawView); + onStart((__bridge CFTypeRef)drawView); } } diff --git a/app/os_js.go b/app/os_js.go index 29f38b53..7e1aace0 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -14,8 +14,10 @@ import ( "unicode/utf8" "gioui.org/internal/f32color" + "gioui.org/op" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" @@ -54,9 +56,6 @@ type window struct { composing bool requestFocus bool - chanAnimation chan struct{} - chanRedraw chan struct{} - config Config inset f32.Point scale float32 @@ -69,7 +68,7 @@ type window struct { contextStatus contextStatus } -func newWindow(win *callbacks, options []Option) error { +func newWindow(win *callbacks, options []Option) { doc := js.Global().Get("document") cont := getContainer(doc) cnv := createCanvas(doc) @@ -84,7 +83,9 @@ func newWindow(win *callbacks, options []Option) error { head: doc.Get("head"), clipboard: js.Global().Get("navigator").Get("clipboard"), wakeups: make(chan struct{}, 1), + w: win, } + w.w.SetDriver(w) w.requestAnimationFrame = w.window.Get("requestAnimationFrame") w.browserHistory = w.window.Get("history") w.visualViewport = w.window.Get("visualViewport") @@ -94,15 +95,13 @@ func newWindow(win *callbacks, options []Option) error { if screen := w.window.Get("screen"); screen.Truthy() { w.screenOrientation = screen.Get("orientation") } - w.chanAnimation = make(chan struct{}, 1) - w.chanRedraw = make(chan struct{}, 1) w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} { - w.chanAnimation <- struct{}{} + w.draw(false) return nil }) w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} { content := args[0].String() - go win.Event(transfer.DataEvent{ + w.processEvent(transfer.DataEvent{ Type: "application/text", Open: func() io.ReadCloser { return io.NopCloser(strings.NewReader(content)) @@ -112,29 +111,13 @@ func newWindow(win *callbacks, options []Option) error { }) w.addEventListeners() w.addHistory() - w.w = win - go func() { - defer w.cleanup() - w.w.SetDriver(w) - w.Configure(options) - w.blur() - w.w.Event(ViewEvent{Element: cont}) - w.w.Event(StageEvent{Stage: StageRunning}) - w.resize() - w.draw(true) - for { - select { - case <-w.wakeups: - w.w.Event(wakeupEvent{}) - case <-w.chanAnimation: - w.animCallback() - case <-w.chanRedraw: - w.draw(true) - } - } - }() - return nil + w.Configure(options) + w.blur() + w.processEvent(ViewEvent{Element: cont}) + w.processEvent(StageEvent{Stage: StageRunning}) + w.resize() + w.draw(true) } func getContainer(doc js.Value) js.Value { @@ -194,12 +177,12 @@ func (w *window) addEventListeners() { w.cnv.Set("width", 0) w.cnv.Set("height", 0) w.resize() - w.requestRedraw() + w.draw(true) return nil }) w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} { w.resize() - w.requestRedraw() + w.draw(true) return nil }) w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} { @@ -207,7 +190,7 @@ func (w *window) addEventListeners() { return nil }) w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} { - if w.w.Event(key.Event{Name: key.NameBack}) { + if w.processEvent(key.Event{Name: key.NameBack}) { return w.browserHistory.Call("forward") } return w.browserHistory.Call("back") @@ -220,7 +203,7 @@ func (w *window) addEventListeners() { default: ev.Stage = StageRunning } - w.w.Event(ev) + w.processEvent(ev) return nil }) w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} { @@ -280,18 +263,18 @@ func (w *window) addEventListeners() { w.touches[i] = js.Null() } w.touches = w.touches[:0] - w.w.Event(pointer.Event{ + w.processEvent(pointer.Event{ Kind: pointer.Cancel, Source: pointer.Touch, }) return nil }) w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} { - w.w.Event(key.FocusEvent{Focus: true}) + w.processEvent(key.FocusEvent{Focus: true}) return nil }) w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} { - w.w.Event(key.FocusEvent{Focus: false}) + w.processEvent(key.FocusEvent{Focus: false}) w.blur() return nil }) @@ -380,10 +363,50 @@ func (w *window) keyEvent(e js.Value, ks key.State) { Modifiers: modifiersFor(e), State: ks, } - w.w.Event(cmd) + w.processEvent(cmd) } } +func (w *window) ProcessEvent(e event.Event) { + w.processEvent(e) +} + +func (w *window) processEvent(e event.Event) bool { + if !w.w.ProcessEvent(e) { + return false + } + select { + case w.wakeups <- struct{}{}: + default: + } + return true +} + +func (w *window) Event() event.Event { + for { + evt, ok := w.w.nextEvent() + if ok { + if _, destroy := evt.(DestroyEvent); destroy { + w.cleanup() + } + return evt + } + <-w.wakeups + } +} + +func (w *window) Invalidate() { + w.w.Invalidate() +} + +func (w *window) Run(f func()) { + f() +} + +func (w *window) Frame(frame *op.Ops) { + w.w.ProcessFrame(frame, nil) +} + // modifiersFor returns the modifier set for a DOM MouseEvent or // KeyEvent. func modifiersFor(e js.Value) key.Modifiers { @@ -431,7 +454,7 @@ func (w *window) touchEvent(kind pointer.Kind, e js.Value) { X: float32(x) * scale, Y: float32(y) * scale, } - w.w.Event(pointer.Event{ + w.processEvent(pointer.Event{ Kind: kind, Source: pointer.Touch, Position: pos, @@ -481,7 +504,7 @@ func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) { if jbtns&4 != 0 { btns |= pointer.ButtonTertiary } - w.w.Event(pointer.Event{ + w.processEvent(pointer.Event{ Kind: kind, Source: pointer.Mouse, Buttons: btns, @@ -508,17 +531,6 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F return jsf } -func (w *window) animCallback() { - anim := w.animating - w.animRequested = anim - if anim { - w.requestAnimationFrame.Invoke(w.redraw) - } - if anim { - w.draw(false) - } -} - func (w *window) EditorStateChanged(old, new editorState) {} func (w *window) SetAnimating(anim bool) { @@ -574,7 +586,7 @@ func (w *window) Configure(options []Option) { if cnf.Decorated != prev.Decorated { w.config.Decorated = cnf.Decorated } - w.w.Event(ConfigEvent{Config: w.config}) + w.processEvent(ConfigEvent{Config: w.config}) } func (w *window) Perform(system.Action) {} @@ -613,23 +625,14 @@ func (w *window) SetCursor(cursor pointer.Cursor) { style.Set("cursor", webCursor[cursor]) } -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. - go func() { - if show { - w.focus() - } else { - w.blur() - } - }() + if show { + w.focus() + } else { + w.blur() + } } func (w *window) SetInputHint(mode key.InputHint) { @@ -646,7 +649,7 @@ func (w *window) resize() { } if size != w.config.Size { w.config.Size = size - w.w.Event(ConfigEvent{Config: w.config}) + w.processEvent(ConfigEvent{Config: w.config}) } if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() { @@ -666,12 +669,19 @@ func (w *window) draw(sync bool) { if w.contextStatus == contextStatusLost { return } + anim := w.animating + w.animRequested = anim + if anim { + w.requestAnimationFrame.Invoke(w.redraw) + } else if !sync { + return + } size, insets, metric := w.getConfig() if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 { return } - w.w.Event(frameEvent{ + w.processEvent(frameEvent{ FrameEvent: FrameEvent{ Now: time.Now(), Size: size, @@ -741,13 +751,6 @@ func (w *window) navigationColor(c color.NRGBA) { theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B})) } -func (w *window) requestRedraw() { - select { - case w.chanRedraw <- struct{}{}: - default: - } -} - func osMain() { select {} } diff --git a/app/os_macos.go b/app/os_macos.go index 3b7982bd..b7842c33 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -16,10 +16,12 @@ import ( "unicode/utf8" "gioui.org/internal/f32" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/io/transfer" + "gioui.org/op" "gioui.org/unit" _ "gioui.org/internal/cocoainit" @@ -256,6 +258,7 @@ type window struct { redraw chan struct{} cursor pointer.Cursor pointerBtns pointer.Buttons + loop *eventLoop scale float32 config Config @@ -274,25 +277,13 @@ var nextTopLeft C.NSPoint // mustView is like lookupView, except that it panics // if the view isn't mapped. func mustView(view C.CFTypeRef) *window { - w, ok := lookupView(view) - if !ok { + w, exists := viewMap[view] + if !exists { panic("no window for view") } return w } -func lookupView(view C.CFTypeRef) (*window, bool) { - w, exists := viewMap[view] - if !exists { - return nil, false - } - return w, true -} - -func deleteView(view C.CFTypeRef) { - delete(viewMap, view) -} - func insertView(view C.CFTypeRef, w *window) { viewMap[view] = w } @@ -307,7 +298,7 @@ func (w *window) ReadClipboard() { defer C.CFRelease(cstr) } content := nsstringToString(cstr) - w.w.Event(transfer.DataEvent{ + w.ProcessEvent(transfer.DataEvent{ Type: "application/text", Open: func() io.ReadCloser { return io.NopCloser(strings.NewReader(content)) @@ -420,7 +411,7 @@ func (w *window) Configure(options []Option) { C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans) C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans) } - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } func (w *window) setTitle(prev, cnf Config) { @@ -493,7 +484,7 @@ func (w *window) setStage(stage Stage) { return } w.stage = stage - w.w.Event(StageEvent{Stage: stage}) + w.ProcessEvent(StageEvent{Stage: stage}) } //export gio_onKeys @@ -507,7 +498,7 @@ func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown w := mustView(view) for _, k := range str { if n, ok := convertKey(k); ok { - w.w.Event(key.Event{ + w.ProcessEvent(key.Event{ Name: n, Modifiers: kmods, State: ks, @@ -562,7 +553,7 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, default: panic("invalid direction") } - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: typ, Source: pointer.Mouse, Time: t, @@ -582,7 +573,7 @@ func gio_onDraw(view C.CFTypeRef) { //export gio_onFocus func gio_onFocus(view C.CFTypeRef, focus C.int) { w := mustView(view) - w.w.Event(key.FocusEvent{Focus: focus == 1}) + w.ProcessEvent(key.FocusEvent{Focus: focus == 1}) if w.stage >= StageInactive { if focus == 0 { w.setStage(StageInactive) @@ -776,14 +767,14 @@ func (w *window) draw() { } if sz != w.config.Size { w.config.Size = sz - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } if sz.X == 0 || sz.Y == 0 { return } cfg := configFor(w.scale) w.setStage(StageRunning) - w.w.Event(frameEvent{ + w.ProcessEvent(frameEvent{ FrameEvent: FrameEvent{ Now: time.Now(), Size: w.config.Size, @@ -793,6 +784,27 @@ func (w *window) draw() { }) } +func (w *window) ProcessEvent(e event.Event) { + w.w.ProcessEvent(e) + w.loop.FlushEvents() +} + +func (w *window) Event() event.Event { + return w.loop.Event() +} + +func (w *window) Invalidate() { + w.loop.Invalidate() +} + +func (w *window) Run(f func()) { + w.loop.Run(f) +} + +func (w *window) Frame(frame *op.Ops) { + w.loop.Frame(frame) +} + func configFor(scale float32) unit.Metric { return unit.Metric{ PxPerDp: scale, @@ -803,11 +815,12 @@ func configFor(scale float32) unit.Metric { //export gio_onClose func gio_onClose(view C.CFTypeRef) { w := mustView(view) - w.w.Event(ViewEvent{}) - w.w.Event(DestroyEvent{}) + w.ProcessEvent(ViewEvent{}) + w.setStage(StagePaused) + w.ProcessEvent(DestroyEvent{}) w.displayLink.Close() w.displayLink = nil - deleteView(view) + delete(viewMap, view) C.CFRelease(w.view) w.view = 0 } @@ -828,14 +841,14 @@ func gio_onShow(view C.CFTypeRef) { func gio_onFullscreen(view C.CFTypeRef) { w := mustView(view) w.config.Mode = Fullscreen - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } //export gio_onWindowed func gio_onWindowed(view C.CFTypeRef) { w := mustView(view) w.config.Mode = Windowed - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } //export gio_onAppHide @@ -857,20 +870,23 @@ func gio_onFinishLaunching() { close(launched) } -func newWindow(win *callbacks, options []Option) error { +func newWindow(win *callbacks, options []Option) { <-launched - errch := make(chan error) + res := make(chan struct{}) runOnMain(func() { - w, err := newOSWindow() - if err != nil { - errch <- err + w := &window{ + redraw: make(chan struct{}, 1), + w: win, + } + w.loop = newEventLoop(w.w, w.wakeup) + win.SetDriver(w) + res <- struct{}{} + if err := w.init(); err != nil { + w.ProcessEvent(DestroyEvent{Err: err}) return } - errch <- nil - w.w = win window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0) w.updateWindowMode() - win.SetDriver(w) w.Configure(options) if nextTopLeft.x == 0 && nextTopLeft.y == 0 { // cascadeTopLeftFromPoint treats (0, 0) as a no-op, @@ -881,22 +897,18 @@ func newWindow(win *callbacks, options []Option) error { // makeKeyAndOrderFront assumes ownership of our window reference. C.makeKeyAndOrderFront(window) layer := C.layerForView(w.view) - w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)}) + w.ProcessEvent(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)}) }) - return <-errch + <-res } -func newOSWindow() (*window, error) { +func (w *window) init() error { view := C.gio_createView() if view == 0 { - return nil, errors.New("newOSWindows: failed to create view") + return errors.New("newOSWindow: failed to create view") } scale := float32(C.getViewBackingScale(view)) - w := &window{ - view: view, - scale: scale, - redraw: make(chan struct{}, 1), - } + w.scale = scale dl, err := newDisplayLink(func() { select { case w.redraw <- struct{}{}: @@ -910,10 +922,11 @@ func newOSWindow() (*window, error) { w.displayLink = dl if err != nil { C.CFRelease(view) - return nil, err + return err } insertView(view, w) - return w, nil + w.view = view + return nil } func osMain() { diff --git a/app/os_unix.go b/app/os_unix.go index 4ec413bb..492a38cc 100644 --- a/app/os_unix.go +++ b/app/os_unix.go @@ -9,6 +9,7 @@ import ( "errors" "unsafe" + "gioui.org/io/event" "gioui.org/io/pointer" ) @@ -49,7 +50,7 @@ type windowDriver func(*callbacks, []Option) error // let each driver initialize these variables with their own version of createWindow. var wlDriver, x11Driver windowDriver -func newWindow(window *callbacks, options []Option) error { +func newWindow(window *callbacks, options []Option) { var errFirst error for _, d := range []windowDriver{wlDriver, x11Driver} { if d == nil { @@ -57,16 +58,39 @@ func newWindow(window *callbacks, options []Option) error { } err := d(window, options) if err == nil { - return nil + return } if errFirst == nil { errFirst = err } } - if errFirst != nil { - return errFirst + window.SetDriver(&dummyDriver{ + win: window, + wakeups: make(chan event.Event, 1), + }) + if errFirst == nil { + errFirst = errors.New("app: no window driver available") + } + window.ProcessEvent(DestroyEvent{Err: errFirst}) +} + +type dummyDriver struct { + win *callbacks + wakeups chan event.Event +} + +func (d *dummyDriver) Event() event.Event { + if e, ok := d.win.nextEvent(); ok { + return e + } + return <-d.wakeups +} + +func (d *dummyDriver) Invalidate() { + select { + case d.wakeups <- wakeupEvent{}: + default: } - return errors.New("app: no window driver available") } // xCursor contains mapping from pointer.Cursor to XCursor. diff --git a/app/os_wayland.go b/app/os_wayland.go index bd3e3a32..a936b731 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -15,6 +15,7 @@ import ( "math" "os" "os/exec" + "runtime" "strconv" "sync" "time" @@ -25,10 +26,12 @@ import ( "gioui.org/app/internal/xkb" "gioui.org/f32" "gioui.org/internal/fling" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/io/transfer" + "gioui.org/op" "gioui.org/unit" ) @@ -97,7 +100,9 @@ type wlDisplay struct { read, write int } - repeat repeatState + repeat repeatState + poller poller + readClipClose chan struct{} } type wlSeat struct { @@ -137,7 +142,7 @@ type repeatState struct { delay time.Duration key uint32 - win *callbacks + win *window stopC chan struct{} start time.Duration @@ -195,11 +200,9 @@ type window struct { } stage Stage - dead bool lastFrameCallback *C.struct_wl_callback animating bool - redraw bool // The most recent configure serial waiting to be ack'ed. serial C.uint32_t scale int @@ -212,6 +215,10 @@ type window struct { clipReads chan transfer.DataEvent wakeups chan struct{} + + // invMu avoids the race between the destruction of disp and + // Invalidate waking it up. + invMu sync.Mutex } type poller struct { @@ -260,25 +267,17 @@ func newWLWindow(callbacks *callbacks, options []Option) error { return err } w.w = callbacks - go func() { - defer d.destroy() - defer w.destroy() + w.w.SetDriver(w) - w.w.SetDriver(w) + // Finish and commit setup from createNativeWindow. + w.Configure(options) + w.draw(true) + C.wl_surface_commit(w.surf) - // Finish and commit setup from createNativeWindow. - w.Configure(options) - C.wl_surface_commit(w.surf) - - w.w.Event(WaylandViewEvent{ - Display: unsafe.Pointer(w.display()), - Surface: unsafe.Pointer(w.surf), - }) - - err := w.loop() - w.w.Event(WaylandViewEvent{}) - w.w.Event(DestroyEvent{Err: err}) - }() + w.ProcessEvent(WaylandViewEvent{ + Display: unsafe.Pointer(w.display()), + Surface: unsafe.Pointer(w.surf), + }) return nil } @@ -549,15 +548,15 @@ func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) { func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) { w := callbackLoad(data).(*window) w.serial = serial - w.redraw = true C.xdg_surface_ack_configure(wmSurf, serial) w.setStage(StageRunning) + w.draw(true) } //export gio_onToplevelClose func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) { w := callbackLoad(data).(*window) - w.dead = true + w.close(nil) } //export gio_onToplevelConfigure @@ -586,8 +585,8 @@ func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_ } else { w.size.Y += int(w.config.decoHeight) } - w.w.Event(ConfigEvent{Config: w.config}) - w.redraw = true + w.ProcessEvent(ConfigEvent{Config: w.config}) + w.draw(true) } } @@ -645,7 +644,7 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output * if w.config.Mode == Minimized { // Minimized window got brought back up: it is no longer so. w.config.Mode = Windowed - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } } @@ -790,7 +789,7 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C. X: fromFixed(x) * float32(w.scale), Y: fromFixed(y) * float32(w.scale), } - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Press, Source: pointer.Touch, Position: w.lastTouch, @@ -806,7 +805,7 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui s.serial = serial w := s.touchFoci[id] delete(s.touchFoci, id) - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Release, Source: pointer.Touch, Position: w.lastTouch, @@ -824,7 +823,7 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32 X: fromFixed(x) * float32(w.scale), Y: fromFixed(y) * float32(w.scale), } - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Move, Position: w.lastTouch, Source: pointer.Touch, @@ -843,7 +842,7 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) { s := callbackLoad(data).(*wlSeat) for id, w := range s.touchFoci { delete(s.touchFoci, id) - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Cancel, Source: pointer.Touch, }) @@ -869,7 +868,7 @@ func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.ui s.serial = serial if w.inCompositor { w.inCompositor = false - w.w.Event(pointer.Event{Kind: pointer.Cancel}) + w.ProcessEvent(pointer.Event{Kind: pointer.Cancel}) } } @@ -930,7 +929,7 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, } w.flushScroll() w.resetFling() - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: kind, Source: pointer.Mouse, Buttons: w.pointerBtns, @@ -1018,8 +1017,11 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis } func (w *window) ReadClipboard() { + if w.disp.readClipClose != nil { + return + } + w.disp.readClipClose = make(chan struct{}) r, err := w.disp.readClipboard() - // Send empty responses on unavailable clipboards or errors. if r == nil || err != nil { return } @@ -1027,13 +1029,17 @@ func (w *window) ReadClipboard() { go func() { defer r.Close() data, _ := io.ReadAll(r) - w.clipReads <- transfer.DataEvent{ + e := transfer.DataEvent{ Type: "application/text", Open: func() io.ReadCloser { return io.NopCloser(bytes.NewReader(data)) }, } - w.Wakeup() + select { + case w.clipReads <- e: + w.disp.wakeup() + case <-w.disp.readClipClose: + } }() } @@ -1096,8 +1102,7 @@ func (w *window) Configure(options []Option) { w.config.MaxSize = cnf.MaxSize w.setWindowConstraints() } - w.w.Event(ConfigEvent{Config: w.config}) - w.redraw = true + w.ProcessEvent(ConfigEvent{Config: w.config}) } func (w *window) setWindowConstraints() { @@ -1134,7 +1139,7 @@ func (w *window) Perform(actions system.Action) { walkActions(actions, func(action system.Action) { switch action { case system.ActionClose: - w.dead = true + w.close(nil) } }) } @@ -1217,7 +1222,7 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se w := callbackLoad(unsafe.Pointer(surf)).(*window) s.keyboardFocus = w s.disp.repeat.Stop(0) - w.w.Event(key.FocusEvent{Focus: true}) + w.ProcessEvent(key.FocusEvent{Focus: true}) } //export gio_onKeyboardLeave @@ -1226,7 +1231,7 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se s.serial = serial s.disp.repeat.Stop(0) w := s.keyboardFocus - w.w.Event(key.FocusEvent{Focus: false}) + w.ProcessEvent(key.FocusEvent{Focus: false}) } //export gio_onKeyboardKey @@ -1244,7 +1249,7 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri // There's no support for IME yet. w.w.EditorInsert(ee.Text) } else { - w.w.Event(e) + w.ProcessEvent(e) } } if state != C.WL_KEYBOARD_KEY_STATE_PRESSED { @@ -1279,7 +1284,7 @@ func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) { r.now = 0 r.stopC = stopC r.key = keyCode - r.win = w.w + r.win = w rate, delay := r.rate, r.delay go func() { timer := time.NewTimer(delay) @@ -1337,9 +1342,9 @@ func (r *repeatState) Repeat(d *wlDisplay) { for _, e := range d.xkb.DispatchKey(r.key, key.Press) { if ee, ok := e.(key.EditEvent); ok { // There's no support for IME yet. - r.win.EditorInsert(ee.Text) + r.win.w.EditorInsert(ee.Text) } else { - r.win.Event(e) + r.win.ProcessEvent(e) } } r.last += delay @@ -1352,28 +1357,76 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui w := callbackLoad(data).(*window) if w.lastFrameCallback == callback { w.lastFrameCallback = nil + w.draw(false) } } -func (w *window) loop() error { - var p poller - for { - if err := w.disp.dispatch(&p); err != nil { - return err - } - select { - case e := <-w.clipReads: - w.w.Event(e) - case <-w.wakeups: - w.w.Event(wakeupEvent{}) - default: - } - if w.dead { - break - } - w.draw() +func (w *window) close(err error) { + w.ProcessEvent(WaylandViewEvent{}) + w.ProcessEvent(DestroyEvent{Err: err}) +} + +func (w *window) dispatch() { + if w.disp == nil { + <-w.wakeups + w.w.Invalidate() + return } - return nil + if err := w.disp.dispatch(); err != nil { + w.close(err) + return + } + select { + case e := <-w.clipReads: + w.disp.readClipClose = nil + w.ProcessEvent(e) + case <-w.wakeups: + w.w.Invalidate() + default: + } +} + +func (w *window) ProcessEvent(e event.Event) { + w.w.ProcessEvent(e) +} + +func (w *window) Event() event.Event { + for { + evt, ok := w.w.nextEvent() + if !ok { + w.dispatch() + continue + } + if _, destroy := evt.(DestroyEvent); destroy { + w.destroy() + w.invMu.Lock() + w.disp.destroy() + w.disp = nil + w.invMu.Unlock() + } + return evt + } +} + +func (w *window) Invalidate() { + select { + case w.wakeups <- struct{}{}: + default: + return + } + w.invMu.Lock() + defer w.invMu.Unlock() + if w.disp != nil { + w.disp.wakeup() + } +} + +func (w *window) Run(f func()) { + f() +} + +func (w *window) Frame(frame *op.Ops) { + w.w.ProcessFrame(frame, nil) } // bindDataDevice initializes the dataDev field if and only if both @@ -1389,13 +1442,21 @@ func (d *wlDisplay) bindDataDevice() { } } -func (d *wlDisplay) dispatch(p *poller) error { +func (d *wlDisplay) dispatch() error { + // wl_display_prepare_read records the current thread for + // use in wl_display_read_events or wl_display_cancel_events. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + dispfd := C.wl_display_get_fd(d.disp) // Poll for events and notifications. - pollfds := append(p.pollfds[:0], + pollfds := append(d.poller.pollfds[:0], syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR}, syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR}, ) + for C.wl_display_prepare_read(d.disp) != 0 { + C.wl_display_dispatch_pending(d.disp) + } dispFd := &pollfds[0] if ret, err := C.wl_display_flush(d.disp); ret < 0 { if err != syscall.EAGAIN { @@ -1406,11 +1467,25 @@ func (d *wlDisplay) dispatch(p *poller) error { dispFd.Events |= syscall.POLLOUT } if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { + C.wl_display_cancel_read(d.disp) return fmt.Errorf("wayland: poll failed: %v", err) } + if dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0 { + C.wl_display_cancel_read(d.disp) + return errors.New("wayland: display file descriptor gone") + } + // Handle events. + if dispFd.Revents&syscall.POLLIN != 0 { + if ret, err := C.wl_display_read_events(d.disp); ret < 0 { + return fmt.Errorf("wayland: wl_display_read_events failed: %v", err) + } + C.wl_display_dispatch_pending(d.disp) + } else { + C.wl_display_cancel_read(d.disp) + } // Clear notifications. for { - _, err := syscall.Read(d.notify.read, p.buf[:]) + _, err := syscall.Read(d.notify.read, d.poller.buf[:]) if err == syscall.EAGAIN { break } @@ -1418,29 +1493,15 @@ func (d *wlDisplay) dispatch(p *poller) error { return fmt.Errorf("wayland: read from notify pipe failed: %v", err) } } - // Handle events - switch { - case dispFd.Revents&syscall.POLLIN != 0: - if ret, err := C.wl_display_dispatch(d.disp); ret < 0 { - return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err) - } - case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0: - return errors.New("wayland: display file descriptor gone") - } d.repeat.Repeat(d) return nil } -func (w *window) Wakeup() { - select { - case w.wakeups <- struct{}{}: - default: - } - w.disp.wakeup() -} - func (w *window) SetAnimating(anim bool) { w.animating = anim + if anim { + w.draw(false) + } } // Wakeup wakes up the event loop through the notification pipe. @@ -1576,7 +1637,7 @@ func (w *window) flushScroll() { if total == (f32.Point{}) { return } - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Scroll, Source: pointer.Mouse, Buttons: w.pointerBtns, @@ -1599,7 +1660,7 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { X: fromFixed(x) * float32(w.scale), Y: fromFixed(y) * float32(w.scale), } - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Move, Position: w.lastPos, Buttons: w.pointerBtns, @@ -1675,13 +1736,13 @@ func (w *window) updateOutputs() { if found && scale != w.scale { w.scale = scale C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale)) - w.redraw = true + w.draw(true) } if !found { w.setStage(StagePaused) } else { w.setStage(StageRunning) - w.redraw = true + w.draw(true) } } @@ -1693,7 +1754,7 @@ func (w *window) getConfig() (image.Point, unit.Metric) { } } -func (w *window) draw() { +func (w *window) draw(sync bool) { w.flushScroll() size, cfg := w.getConfig() if cfg == (unit.Metric{}) { @@ -1701,11 +1762,9 @@ func (w *window) draw() { } if size != w.config.Size { w.config.Size = size - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } anim := w.animating || w.fling.anim.Active() - sync := w.redraw - w.redraw = false // Draw animation only when not waiting for frame callback. redrawAnim := anim && w.lastFrameCallback == nil if !redrawAnim && !sync { @@ -1716,7 +1775,7 @@ func (w *window) draw() { // Use the surface as listener data for gio_onFrameDone. C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf)) } - w.w.Event(frameEvent{ + w.ProcessEvent(frameEvent{ FrameEvent: FrameEvent{ Now: time.Now(), Size: w.config.Size, @@ -1731,7 +1790,7 @@ func (w *window) setStage(s Stage) { return } w.stage = s - w.w.Event(StageEvent{Stage: s}) + w.ProcessEvent(StageEvent{Stage: s}) } func (w *window) display() *C.struct_wl_display { @@ -1825,6 +1884,10 @@ func newWLDisplay() (*wlDisplay, error) { } func (d *wlDisplay) destroy() { + if d.readClipClose != nil { + close(d.readClipClose) + d.readClipClose = nil + } if d.notify.write != 0 { syscall.Close(d.notify.write) d.notify.write = 0 @@ -1866,6 +1929,7 @@ func (d *wlDisplay) destroy() { if d.disp != nil { C.wl_display_disconnect(d.disp) callbackDelete(unsafe.Pointer(d.disp)) + d.disp = nil } } diff --git a/app/os_windows.go b/app/os_windows.go index 017c80c5..680b3e8f 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -19,10 +19,12 @@ import ( syscall "golang.org/x/sys/windows" "gioui.org/app/internal/windows" + "gioui.org/op" "gioui.org/unit" gowindows "golang.org/x/sys/windows" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" @@ -53,6 +55,10 @@ type window struct { borderSize image.Point config Config + loop *eventLoop + + // invMu avoids the race between destroying the window and Invalidate. + invMu sync.Mutex } const _WM_WAKEUP = windows.WM_USER + iota @@ -85,36 +91,38 @@ func osMain() { select {} } -func newWindow(window *callbacks, options []Option) error { - cerr := make(chan error) +func newWindow(win *callbacks, options []Option) { + done := make(chan struct{}) go func() { // GetMessage and PeekMessage can filter on a window HWND, but // then thread-specific messages such as WM_QUIT are ignored. // Instead lock the thread so window messages arrive through // unfiltered GetMessage calls. runtime.LockOSThread() - w, err := createNativeWindow() + + w := &window{ + w: win, + } + w.loop = newEventLoop(w.w, w.wakeup) + w.w.SetDriver(w) + err := w.init() + done <- struct{}{} if err != nil { - cerr <- err + w.ProcessEvent(DestroyEvent{Err: err}) return } - cerr <- nil winMap.Store(w.hwnd, w) defer winMap.Delete(w.hwnd) - w.w = window - w.w.SetDriver(w) - w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)}) + w.ProcessEvent(ViewEvent{HWND: uintptr(w.hwnd)}) w.Configure(options) windows.SetForegroundWindow(w.hwnd) windows.SetFocus(w.hwnd) // Since the window class for the cursor is null, // set it here to show the cursor. w.SetCursor(pointer.CursorDefault) - if err := w.loop(); err != nil { - panic(err) - } + w.runLoop() }() - return <-cerr + <-done } // initResources initializes the resources global. @@ -149,13 +157,13 @@ func initResources() error { const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE -func createNativeWindow() (*window, error) { +func (w *window) init() error { var resErr error resources.once.Do(func() { resErr = initResources() }) if resErr != nil { - return nil, resErr + return resErr } const dwStyle = windows.WS_OVERLAPPEDWINDOW @@ -171,16 +179,15 @@ func createNativeWindow() (*window, error) { resources.handle, 0) if err != nil { - return nil, err - } - w := &window{ - hwnd: hwnd, + return err } w.hdc, err = windows.GetDC(hwnd) if err != nil { - return nil, err + windows.DestroyWindow(hwnd) + return err } - return w, nil + w.hwnd = hwnd + return nil } // update() handles changes done by the user, and updates the configuration. @@ -197,7 +204,7 @@ func (w *window) update() { windows.GetSystemMetrics(windows.SM_CXSIZEFRAME), windows.GetSystemMetrics(windows.SM_CYSIZEFRAME), ) - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr { @@ -238,7 +245,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr e.State = key.Release } - w.w.Event(e) + w.ProcessEvent(e) if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) { // Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs @@ -259,15 +266,15 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr case windows.WM_MBUTTONUP: w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers()) case windows.WM_CANCELMODE: - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Cancel, }) case windows.WM_SETFOCUS: w.focused = true - w.w.Event(key.FocusEvent{Focus: true}) + w.ProcessEvent(key.FocusEvent{Focus: true}) case windows.WM_KILLFOCUS: w.focused = false - w.w.Event(key.FocusEvent{Focus: false}) + w.ProcessEvent(key.FocusEvent{Focus: false}) case windows.WM_NCACTIVATE: if w.stage >= StageInactive { if wParam == windows.TRUE { @@ -288,7 +295,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr case windows.WM_MOUSEMOVE: x, y := coordsFromlParam(lParam) p := f32.Point{X: float32(x), Y: float32(y)} - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Move, Source: pointer.Mouse, Position: p, @@ -301,14 +308,16 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr case windows.WM_MOUSEHWHEEL: w.scrollEvent(wParam, lParam, true, getModifiers()) case windows.WM_DESTROY: - w.w.Event(ViewEvent{}) - w.w.Event(DestroyEvent{}) + w.ProcessEvent(ViewEvent{}) + w.ProcessEvent(DestroyEvent{}) if w.hdc != 0 { windows.ReleaseDC(w.hdc) w.hdc = 0 } + w.invMu.Lock() // The system destroys the HWND for us. w.hwnd = 0 + w.invMu.Unlock() windows.PostQuitMessage(0) case windows.WM_NCCALCSIZE: if w.config.Decorated { @@ -328,7 +337,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr // Adjust window position to avoid the extra padding in maximized // state. See https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543. // Note that trying to do the adjustment in WM_GETMINMAXINFO is ignored by Windows. - szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(uintptr(lParam))) + szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(lParam)) mi := windows.GetMonitorInfo(w.hwnd) szp.Rgrc[0] = mi.WorkArea return 0 @@ -350,7 +359,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr w.setStage(StageRunning) } case windows.WM_GETMINMAXINFO: - mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam))) + mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam)) var bw, bh int32 if w.config.Decorated { r := windows.GetWindowRect(w.hwnd) @@ -378,7 +387,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr return windows.TRUE } case _WM_WAKEUP: - w.w.Event(wakeupEvent{}) + w.loop.Wakeup() + w.loop.FlushEvents() case windows.WM_IME_STARTCOMPOSITION: imc := windows.ImmGetContext(w.hwnd) if imc == 0 { @@ -518,7 +528,7 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, } x, y := coordsFromlParam(lParam) p := f32.Point{X: float32(x), Y: float32(y)} - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: kind, Source: pointer.Mouse, Position: p, @@ -553,7 +563,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key. sp.Y = -dist } } - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Scroll, Source: pointer.Mouse, Position: p, @@ -565,7 +575,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key. } // Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/ -func (w *window) loop() error { +func (w *window) runLoop() { msg := new(windows.Msg) loop: for { @@ -576,7 +586,7 @@ loop: } switch ret := windows.GetMessage(msg, 0, 0, 0); ret { case -1: - return errors.New("GetMessage failed") + panic(errors.New("GetMessage failed")) case 0: // WM_QUIT received. break loop @@ -584,7 +594,6 @@ loop: windows.TranslateMessage(msg) windows.DispatchMessage(msg) } - return nil } func (w *window) EditorStateChanged(old, new editorState) { @@ -602,7 +611,35 @@ func (w *window) SetAnimating(anim bool) { w.animating = anim } -func (w *window) Wakeup() { +func (w *window) ProcessEvent(e event.Event) { + w.w.ProcessEvent(e) + w.loop.FlushEvents() +} + +func (w *window) Event() event.Event { + return w.loop.Event() +} + +func (w *window) Invalidate() { + w.loop.Invalidate() +} + +func (w *window) Run(f func()) { + w.loop.Run(f) +} + +func (w *window) Frame(frame *op.Ops) { + w.loop.Frame(frame) +} + +func (w *window) wakeup() { + w.invMu.Lock() + defer w.invMu.Unlock() + if w.hwnd == 0 { + w.loop.Wakeup() + w.loop.FlushEvents() + return + } if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil { panic(err) } @@ -611,7 +648,7 @@ func (w *window) Wakeup() { func (w *window) setStage(s Stage) { if s != w.stage { w.stage = s - w.w.Event(StageEvent{Stage: s}) + w.ProcessEvent(StageEvent{Stage: s}) } } @@ -621,7 +658,7 @@ func (w *window) draw(sync bool) { } dpi := windows.GetWindowDPI(w.hwnd) cfg := configForDPI(dpi) - w.w.Event(frameEvent{ + w.ProcessEvent(frameEvent{ FrameEvent: FrameEvent{ Now: time.Now(), Size: w.config.Size, @@ -668,7 +705,7 @@ func (w *window) readClipboard() error { } defer windows.GlobalUnlock(mem) content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) - w.w.Event(transfer.DataEvent{ + w.ProcessEvent(transfer.DataEvent{ Type: "application/text", Open: func() io.ReadCloser { return io.NopCloser(strings.NewReader(content)) diff --git a/app/os_x11.go b/app/os_x11.go index 2c36c0c9..0745e488 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -38,10 +38,12 @@ import ( "unsafe" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/io/transfer" + "gioui.org/op" "gioui.org/unit" syscall "golang.org/x/sys/unix" @@ -98,7 +100,6 @@ type x11Window struct { notify struct { read, write int } - dead bool animating bool @@ -111,6 +112,11 @@ type x11Window struct { config Config wakeups chan struct{} + handler x11EventHandler + buf [100]byte + + // invMy avoids the race between destroy and Invalidate. + invMu sync.Mutex } var ( @@ -234,7 +240,7 @@ func (w *x11Window) Configure(options []Option) { if cnf.Decorated != prev.Decorated { w.config.Decorated = cnf.Decorated } - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } func (w *x11Window) setTitle(prev, cnf Config) { @@ -377,11 +383,47 @@ func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) { var x11OneByte = make([]byte, 1) -func (w *x11Window) Wakeup() { +func (w *x11Window) ProcessEvent(e event.Event) { + w.w.ProcessEvent(e) +} + +func (w *x11Window) shutdown(err error) { + w.ProcessEvent(X11ViewEvent{}) + w.ProcessEvent(DestroyEvent{Err: err}) +} + +func (w *x11Window) Event() event.Event { + for { + evt, ok := w.w.nextEvent() + if !ok { + w.dispatch() + continue + } + if _, destroy := evt.(DestroyEvent); destroy { + w.destroy() + } + return evt + } +} + +func (w *x11Window) Run(f func()) { + f() +} + +func (w *x11Window) Frame(frame *op.Ops) { + w.w.ProcessFrame(frame, nil) +} + +func (w *x11Window) Invalidate() { select { case w.wakeups <- struct{}{}: default: } + w.invMu.Lock() + defer w.invMu.Unlock() + if w.x == nil { + return + } if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN { panic(fmt.Errorf("failed to write to pipe: %v", err)) } @@ -400,11 +442,23 @@ func (w *x11Window) setStage(s Stage) { return } w.stage = s - w.w.Event(StageEvent{Stage: s}) + w.ProcessEvent(StageEvent{Stage: s}) } -func (w *x11Window) loop() { - h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)} +func (w *x11Window) dispatch() { + if w.x == nil { + // Only Invalidate can wake us up. + <-w.wakeups + w.w.Invalidate() + return + } + + select { + case <-w.wakeups: + w.w.Invalidate() + default: + } + xfd := C.XConnectionNumber(w.x) // Poll for events and notifications. @@ -414,64 +468,52 @@ func (w *x11Window) loop() { } xEvents := &pollfds[0].Revents // Plenty of room for a backlog of notifications. - buf := make([]byte, 100) -loop: - for !w.dead { - var syn, anim bool - // Check for pending draw events before checking animation or blocking. - // 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 { - anim = w.animating - if !anim { - // Clear poll events. - *xEvents = 0 - // Wait for X event or gio notification. - if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { - panic(fmt.Errorf("x11 loop: poll failed: %w", err)) - } - switch { - case *xEvents&syscall.POLLIN != 0: - syn = h.handleEvents() - if w.dead { - break loop - } - case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: - break loop - } + var syn, anim bool + // Check for pending draw events before checking animation or blocking. + // 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 = w.handler.handleEvents(); !syn { + anim = w.animating + if !anim { + // Clear poll events. + *xEvents = 0 + // Wait for X event or gio notification. + if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { + panic(fmt.Errorf("x11 loop: poll failed: %w", err)) + } + switch { + case *xEvents&syscall.POLLIN != 0: + syn = w.handler.handleEvents() + case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: } } - // Clear notifications. - for { - _, err := syscall.Read(w.notify.read, buf) - if err == syscall.EAGAIN { - break - } - if err != nil { - panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err)) - } + } + // Clear notifications. + for { + _, err := syscall.Read(w.notify.read, w.buf[:]) + if err == syscall.EAGAIN { + break } - select { - case <-w.wakeups: - w.w.Event(wakeupEvent{}) - default: - } - - if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 { - w.w.Event(frameEvent{ - FrameEvent: FrameEvent{ - Now: time.Now(), - Size: w.config.Size, - Metric: w.metric, - }, - Sync: syn, - }) + if err != nil { + panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err)) } } + if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 { + w.ProcessEvent(frameEvent{ + FrameEvent: FrameEvent{ + Now: time.Now(), + Size: w.config.Size, + Metric: w.metric, + }, + Sync: syn, + }) + } } func (w *x11Window) destroy() { + w.invMu.Lock() + defer w.invMu.Unlock() if w.notify.write != 0 { syscall.Close(w.notify.write) w.notify.write = 0 @@ -486,6 +528,7 @@ func (w *x11Window) destroy() { } C.XDestroyWindow(w.x, w.xw) C.XCloseDisplay(w.x) + w.x = nil } // atom is a wrapper around XInternAtom. Callers should cache the result @@ -543,7 +586,7 @@ func (h *x11EventHandler) handleEvents() bool { // There's no support for IME yet. w.w.EditorInsert(ee.Text) } else { - w.w.Event(e) + w.ProcessEvent(e) } } case C.ButtonPress, C.ButtonRelease: @@ -605,10 +648,10 @@ func (h *x11EventHandler) handleEvents() bool { w.pointerBtns &^= btn } ev.Buttons = w.pointerBtns - w.w.Event(ev) + w.ProcessEvent(ev) case C.MotionNotify: mevt := (*C.XMotionEvent)(unsafe.Pointer(xev)) - w.w.Event(pointer.Event{ + w.ProcessEvent(pointer.Event{ Kind: pointer.Move, Source: pointer.Mouse, Buttons: w.pointerBtns, @@ -623,14 +666,14 @@ func (h *x11EventHandler) handleEvents() bool { // redraw only on the last expose event redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0 case C.FocusIn: - w.w.Event(key.FocusEvent{Focus: true}) + w.ProcessEvent(key.FocusEvent{Focus: true}) case C.FocusOut: - w.w.Event(key.FocusEvent{Focus: false}) + w.ProcessEvent(key.FocusEvent{Focus: false}) case C.ConfigureNotify: // window configuration change cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size { w.config.Size = sz - w.w.Event(ConfigEvent{Config: w.config}) + w.ProcessEvent(ConfigEvent{Config: w.config}) } // redraw will be done by a later expose event case C.SelectionNotify: @@ -652,7 +695,7 @@ func (h *x11EventHandler) handleEvents() bool { break } str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems)) - w.w.Event(transfer.DataEvent{ + w.ProcessEvent(transfer.DataEvent{ Type: "application/text", Open: func() io.ReadCloser { return io.NopCloser(strings.NewReader(str)) @@ -711,7 +754,7 @@ func (h *x11EventHandler) handleEvents() bool { cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev)) switch *(*C.long)(unsafe.Pointer(&cevt.data)) { case C.long(w.atoms.evDelWindow): - w.dead = true + w.shutdown(nil) return false } } @@ -793,8 +836,10 @@ func newX11Window(gioWin *callbacks, options []Option) error { wakeups: make(chan struct{}, 1), config: Config{Size: cnf.Size}, } + w.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)} w.notify.read = pipe[0] w.notify.write = pipe[1] + w.w.SetDriver(w) if err := w.updateXkbKeymap(); err != nil { w.destroy() @@ -830,19 +875,11 @@ func newX11Window(gioWin *callbacks, options []Option) error { // extensions C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1) - go func() { - w.w.SetDriver(w) - - // make the window visible on the screen - C.XMapWindow(dpy, win) - w.Configure(options) - w.w.Event(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)}) - w.setStage(StageRunning) - w.loop() - w.w.Event(X11ViewEvent{}) - w.w.Event(DestroyEvent{Err: nil}) - w.destroy() - }() + // make the window visible on the screen + C.XMapWindow(dpy, win) + w.Configure(options) + w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)}) + w.setStage(StageRunning) return nil } diff --git a/app/window.go b/app/window.go index a37620e1..5b392545 100644 --- a/app/window.go +++ b/app/window.go @@ -8,6 +8,7 @@ import ( "image" "image/color" "runtime" + "sync" "time" "unicode/utf8" @@ -38,32 +39,13 @@ type Option func(unit.Metric, *Config) type Window struct { ctx context gpu gpu.GPU - - // driverFuncs is a channel of functions to run when - // the Window has a valid driver. - driverFuncs chan func(d driver) - // wakeups wakes up the native event loop to send a - // WakeupEvent that flushes driverFuncs. - wakeups chan struct{} - // wakeupFuncs is sent wakeup functions when the driver changes. - wakeupFuncs chan func() - // redraws is notified when a redraw is requested by the client. - redraws chan struct{} - // immediateRedraws is like redraw but doesn't need a wakeup. - immediateRedraws chan struct{} - // scheduledRedraws is sent the most recent delayed redraw time. - scheduledRedraws chan time.Time - // options are the options waiting to be applied. - options chan []Option - // actions are the actions waiting to be performed. - actions chan system.Action - - // out is where the platform backend delivers events bound for the - // user program. - out chan event.Event - frames chan *op.Ops - frameAck chan struct{} - destroy chan struct{} + // timer tracks the delayed invalidate goroutine. + timer struct { + // quit is shuts down the goroutine. + quit chan struct{} + // update the invalidate time. + update chan time.Time + } stage Stage animating bool @@ -72,8 +54,7 @@ type Window struct { // viewport is the latest frame size with insets applied. viewport image.Rectangle // metric is the metric from the most recent frame. - metric unit.Metric - + metric unit.Metric queue input.Router cursor pointer.Cursor decorations struct { @@ -89,11 +70,8 @@ type Window struct { *material.Theme *widget.Decorations } - callbacks callbacks - nocontext bool - // semantic data, lazily evaluated if requested by a backend to speed up // the cases where semantic data is not needed. semantic struct { @@ -104,25 +82,35 @@ type Window struct { tree []input.SemanticNode ids map[input.SemanticID]input.SemanticNode } - imeState editorState - - // event stores the state required for processing and delivering events - // from NextEvent. If we had support for range over func, this would - // be the iterator state. - eventState struct { - created bool - initialOpts []Option - wakeup func() - timer *time.Timer + driver driver + // basic is the driver interface that is needed even after the window is gone. + basic basicDriver + once sync.Once + initialOpts []Option + // coalesced tracks the most recent events waiting to be delivered + // to the client. + coalesced eventSummary + // frame tracks the most recently frame event. + lastFrame struct { + sync bool + size image.Point + off image.Point + deco op.CallOp } } +type eventSummary struct { + wakeup bool + cfg *ConfigEvent + view *ViewEvent + frame *frameEvent + stage *StageEvent + destroy *DestroyEvent +} + type callbacks struct { - w *Window - d driver - busy bool - waitEvents []event.Event + w *Window } // NewWindow creates a new window for a set of window @@ -162,19 +150,7 @@ func NewWindow(options ...Option) *Window { cnf.apply(unit.Metric{}, options) w := &Window{ - out: make(chan event.Event), - immediateRedraws: make(chan struct{}), - redraws: make(chan struct{}, 1), - scheduledRedraws: make(chan time.Time, 1), - frames: make(chan *op.Ops), - frameAck: make(chan struct{}), - driverFuncs: make(chan func(d driver), 1), - wakeups: make(chan struct{}, 1), - wakeupFuncs: make(chan func()), - destroy: make(chan struct{}), - options: make(chan []Option, 1), - actions: make(chan system.Action, 1), - nocontext: cnf.CustomRenderer, + nocontext: cnf.CustomRenderer, } w.decorations.Theme = theme w.decorations.Decorations = deco @@ -183,7 +159,7 @@ func NewWindow(options ...Option) *Window { w.imeState.compose = key.Range{Start: -1, End: -1} w.semantic.ids = make(map[input.SemanticID]input.SemanticNode) w.callbacks.w = w - w.eventState.initialOpts = options + w.initialOpts = options return w } @@ -193,15 +169,7 @@ func decoHeightOpt(h unit.Dp) Option { } } -// update the window contents, input operations declare input handlers, -// and so on. The supplied operations list completely replaces the window state -// from previous calls. -func (w *Window) update(frame *op.Ops) { - w.frames <- frame - <-w.frameAck -} - -func (w *Window) validateAndProcess(d driver, size image.Point, sync bool, frame *op.Ops, sigChan chan<- struct{}) error { +func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops, sigChan chan<- struct{}) error { signal := func() { if sigChan != nil { // We're done with frame, let the client continue. @@ -215,7 +183,7 @@ func (w *Window) validateAndProcess(d driver, size image.Point, sync bool, frame if w.gpu == nil && !w.nocontext { var err error if w.ctx == nil { - w.ctx, err = d.NewContext() + w.ctx, err = w.driver.NewContext() if err != nil { return err } @@ -294,7 +262,22 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error { return w.gpu.Frame(frame, target, viewport) } -func (w *Window) processFrame(d driver) { +func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) { + wrapper := &w.decorations.Ops + off := op.Offset(w.lastFrame.off).Push(wrapper) + ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal)) + off.Pop() + w.lastFrame.deco.Add(wrapper) + if err := w.validateAndProcess(w.lastFrame.size, w.lastFrame.sync, wrapper, ack); err != nil { + w.destroyGPU() + w.driver.ProcessEvent(DestroyEvent{Err: err}) + return + } + w.updateState() + w.updateCursor() +} + +func (w *Window) updateState() { for k := range w.semantic.ids { delete(w.semantic.ids, k) } @@ -302,34 +285,34 @@ func (w *Window) processFrame(d driver) { q := &w.queue switch q.TextInputState() { case input.TextInputOpen: - d.ShowTextInput(true) + w.driver.ShowTextInput(true) case input.TextInputClose: - d.ShowTextInput(false) + w.driver.ShowTextInput(false) } if hint, ok := q.TextInputHint(); ok { - d.SetInputHint(hint) + w.driver.SetInputHint(hint) } if mime, txt, ok := q.WriteClipboard(); ok { - d.WriteClipboard(mime, txt) + w.driver.WriteClipboard(mime, txt) } if q.ClipboardRequested() { - d.ReadClipboard() + w.driver.ReadClipboard() } oldState := w.imeState newState := oldState newState.EditorState = q.EditorState() if newState != oldState { w.imeState = newState - d.EditorStateChanged(oldState, newState) + w.driver.EditorStateChanged(oldState, newState) } if t, ok := q.WakeupTime(); ok { w.setNextFrame(t) } - w.updateAnimation(d) + w.updateAnimation() } // Invalidate the window such that a [FrameEvent] will be generated immediately. -// If the window is inactive, the event is sent when the window becomes active. +// If the window is inactive, an unspecified event is sent instead. // // Note that Invalidate is intended for externally triggered updates, such as a // response from a network request. The [op.InvalidateCmd] command is more efficient @@ -337,16 +320,8 @@ func (w *Window) processFrame(d driver) { // // Invalidate is safe for concurrent use. func (w *Window) Invalidate() { - select { - case w.immediateRedraws <- struct{}{}: - return - default: - } - select { - case w.redraws <- struct{}{}: - w.wakeup() - default: - } + w.init() + w.basic.Invalidate() } // Option applies the options to the window. @@ -354,15 +329,21 @@ func (w *Window) Option(opts ...Option) { if len(opts) == 0 { return } - for { - select { - case old := <-w.options: - opts = append(old, opts...) - case w.options <- opts: - w.wakeup() - return + w.Run(func() { + cnf := Config{Decorated: w.decorations.enabled} + for _, opt := range opts { + opt(w.metric, &cnf) } - } + w.decorations.enabled = cnf.Decorated + decoHeight := w.decorations.height + if !w.decorations.enabled { + decoHeight = 0 + } + opts = append(opts, decoHeightOpt(decoHeight)) + w.driver.Configure(opts) + w.setNextFrame(time.Time{}) + w.updateAnimation() + }) } // Run f in the same thread as the native window event loop, and wait for f to @@ -374,52 +355,64 @@ func (w *Window) Option(opts ...Option) { // Note that most programs should not call Run; configuring a Window with // [CustomRenderer] is a notable exception. func (w *Window) Run(f func()) { + w.init() + if w.driver == nil { + return + } done := make(chan struct{}) - w.driverDefer(func(d driver) { + w.driver.Run(func() { defer close(done) f() }) - select { - case <-done: - case <-w.destroy: - } + <-done } -// driverDefer is like Run but can be run from any context. It doesn't wait -// for f to return. -func (w *Window) driverDefer(f func(d driver)) { - select { - case w.driverFuncs <- f: - w.wakeup() - case <-w.destroy: +func (w *Window) updateAnimation() { + if w.driver == nil { + return } -} - -func (w *Window) updateAnimation(d driver) { animate := false if w.stage >= StageInactive && w.hasNextFrame { if dt := time.Until(w.nextFrame); dt <= 0 { animate = true } else { // Schedule redraw. - select { - case <-w.scheduledRedraws: - default: - } - w.scheduledRedraws <- w.nextFrame + w.scheduleInvalidate(w.nextFrame) } } if animate != w.animating { w.animating = animate - d.SetAnimating(animate) + w.driver.SetAnimating(animate) } } -func (w *Window) wakeup() { - select { - case w.wakeups <- struct{}{}: - default: +func (w *Window) scheduleInvalidate(t time.Time) { + if w.timer.quit == nil { + w.timer.quit = make(chan struct{}) + w.timer.update = make(chan time.Time) + go func() { + var timer *time.Timer + for { + var timeC <-chan time.Time + if timer != nil { + timeC = timer.C + } + select { + case <-w.timer.quit: + w.timer.quit <- struct{}{} + return + case t := <-w.timer.update: + if timer != nil { + timer.Stop() + } + timer = time.NewTimer(time.Until(t)) + case <-timeC: + w.Invalidate() + } + } + }() } + w.timer.update <- t } func (w *Window) setNextFrame(at time.Time) { @@ -429,61 +422,19 @@ func (w *Window) setNextFrame(at time.Time) { } } -func (c *callbacks) SetDriver(d driver) { - c.d = d - var wakeup func() - if d != nil { - wakeup = d.Wakeup +func (c *callbacks) SetDriver(d basicDriver) { + c.w.basic = d + if d, ok := d.(driver); ok { + c.w.driver = d } - c.w.wakeupFuncs <- wakeup } -func (c *callbacks) Event(e event.Event) bool { - if c.d == nil { - panic("event while no driver active") - } - c.waitEvents = append(c.waitEvents, e) - if c.busy { - return true - } - c.busy = true - var handled bool - for len(c.waitEvents) > 0 { - e := c.waitEvents[0] - copy(c.waitEvents, c.waitEvents[1:]) - c.waitEvents = c.waitEvents[:len(c.waitEvents)-1] - handled = c.w.processEvent(c.d, e) - } - c.busy = false - select { - case <-c.w.destroy: - return handled - default: - } - c.w.updateState(c.d) - if _, ok := e.(wakeupEvent); ok { - select { - case opts := <-c.w.options: - cnf := Config{Decorated: c.w.decorations.enabled} - for _, opt := range opts { - opt(c.w.metric, &cnf) - } - c.w.decorations.enabled = cnf.Decorated - decoHeight := c.w.decorations.height - if !c.w.decorations.enabled { - decoHeight = 0 - } - opts = append(opts, decoHeightOpt(decoHeight)) - c.d.Configure(opts) - default: - } - select { - case acts := <-c.w.actions: - c.d.Perform(acts) - default: - } - } - return handled +func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) { + c.w.processFrame(frame, ack) +} + +func (c *callbacks) ProcessEvent(e event.Event) bool { + return c.w.processEvent(e) } // SemanticRoot returns the ID of the semantic root. @@ -534,13 +485,13 @@ func (c *callbacks) EditorInsert(text string) { func (c *callbacks) EditorReplace(r key.Range, text string) { c.w.imeState.Replace(r, text) - c.Event(key.EditEvent{Range: r, Text: text}) - c.Event(key.SnippetEvent(c.w.imeState.Snippet.Range)) + c.w.driver.ProcessEvent(key.EditEvent{Range: r, Text: text}) + c.w.driver.ProcessEvent(key.SnippetEvent(c.w.imeState.Snippet.Range)) } func (c *callbacks) SetEditorSelection(r key.Range) { c.w.imeState.Selection.Range = r - c.Event(key.SelectionEvent(r)) + c.w.driver.ProcessEvent(key.SelectionEvent(r)) } func (c *callbacks) SetEditorSnippet(r key.Range) { @@ -548,7 +499,7 @@ func (c *callbacks) SetEditorSnippet(r key.Range) { // No need to expand. return } - c.Event(key.SnippetEvent(r)) + c.w.driver.ProcessEvent(key.SnippetEvent(r)) } func (w *Window) moveFocus(dir key.FocusDirection) { @@ -578,29 +529,13 @@ func (w *Window) moveFocus(dir key.FocusDirection) { func (c *callbacks) ClickFocus() { c.w.queue.ClickFocus() c.w.setNextFrame(time.Time{}) - c.w.updateAnimation(c.d) + c.w.updateAnimation() } func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) { return c.w.queue.ActionAt(p) } -func (w *Window) waitAck(d driver) { - for { - select { - case f := <-w.driverFuncs: - f(d) - case w.out <- theFlushEvent: - // A dummy event went through, so we know the application has processed the previous event. - return - case <-w.immediateRedraws: - // Invalidate was called during frame processing. - w.setNextFrame(time.Time{}) - w.updateAnimation(d) - } - } -} - func (w *Window) destroyGPU() { if w.gpu != nil { w.ctx.Lock() @@ -614,27 +549,6 @@ func (w *Window) destroyGPU() { } } -// waitFrame waits for the client to either call [FrameEvent.Frame] -// or to continue event handling. -func (w *Window) waitFrame(d driver) *op.Ops { - for { - select { - case f := <-w.driverFuncs: - f(d) - case frame := <-w.frames: - // The client called FrameEvent.Frame. - return frame - case w.out <- theFlushEvent: - // The client ignored FrameEvent and continued processing - // events. - return nil - case <-w.immediateRedraws: - // Invalidate was called during frame processing. - w.setNextFrame(time.Time{}) - } - } -} - // updateSemantics refreshes the semantics tree, the id to node map and the ids of // updated nodes. func (w *Window) updateSemantics() { @@ -671,26 +585,46 @@ func (w *Window) collectSemanticDiffs(diffs *[]input.SemanticID, n input.Semanti } } -func (w *Window) updateState(d driver) { - for { - select { - case f := <-w.driverFuncs: - f(d) - case <-w.redraws: - w.setNextFrame(time.Time{}) - w.updateAnimation(d) - default: - return - } - } +func (c *callbacks) Invalidate() { + c.w.setNextFrame(time.Time{}) + c.w.updateAnimation() + // Guarantee a wakeup, even when not animating. + c.w.processEvent(wakeupEvent{}) } -func (w *Window) processEvent(d driver, e event.Event) bool { - select { - case <-w.destroy: - return false - default: +func (c *callbacks) nextEvent() (event.Event, bool) { + s := &c.w.coalesced + // Every event counts as a wakeup. + defer func() { s.wakeup = false }() + switch { + case s.view != nil: + e := *s.view + s.view = nil + return e, true + case s.destroy != nil: + e := *s.destroy + // Clear pending events after DestroyEvent is delivered. + *s = eventSummary{} + return e, true + case s.cfg != nil: + e := *s.cfg + s.cfg = nil + return e, true + case s.stage != nil: + e := *s.stage + s.stage = nil + return e, true + case s.frame != nil: + e := *s.frame + s.frame = nil + return e.FrameEvent, true + case s.wakeup: + return wakeupEvent{}, true } + return nil, false +} + +func (w *Window) processEvent(e event.Event) bool { switch e2 := e.(type) { case StageEvent: if e2.Stage < StageInactive { @@ -702,9 +636,10 @@ func (w *Window) processEvent(d driver, e event.Event) bool { } } w.stage = e2.Stage - w.updateAnimation(d) - w.out <- e - w.waitAck(d) + w.updateAnimation() + w.coalesced.stage = &e2 + case wakeupEvent: + w.coalesced.wakeup = true case frameEvent: if e2.Size == (image.Point{}) { panic(errors.New("internal error: zero-sized Draw")) @@ -715,12 +650,9 @@ func (w *Window) processEvent(d driver, e event.Event) bool { } w.metric = e2.Metric w.hasNextFrame = false - e2.Frame = w.update + e2.Frame = w.driver.Frame e2.Source = w.queue.Source() - // Prepare the decorations and update the frame insets. - wrapper := &w.decorations.Ops - wrapper.Reset() viewport := image.Rectangle{ Min: image.Point{ X: e2.Metric.Dp(e2.Insets.Left), @@ -736,41 +668,30 @@ func (w *Window) processEvent(d driver, e event.Event) bool { w.queue.RevealFocus(viewport) } w.viewport = viewport - viewSize := e2.Size + wrapper := &w.decorations.Ops + wrapper.Reset() m := op.Record(wrapper) - size, offset := w.decorate(d, e2.FrameEvent, wrapper) - e2.FrameEvent.Size = size - deco := m.Stop() - w.out <- e2.FrameEvent - frame := w.waitFrame(d) - var signal chan<- struct{} - if frame != nil { - signal = w.frameAck - off := op.Offset(offset).Push(wrapper) - ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal)) - off.Pop() - } - deco.Add(wrapper) - if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil { - w.destroyGPU() - w.out <- DestroyEvent{Err: err} - close(w.destroy) - break - } - w.processFrame(d) - w.updateCursor(d) + offset := w.decorate(e2.FrameEvent, wrapper) + w.lastFrame.deco = m.Stop() + w.lastFrame.size = e2.Size + w.lastFrame.sync = e2.Sync + w.lastFrame.off = offset + e2.Size = e2.Size.Sub(offset) + w.coalesced.frame = &e2 case DestroyEvent: w.destroyGPU() - w.out <- e2 - close(w.destroy) + w.driver = nil + if q := w.timer.quit; q != nil { + q <- struct{}{} + <-q + } + w.coalesced.destroy = &e2 case ViewEvent: - w.out <- e2 - w.waitAck(d) + w.coalesced.view = &e2 case ConfigEvent: w.decorations.Config = e2.Config e2.Config = w.effectiveConfig() - w.out <- e2 - case wakeupEvent: + w.coalesced.cfg = &e2 case event.Event: focusDir := key.FocusDirection(-1) if e, ok := e2.(key.Event); ok && e.State == key.Press { @@ -800,10 +721,10 @@ func (w *Window) processEvent(d driver, e event.Event) bool { w.moveFocus(focusDir) t, handled = w.queue.WakeupTime() } - w.updateCursor(d) + w.updateCursor() if handled { w.setNextFrame(t) - w.updateAnimation(d) + w.updateAnimation() } return handled } @@ -811,58 +732,22 @@ func (w *Window) processEvent(d driver, e event.Event) bool { } // NextEvent blocks until an event is received from the window, such as -// [FrameEvent]. It blocks forever if called after [DestroyEvent] -// has been returned. +// [FrameEvent], or until [Invalidate] is called. func (w *Window) NextEvent() event.Event { - state := &w.eventState - if !state.created { - state.created = true - if err := newWindow(&w.callbacks, state.initialOpts); err != nil { - close(w.destroy) - return DestroyEvent{Err: err} - } - } - for { - var ( - wakeups <-chan struct{} - timeC <-chan time.Time - ) - if state.wakeup != nil { - wakeups = w.wakeups - if state.timer != nil { - timeC = state.timer.C - } - } - select { - case t := <-w.scheduledRedraws: - if state.timer != nil { - state.timer.Stop() - } - state.timer = time.NewTimer(time.Until(t)) - case e := <-w.out: - // Receiving a flushEvent indicates to the platform backend that - // all previous events have been processed by the user program. - if _, ok := e.(flushEvent); ok { - break - } - return e - case <-timeC: - select { - case w.redraws <- struct{}{}: - state.wakeup() - default: - } - case <-wakeups: - state.wakeup() - case state.wakeup = <-w.wakeupFuncs: - } - } + w.init() + return w.basic.Event() } -func (w *Window) updateCursor(d driver) { +func (w *Window) init() { + w.once.Do(func() { + newWindow(&w.callbacks, w.initialOpts) + }) +} + +func (w *Window) updateCursor() { if c := w.queue.Cursor(); c != w.cursor { w.cursor = c - d.SetCursor(c) + w.driver.SetCursor(c) } } @@ -872,9 +757,9 @@ func (w *Window) fallbackDecorate() bool { } // decorate the window if enabled and returns the corresponding Insets. -func (w *Window) decorate(d driver, e FrameEvent, o *op.Ops) (size, offset image.Point) { +func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point { if !w.fallbackDecorate() { - return e.Size, image.Pt(0, 0) + return image.Pt(0, 0) } deco := w.decorations.Decorations allActions := system.ActionMinimize | system.ActionMaximize | system.ActionUnmaximize | @@ -903,16 +788,17 @@ func (w *Window) decorate(d driver, e FrameEvent, o *op.Ops) (size, offset image Constraints: layout.Exact(e.Size), } // Update the window based on the actions on the decorations. - w.Perform(deco.Update(gtx)) + opts, acts := splitActions(deco.Update(gtx)) + w.driver.Configure(opts) + w.driver.Perform(acts) style.Layout(gtx) // Offset to place the frame content below the decorations. decoHeight := gtx.Dp(w.decorations.Config.decoHeight) if w.decorations.currentHeight != decoHeight { w.decorations.currentHeight = decoHeight - w.out <- ConfigEvent{Config: w.effectiveConfig()} + w.coalesced.cfg = &ConfigEvent{Config: w.effectiveConfig()} } - e.Size.Y -= w.decorations.currentHeight - return e.Size, image.Pt(0, decoHeight) + return image.Pt(0, decoHeight) } func (w *Window) effectiveConfig() Config { @@ -922,33 +808,38 @@ func (w *Window) effectiveConfig() Config { return cnf } -// Perform the actions on the window. -func (w *Window) Perform(actions system.Action) { +// splitActions splits options from actions and return them and the remaining +// actions. +func splitActions(actions system.Action) ([]Option, system.Action) { + var opts []Option walkActions(actions, func(action system.Action) { switch action { case system.ActionMinimize: - w.Option(Minimized.Option()) + opts = append(opts, Minimized.Option()) case system.ActionMaximize: - w.Option(Maximized.Option()) + opts = append(opts, Maximized.Option()) case system.ActionUnmaximize: - w.Option(Windowed.Option()) + opts = append(opts, Windowed.Option()) + case system.ActionFullscreen: + opts = append(opts, Fullscreen.Option()) default: return } actions &^= action }) - if actions == 0 { + return opts, actions +} + +// Perform the actions on the window. +func (w *Window) Perform(actions system.Action) { + opts, acts := splitActions(actions) + w.Option(opts...) + if acts == 0 { return } - for { - select { - case old := <-w.actions: - actions |= old - case w.actions <- actions: - w.wakeup() - return - } - } + w.Run(func() { + w.driver.Perform(actions) + }) } // Title sets the title of the window.