diff --git a/app/settings.go b/app/settings.go index 3ee1cb205a..37981d0831 100644 --- a/app/settings.go +++ b/app/settings.go @@ -41,6 +41,9 @@ type settings struct { changeListeners async.Map[chan fyne.Settings, bool] watcher any // normally *fsnotify.Watcher or nil - avoid import in this file + changeListenerFuncsMutex sync.Mutex + changeListenerFuncs []func(fyne.Settings) + schema SettingsSchema } @@ -116,6 +119,12 @@ func (s *settings) AddChangeListener(listener chan fyne.Settings) { s.changeListeners.Store(listener, true) // the boolean is just a dummy value here. } +func (s *settings) AddChangeListenerFunc(f func(fyne.Settings)) { + s.changeListenerFuncsMutex.Lock() + s.changeListenerFuncs = append(s.changeListenerFuncs, f) + s.changeListenerFuncsMutex.Unlock() +} + func (s *settings) apply() { s.changeListeners.Range(func(listener chan fyne.Settings, _ bool) bool { select { @@ -126,6 +135,11 @@ func (s *settings) apply() { } return true }) + s.changeListenerFuncsMutex.Lock() + for _, f := range s.changeListenerFuncs { + f(s) + } + s.changeListenerFuncsMutex.Unlock() } func (s *settings) fileChanged() { diff --git a/internal/animation/animation_test.go b/internal/animation/animation_test.go index b3f64b049c..14ff5b2ed2 100644 --- a/internal/animation/animation_test.go +++ b/internal/animation/animation_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/test" ) func tick(run *Runner) { @@ -18,6 +19,7 @@ func tick(run *Runner) { } func TestGLDriver_StartAnimation(t *testing.T) { + test.NewTempApp(t) done := make(chan float32) run := &Runner{} a := &fyne.Animation{ @@ -37,6 +39,7 @@ func TestGLDriver_StartAnimation(t *testing.T) { } func TestGLDriver_StopAnimation(t *testing.T) { + test.NewTempApp(t) done := make(chan float32) run := &Runner{} a := &fyne.Animation{ @@ -54,12 +57,13 @@ func TestGLDriver_StopAnimation(t *testing.T) { t.Error("animation was not ticked") } run.Stop(a) - run.animationMutex.RLock() + run.animationMutex.Lock() assert.Zero(t, len(run.animations)) - run.animationMutex.RUnlock() + run.animationMutex.Unlock() } func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) { + test.NewTempApp(t) var wg sync.WaitGroup run := &Runner{} @@ -107,7 +111,7 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) { wg.Wait() // animations stopped inside tick are really stopped in the next runner cycle time.Sleep(time.Second/60 + 100*time.Millisecond) - run.animationMutex.RLock() + run.animationMutex.Lock() assert.Zero(t, len(run.animations)) - run.animationMutex.RUnlock() + run.animationMutex.Unlock() } diff --git a/internal/animation/runner.go b/internal/animation/runner.go index 0acdb71776..c6c1628c5f 100644 --- a/internal/animation/runner.go +++ b/internal/animation/runner.go @@ -11,7 +11,7 @@ import ( type Runner struct { // animationMutex synchronizes access to `animations` and `pendingAnimations` // between the runner goroutine and calls to Start and Stop - animationMutex sync.RWMutex + animationMutex sync.Mutex // animations is the list of animations that are being ticked in the current frame animations []*anim @@ -33,7 +33,7 @@ type Runner struct { // Start will register the passed application and initiate its ticking. func (r *Runner) Start(a *fyne.Animation) { r.animationMutex.Lock() - defer r.animationMutex.Unlock() + hadAnimations := len(r.pendingAnimations) > 0 || len(r.animations) > 0 if !r.runnerStarted { r.runnerStarted = true @@ -51,6 +51,21 @@ func (r *Runner) Start(a *fyne.Animation) { } r.pendingAnimations = append(r.pendingAnimations, newAnim(a)) } + r.animationMutex.Unlock() + + if !hadAnimations { + // wake up main thread if needed to begin running animations + if drv, ok := fyne.CurrentApp().Driver().(interface{ WakeUp() }); ok { + drv.WakeUp() + } + } +} + +func (r *Runner) HasAnimations() bool { + r.animationMutex.Lock() + defer r.animationMutex.Unlock() + + return len(r.pendingAnimations) > 0 || len(r.animations) > 0 } // Stop causes an animation to stop ticking (if it was still running) and removes it from the runner. @@ -86,18 +101,19 @@ func (r *Runner) Stop(a *fyne.Animation) { // TickAnimations progresses all running animations by one tick. // This will be called from the driver to update objects immediately before next paint. -func (r *Runner) TickAnimations() { +func (r *Runner) TickAnimations() (done bool) { if !r.runnerStarted { return } - done := r.runOneFrame() + done = r.runOneFrame() if done { r.animationMutex.Lock() r.runnerStarted = false r.animationMutex.Unlock() } + return done } func (r *Runner) runOneFrame() (done bool) { diff --git a/internal/driver/common/canvas.go b/internal/driver/common/canvas.go index 834c4a2e86..c6fbd003ae 100644 --- a/internal/driver/common/canvas.go +++ b/internal/driver/common/canvas.go @@ -349,6 +349,13 @@ func (c *Canvas) CheckDirtyAndClear() bool { // SetDirty sets canvas dirty flag atomically. func (c *Canvas) SetDirty() { c.dirty = true + + // wake up main thread in case we were called from goroutine + // TODO: hide this behind the migration flag introduced in + // https://github.com/fyne-io/fyne/pull/5425 + if drv, ok := fyne.CurrentApp().Driver().(interface{ WakeUp() }); ok { + drv.WakeUp() + } } // SetMenuTreeAndFocusMgr sets menu tree and focus manager. diff --git a/internal/driver/glfw/driver.go b/internal/driver/glfw/driver.go index 72b931bbc4..6f619be487 100644 --- a/internal/driver/glfw/driver.go +++ b/internal/driver/glfw/driver.go @@ -28,7 +28,6 @@ var _ fyne.Driver = (*gLDriver)(nil) type gLDriver struct { windows []fyne.Window - done chan struct{} animation animation.Runner @@ -97,7 +96,7 @@ func (d *gLDriver) Quit() { // Only call close once to avoid panic. if running.CompareAndSwap(true, false) { - close(d.done) + d.WakeUp() } } @@ -163,7 +162,5 @@ func (d *gLDriver) SetDisableScreenBlanking(disable bool) { func NewGLDriver() *gLDriver { repository.Register("file", intRepo.NewFileRepository()) - return &gLDriver{ - done: make(chan struct{}), - } + return &gLDriver{} } diff --git a/internal/driver/glfw/loop.go b/internal/driver/glfw/loop.go index 4a075ad95a..2dd91fd34e 100644 --- a/internal/driver/glfw/loop.go +++ b/internal/driver/glfw/loop.go @@ -54,6 +54,7 @@ func runOnMainWithWait(f func(), wait bool) { } else { funcQueue.In() <- funcData{f: f} } + wakeUpDriver() } // Preallocate to avoid allocations on every drawSingleFrame. @@ -91,6 +92,11 @@ func (d *gLDriver) drawSingleFrame() { refreshingCanvases = refreshingCanvases[:0] } +var ( + settingsToApply fyne.Settings + settingsMutex sync.Mutex +) + func (d *gLDriver) runGL() { if !running.CompareAndSwap(false, true) { return // Run was called twice. @@ -104,71 +110,122 @@ func (d *gLDriver) runGL() { f() } - settingsChange := make(chan fyne.Settings) - fyne.CurrentApp().Settings().AddChangeListener(settingsChange) + if stg, ok := fyne.CurrentApp().Settings().(interface{ AddChangeListenerFunc(func(fyne.Settings)) }); ok { + stg.AddChangeListenerFunc(func(s fyne.Settings) { + settingsMutex.Lock() + settingsToApply = s + settingsMutex.Unlock() + wakeUpDriver() + }) + } - eventTick := time.NewTicker(time.Second / 60) for { - select { - case <-d.done: - eventTick.Stop() - d.Terminate() - l := fyne.CurrentApp().Lifecycle().(*app.Lifecycle) - if f := l.OnStopped(); f != nil { - l.QueueEvent(f) + d.waitEvents() + + if d.animation.HasAnimations() { + // run animations while available + if exit := d.runAnimationLoop(); exit { + return + } + } else { + // idle mode: run single frame and sleep on d.waitEvents() above + if exit, _ := d.runSingleFrame(); exit { + return } - return + } + } +} + +// runs a loop that invokes runSingleFrame at 60 fps until there are no more animations +// uses pollEvents to check for events every frame +func (d *gLDriver) runAnimationLoop() bool { + t := time.NewTicker(time.Second / 60) + defer t.Stop() + for range t.C { + d.pollEvents() + exit, animationsDone := d.runSingleFrame() + if exit { + return exit + } else if animationsDone { + break + } + } + return false +} + +func (d *gLDriver) runSingleFrame() (exit, animationsDone bool) { + // check if we're shutting down + if !running.Load() { + d.Terminate() + l := fyne.CurrentApp().Lifecycle().(*app.Lifecycle) + if f := l.OnStopped(); f != nil { + l.QueueEvent(f) + } + return true, false + } + + // run funcs queued to main + funcsDone := false + for !funcsDone { + select { case f := <-funcQueue.Out(): f.f() if f.done != nil { f.done <- struct{}{} } - case <-eventTick.C: - d.pollEvents() - for i := 0; i < len(d.windows); i++ { - w := d.windows[i].(*window) - if w.viewport == nil { - continue - } - - if w.viewport.ShouldClose() { - d.destroyWindow(w, i) - i-- // Trailing windows are moved forward one step. - continue - } - - expand := w.shouldExpand - fullScreen := w.fullScreen - - if expand && !fullScreen { - w.fitContent() - shouldExpand := w.shouldExpand - w.shouldExpand = false - view := w.viewport - - if shouldExpand && runtime.GOOS != "js" { - view.SetSize(w.shouldWidth, w.shouldHeight) - } - } + default: + funcsDone = true + } + } + // apply settings change if any + settingsMutex.Lock() + set := settingsToApply + settingsToApply = nil + settingsMutex.Unlock() + if set != nil { + painter.ClearFontCache() + cache.ResetThemeCaches() + app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) { + c, ok := w.Canvas().(*glCanvas) + if !ok { + return } + c.applyThemeOutOfTreeObjects() + c.reloadScale() + }) + } - d.animation.TickAnimations() - d.drawSingleFrame() - case set := <-settingsChange: - painter.ClearFontCache() - cache.ResetThemeCaches() - app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) { - c, ok := w.Canvas().(*glCanvas) - if !ok { - return - } - c.applyThemeOutOfTreeObjects() - c.reloadScale() - }) + for i := 0; i < len(d.windows); i++ { + w := d.windows[i].(*window) + if w.viewport == nil { + continue + } + if w.viewport.ShouldClose() { + d.destroyWindow(w, i) + i-- // Trailing windows are moved forward one position. + continue + } + + expand := w.shouldExpand + fullScreen := w.fullScreen + + if expand && !fullScreen { + w.fitContent() + shouldExpand := w.shouldExpand + w.shouldExpand = false + view := w.viewport + + if shouldExpand && runtime.GOOS != "js" { + view.SetSize(w.shouldWidth, w.shouldHeight) + } } } + animationsDone = d.animation.TickAnimations() + d.drawSingleFrame() + + return false, animationsDone } func (d *gLDriver) destroyWindow(w *window, index int) { diff --git a/internal/driver/glfw/loop_desktop.go b/internal/driver/glfw/loop_desktop.go index 83a46788b7..3fff74a03f 100644 --- a/internal/driver/glfw/loop_desktop.go +++ b/internal/driver/glfw/loop_desktop.go @@ -24,6 +24,20 @@ func (d *gLDriver) pollEvents() { glfw.PollEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called } +func (d *gLDriver) waitEvents() { + glfw.WaitEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called +} + func (d *gLDriver) Terminate() { glfw.Terminate() } + +// WakeUp tells the driver to wake up from an idle state. +func (d *gLDriver) WakeUp() { + wakeUpDriver() +} + +// wakeUpDriver wakes up the driver but in the case a reference to it is not easily available. +func wakeUpDriver() { + glfw.PostEmptyEvent() +} diff --git a/internal/driver/glfw/loop_wasm.go b/internal/driver/glfw/loop_wasm.go index 37659c4b5b..24dc8abbf8 100644 --- a/internal/driver/glfw/loop_wasm.go +++ b/internal/driver/glfw/loop_wasm.go @@ -23,6 +23,20 @@ func (d *gLDriver) pollEvents() { glfw.PollEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called } +func (d *gLDriver) waitEvents() { + glfw.WaitEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called +} + func (d *gLDriver) Terminate() { glfw.Terminate() } + +// WakeUp tells the driver to wake up from an idle state. +func (d *gLDriver) WakeUp() { + wakeUpDriver() +} + +// wakeUpDriver wakes up the driver but in the case a reference to it is not easily available. +func wakeUpDriver() { + glfw.PostEmptyEvent() +}