diff --git a/app/app.go b/app/app.go index fa93fca969..3759c95294 100644 --- a/app/app.go +++ b/app/app.go @@ -64,6 +64,7 @@ func (a *fyneApp) NewWindow(title string) fyne.Window { } func (a *fyneApp) Run() { + go a.lifecycle.RunEventQueue() a.driver.Run() } @@ -138,6 +139,7 @@ func newAppWithDriver(d fyne.Driver, id string) fyne.App { fyne.SetCurrentApp(newApp) newApp.prefs = newApp.newDefaultPreferences() + newApp.lifecycle.InitEventQueue() newApp.lifecycle.SetOnStoppedHookExecuted(func() { if prefs, ok := newApp.prefs.(*preferences); ok { prefs.forceImmediateSave() diff --git a/internal/app/lifecycle.go b/internal/app/lifecycle.go index 30dcbe36c8..f41aeda099 100644 --- a/internal/app/lifecycle.go +++ b/internal/app/lifecycle.go @@ -4,6 +4,7 @@ import ( "sync/atomic" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) var _ fyne.Lifecycle = (*Lifecycle)(nil) @@ -18,6 +19,8 @@ type Lifecycle struct { onStopped atomic.Pointer[func()] onStoppedHookExecuted func() + + eventQueue *async.UnboundedFuncChan } // SetOnStoppedHookExecuted is an internal function that lets Fyne schedule a clean-up after @@ -100,3 +103,36 @@ func (l *Lifecycle) OnStopped() func() { stopHook() } } + +// DestroyEventQueue destroys the event queue. +func (l *Lifecycle) DestroyEventQueue() { + l.eventQueue.Close() +} + +// InitEventQueue initializes the event queue. +func (l *Lifecycle) InitEventQueue() { + // This channel should be closed when the window is closed. + l.eventQueue = async.NewUnboundedFuncChan() +} + +// QueueEvent uses this method to queue up a callback that handles an event. This ensures +// user interaction events for a given window are processed in order. +func (l *Lifecycle) QueueEvent(fn func()) { + l.eventQueue.In() <- fn +} + +// RunEventQueue runs the event queue. This should called inside a go routine. +// This function blocks. +func (l *Lifecycle) RunEventQueue() { + for fn := range l.eventQueue.Out() { + fn() + } +} + +// WaitForEvents wait for all the events. +func (l *Lifecycle) WaitForEvents() { + done := make(chan struct{}) + + l.eventQueue.In() <- func() { done <- struct{}{} } + <-done +} diff --git a/internal/app/lifecycle_test.go b/internal/app/lifecycle_test.go index 4c5e25cdeb..f7ba9468c4 100644 --- a/internal/app/lifecycle_test.go +++ b/internal/app/lifecycle_test.go @@ -15,7 +15,10 @@ func TestLifecycle(t *testing.T) { assert.Nil(t, life.OnStarted()) assert.Nil(t, life.OnStopped()) - var entered, exited, start, stop, hookedStop bool + var entered, exited, start, stop, hookedStop, called bool + life.InitEventQueue() + go life.RunEventQueue() + life.QueueEvent(func() { called = true }) life.SetOnEnteredForeground(func() { entered = true }) life.OnEnteredForeground()() assert.True(t, entered) @@ -48,4 +51,8 @@ func TestLifecycle(t *testing.T) { assert.Nil(t, life.OnExitedForeground()) assert.Nil(t, life.OnStarted()) assert.Nil(t, life.OnStopped()) + + life.WaitForEvents() + life.DestroyEventQueue() + assert.True(t, called) } diff --git a/internal/driver/glfw/driver.go b/internal/driver/glfw/driver.go index 0dcaf756aa..0ba77675bf 100644 --- a/internal/driver/glfw/driver.go +++ b/internal/driver/glfw/driver.go @@ -166,6 +166,11 @@ func (d *gLDriver) Run() { go d.catchTerm() d.runGL() + + // Ensure lifecycle events run to completion before the app exits + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + l.WaitForEvents() + l.DestroyEventQueue() } func (d *gLDriver) DoubleTapDelay() time.Duration { diff --git a/internal/driver/glfw/loop.go b/internal/driver/glfw/loop.go index 6b1d8dc65b..e9fdc28b50 100644 --- a/internal/driver/glfw/loop.go +++ b/internal/driver/glfw/loop.go @@ -124,8 +124,9 @@ func (d *gLDriver) runGL() { eventTick.Stop() d.drawDone <- struct{}{} // wait for draw thread to stop d.Terminate() - if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnStopped(); f != nil { - go f() // don't block main, we don't have window event queue + l := fyne.CurrentApp().Lifecycle().(*app.Lifecycle) + if f := l.OnStopped(); f != nil { + l.QueueEvent(f) } return case f := <-funcQueue: diff --git a/internal/driver/mobile/driver.go b/internal/driver/mobile/driver.go index 47c22b4af6..54cb7e8631 100644 --- a/internal/driver/mobile/driver.go +++ b/internal/driver/mobile/driver.go @@ -143,6 +143,11 @@ func (d *driver) Run() { settingsChange := make(chan fyne.Settings) fyne.CurrentApp().Settings().AddChangeListener(settingsChange) draw := time.NewTicker(time.Second / 60) + defer func() { + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + l.WaitForEvents() + l.DestroyEventQueue() + }() for { select { @@ -292,8 +297,9 @@ func (d *driver) onStart() { } func (d *driver) onStop() { - if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnStopped(); f != nil { - go f() // don't block main, we don't have window event queue + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + if f := l.OnStopped(); f != nil { + l.QueueEvent(f) } }