diff --git a/app/doc.go b/app/doc.go index abfba20c..51e872b4 100644 --- a/app/doc.go +++ b/app/doc.go @@ -12,16 +12,16 @@ Create a new Window by calling NewWindow. On mobile platforms or when Gio is embedded in another project, NewWindow merely connects with a previously created window. -A Window is run by receiving events from its Events channel. The most -important event is FrameEvent that prompts an update of the window -contents and state. +A Window is run by calling NextEvent in a loop. The most important event is +FrameEvent that prompts an update of the window contents. For example: import "gioui.org/unit" w := app.NewWindow() - for e := range w.Events() { + for { + e := w.NextEvent() if e, ok := e.(system.FrameEvent); ok { ops.Reset() // Add operations to ops. @@ -50,7 +50,8 @@ For example, to display a blank but otherwise functional window: func main() { go func() { w := app.NewWindow() - for range w.Events() { + for { + w.NextEvent() } }() app.Main() diff --git a/app/window.go b/app/window.go index 20f92c48..b35985d6 100644 --- a/app/window.go +++ b/app/window.go @@ -61,6 +61,8 @@ type Window struct { // 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{} @@ -107,6 +109,16 @@ type Window struct { } 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 + } } type editorState struct { @@ -185,7 +197,7 @@ func NewWindow(options ...Option) *Window { w.imeState.compose = key.Range{Start: -1, End: -1} w.semantic.ids = make(map[router.SemanticID]router.SemanticNode) w.callbacks.w = w - go w.run(options) + w.eventState.initialOpts = options return w } @@ -195,11 +207,6 @@ func decoHeightOpt(h unit.Dp) Option { } } -// Events returns the channel where events are delivered. -func (w *Window) Events() <-chan event.Event { - return w.out -} - // update the window contents, input operations declare input handlers, // and so on. The supplied operations list completely replaces the window state // from previous calls. @@ -713,7 +720,7 @@ func (w *Window) waitAck(d driver) { select { case f := <-w.driverFuncs: f(d) - case w.out <- event.Event(nil): + case w.out <- theFlushEvent: // A dummy event went through, so we know the application has processed the previous event. return case <-w.immediateRedraws: @@ -747,7 +754,7 @@ func (w *Window) waitFrame(d driver) *op.Ops { case frame := <-w.frames: // The client called FrameEvent.Frame. return frame - case w.out <- event.Event(nil): + case w.out <- theFlushEvent: // The client ignored FrameEvent and continued processing // events. return nil @@ -881,7 +888,6 @@ func (w *Window) processEvent(d driver, e event.Event) bool { if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil { w.destroyGPU() w.out <- system.DestroyEvent{Err: err} - close(w.out) close(w.destroy) break } @@ -890,7 +896,6 @@ func (w *Window) processEvent(d driver, e event.Event) bool { case system.DestroyEvent: w.destroyGPU() w.out <- e2 - close(w.out) close(w.destroy) case ViewEvent: w.out <- e2 @@ -938,43 +943,51 @@ func (w *Window) processEvent(d driver, e event.Event) bool { return true } -func (w *Window) run(options []Option) { - if err := newWindow(&w.callbacks, options); err != nil { - w.out <- system.DestroyEvent{Err: err} - close(w.out) - close(w.destroy) - return +// NextEvent blocks until an event is received from the window, such as +// [io/system.FrameEvent]. It blocks forever if called after [io/system.DestroyEvent] +// has been returned. +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 system.DestroyEvent{Err: err} + } } - var wakeup func() - var timer *time.Timer for { var ( wakeups <-chan struct{} timeC <-chan time.Time ) - if wakeup != nil { + if state.wakeup != nil { wakeups = w.wakeups - if timer != nil { - timeC = timer.C + if state.timer != nil { + timeC = state.timer.C } } select { case t := <-w.scheduledRedraws: - if timer != nil { - timer.Stop() + if state.timer != nil { + state.timer.Stop() } - timer = time.NewTimer(time.Until(t)) - case <-w.destroy: - return + 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{}{}: - wakeup() + state.wakeup() default: } case <-wakeups: - wakeup() - case wakeup = <-w.wakeupFuncs: + state.wakeup() + case state.wakeup = <-w.wakeupFuncs: } } } @@ -1165,3 +1178,14 @@ func Decorated(enabled bool) Option { cnf.Decorated = enabled } } + +// flushEvent is sent to detect when the user program +// has completed processing of all prior events. Its an +// [io/event.Event] but only for internal use. +type flushEvent struct{} + +func (t flushEvent) ImplementsEvent() {} + +// theFlushEvent avoids allocating garbage when sending +// flushEvents. +var theFlushEvent flushEvent