mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
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:
+6
-5
@@ -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()
|
||||
|
||||
+53
-29
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user