app: [API] replace events channel with an iterator interface

The goroutine started by Window.run runs concurrently with the user
goroutine receiving from Window.Events, leading to races such as #543.
This change replaces the Window.run goroutine and the Window.Events
channel with an iterator API driven by the user goroutine directly.

Fixes: https://todo.sr.ht/~eliasnaur/gio/543
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-10-12 16:01:07 -05:00
parent 7550d85447
commit 37717d0df9
2 changed files with 59 additions and 34 deletions
+6 -5
View File
@@ -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 is embedded in another project, NewWindow merely connects with a previously
created window. created window.
A Window is run by receiving events from its Events channel. The most A Window is run by calling NextEvent in a loop. The most important event is
important event is FrameEvent that prompts an update of the window FrameEvent that prompts an update of the window contents.
contents and state.
For example: For example:
import "gioui.org/unit" import "gioui.org/unit"
w := app.NewWindow() w := app.NewWindow()
for e := range w.Events() { for {
e := w.NextEvent()
if e, ok := e.(system.FrameEvent); ok { if e, ok := e.(system.FrameEvent); ok {
ops.Reset() ops.Reset()
// Add operations to ops. // Add operations to ops.
@@ -50,7 +50,8 @@ For example, to display a blank but otherwise functional window:
func main() { func main() {
go func() { go func() {
w := app.NewWindow() w := app.NewWindow()
for range w.Events() { for {
w.NextEvent()
} }
}() }()
app.Main() app.Main()
+53 -29
View File
@@ -61,6 +61,8 @@ type Window struct {
// actions are the actions waiting to be performed. // actions are the actions waiting to be performed.
actions chan system.Action actions chan system.Action
// out is where the platform backend delivers events bound for the
// user program.
out chan event.Event out chan event.Event
frames chan *op.Ops frames chan *op.Ops
frameAck chan struct{} frameAck chan struct{}
@@ -107,6 +109,16 @@ type Window struct {
} }
imeState editorState 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 { type editorState struct {
@@ -185,7 +197,7 @@ func NewWindow(options ...Option) *Window {
w.imeState.compose = key.Range{Start: -1, End: -1} w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[router.SemanticID]router.SemanticNode) w.semantic.ids = make(map[router.SemanticID]router.SemanticNode)
w.callbacks.w = w w.callbacks.w = w
go w.run(options) w.eventState.initialOpts = options
return w 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, // update the window contents, input operations declare input handlers,
// and so on. The supplied operations list completely replaces the window state // and so on. The supplied operations list completely replaces the window state
// from previous calls. // from previous calls.
@@ -713,7 +720,7 @@ func (w *Window) waitAck(d driver) {
select { select {
case f := <-w.driverFuncs: case f := <-w.driverFuncs:
f(d) 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. // A dummy event went through, so we know the application has processed the previous event.
return return
case <-w.immediateRedraws: case <-w.immediateRedraws:
@@ -747,7 +754,7 @@ func (w *Window) waitFrame(d driver) *op.Ops {
case frame := <-w.frames: case frame := <-w.frames:
// The client called FrameEvent.Frame. // The client called FrameEvent.Frame.
return frame return frame
case w.out <- event.Event(nil): case w.out <- theFlushEvent:
// The client ignored FrameEvent and continued processing // The client ignored FrameEvent and continued processing
// events. // events.
return nil 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 { if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil {
w.destroyGPU() w.destroyGPU()
w.out <- system.DestroyEvent{Err: err} w.out <- system.DestroyEvent{Err: err}
close(w.out)
close(w.destroy) close(w.destroy)
break break
} }
@@ -890,7 +896,6 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
case system.DestroyEvent: case system.DestroyEvent:
w.destroyGPU() w.destroyGPU()
w.out <- e2 w.out <- e2
close(w.out)
close(w.destroy) close(w.destroy)
case ViewEvent: case ViewEvent:
w.out <- e2 w.out <- e2
@@ -938,43 +943,51 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
return true return true
} }
func (w *Window) run(options []Option) { // NextEvent blocks until an event is received from the window, such as
if err := newWindow(&w.callbacks, options); err != nil { // [io/system.FrameEvent]. It blocks forever if called after [io/system.DestroyEvent]
w.out <- system.DestroyEvent{Err: err} // has been returned.
close(w.out) func (w *Window) NextEvent() event.Event {
close(w.destroy) state := &w.eventState
return 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 { for {
var ( var (
wakeups <-chan struct{} wakeups <-chan struct{}
timeC <-chan time.Time timeC <-chan time.Time
) )
if wakeup != nil { if state.wakeup != nil {
wakeups = w.wakeups wakeups = w.wakeups
if timer != nil { if state.timer != nil {
timeC = timer.C timeC = state.timer.C
} }
} }
select { select {
case t := <-w.scheduledRedraws: case t := <-w.scheduledRedraws:
if timer != nil { if state.timer != nil {
timer.Stop() state.timer.Stop()
} }
timer = time.NewTimer(time.Until(t)) state.timer = time.NewTimer(time.Until(t))
case <-w.destroy: case e := <-w.out:
return // 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: case <-timeC:
select { select {
case w.redraws <- struct{}{}: case w.redraws <- struct{}{}:
wakeup() state.wakeup()
default: default:
} }
case <-wakeups: case <-wakeups:
wakeup() state.wakeup()
case wakeup = <-w.wakeupFuncs: case state.wakeup = <-w.wakeupFuncs:
} }
} }
} }
@@ -1165,3 +1178,14 @@ func Decorated(enabled bool) Option {
cnf.Decorated = enabled 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