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
+53 -29
View File
@@ -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