From e26871a924325dddae163f82475c1eacd85275dc Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 10 Dec 2023 10:46:03 -0800 Subject: [PATCH 1/8] WIP- optimize animation runner --- animation.go | 16 ++++++- driver.go | 9 +++- internal/animation/animation.go | 10 ---- internal/animation/animation_test.go | 18 +++---- internal/animation/runner.go | 72 ++++++++++------------------ internal/driver/glfw/animation.go | 2 +- internal/driver/mobile/animation.go | 2 +- 7 files changed, 57 insertions(+), 72 deletions(-) diff --git a/animation.go b/animation.go index a8aeba12fb..035fb3a6de 100644 --- a/animation.go +++ b/animation.go @@ -1,6 +1,8 @@ package fyne -import "time" +import ( + "time" +) // AnimationCurve represents an animation algorithm for calculating the progress through a timeline. // Custom animations can be provided by implementing the "func(float32) float32" definition. @@ -42,6 +44,8 @@ type Animation struct { Duration time.Duration RepeatCount int Tick func(float32) + + stopped bool } // NewAnimation creates a very basic animation where the callback function will be called for every @@ -55,12 +59,20 @@ func NewAnimation(d time.Duration, fn func(float32)) *Animation { // Start registers the animation with the application run-loop and starts its execution. func (a *Animation) Start() { + a.stopped = false CurrentApp().Driver().StartAnimation(a) } // Stop will end this animation and remove it from the run-loop. func (a *Animation) Stop() { - CurrentApp().Driver().StopAnimation(a) + a.stopped = true +} + +// Stopped returns true if this animation has been stopped or has completed running. +// +// Since: 2.5 +func (a *Animation) Stopped() bool { + return a.stopped } func animationEaseIn(val float32) float32 { diff --git a/driver.go b/driver.go index 8737f69148..cb1fd7347c 100644 --- a/driver.go +++ b/driver.go @@ -26,7 +26,12 @@ type Driver interface { Quit() // StartAnimation registers a new animation with this driver and requests it be started. - StartAnimation(*Animation) + // + // Deprecated: Use a.Start() instead. + StartAnimation(a *Animation) + // StopAnimation stops an animation and unregisters from this driver. - StopAnimation(*Animation) + // + // Deprecated: Use a.Stop() instead. + StopAnimation(a *Animation) } diff --git a/internal/animation/animation.go b/internal/animation/animation.go index 68eccec1d9..7ffda4ec57 100644 --- a/internal/animation/animation.go +++ b/internal/animation/animation.go @@ -1,7 +1,6 @@ package animation import ( - "sync/atomic" "time" "fyne.io/fyne/v2" @@ -14,7 +13,6 @@ type anim struct { reverse bool start time.Time total int64 - stopped uint32 // atomic, 0 == false 1 == true } func newAnim(a *fyne.Animation) *anim { @@ -23,11 +21,3 @@ func newAnim(a *fyne.Animation) *anim { animate.repeatsLeft = a.RepeatCount return animate } - -func (a *anim) setStopped() { - atomic.StoreUint32(&a.stopped, 1) -} - -func (a *anim) isStopped() bool { - return atomic.LoadUint32(&a.stopped) == 1 -} diff --git a/internal/animation/animation_test.go b/internal/animation/animation_test.go index cb5b173170..7ea11d1f45 100644 --- a/internal/animation/animation_test.go +++ b/internal/animation/animation_test.go @@ -47,10 +47,10 @@ func TestGLDriver_StopAnimation(t *testing.T) { case <-time.After(time.Second): t.Error("animation was not ticked") } - run.Stop(a) - run.animationMutex.RLock() - assert.Zero(t, len(run.animations)) - run.animationMutex.RUnlock() + a.Stop() + run.animationMutex.Lock() + assert.True(t, a.Stopped(), "animation was not stopped") + run.animationMutex.Unlock() } func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) { @@ -64,7 +64,7 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) { Tick: func(f float32) {}, } run.Start(a) - run.Stop(a) + a.Stop() // stopping animation inside tick function for i := 0; i < 10; i++ { @@ -73,7 +73,7 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) { b = &fyne.Animation{ Duration: time.Second, Tick: func(d float32) { - run.Stop(b) + b.Stop() wg.Done() }} run.Start(b) @@ -86,12 +86,12 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) { Tick: func(f float32) {}, } run.Start(c) - run.Stop(c) + c.Stop() 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 e4278bfff0..6c6ebaeb4c 100644 --- a/internal/animation/runner.go +++ b/internal/animation/runner.go @@ -9,11 +9,11 @@ import ( // Runner is the main driver for animations package type Runner struct { - animationMutex sync.RWMutex - animations []*anim + animationMutex sync.Mutex pendingAnimations []*anim + runnerStarted bool - runnerStarted bool + animations []*anim // accessed only by runAnimations } // Start will register the passed application and initiate its ticking. @@ -21,66 +21,44 @@ func (r *Runner) Start(a *fyne.Animation) { r.animationMutex.Lock() defer r.animationMutex.Unlock() + r.pendingAnimations = append(r.pendingAnimations, newAnim(a)) if !r.runnerStarted { r.runnerStarted = true - r.animations = append(r.animations, newAnim(a)) r.runAnimations() - } else { - r.pendingAnimations = append(r.pendingAnimations, newAnim(a)) } } -// Stop causes an animation to stop ticking (if it was still running) and removes it from the runner. -func (r *Runner) Stop(a *fyne.Animation) { - r.animationMutex.Lock() - defer r.animationMutex.Unlock() - - newList := make([]*anim, 0, len(r.animations)) - stopped := false - for _, item := range r.animations { - if item.a != a { - newList = append(newList, item) - } else { - item.setStopped() - stopped = true - } - } - r.animations = newList - if stopped { - return - } - - newList = make([]*anim, 0, len(r.pendingAnimations)) - for _, item := range r.pendingAnimations { - if item.a != a { - newList = append(newList, item) - } else { - item.setStopped() - } - } - r.pendingAnimations = newList -} - func (r *Runner) runAnimations() { draw := time.NewTicker(time.Second / 60) go func() { for done := false; !done; { <-draw.C - r.animationMutex.Lock() - oldList := r.animations - r.animationMutex.Unlock() - newList := make([]*anim, 0, len(oldList)) - for _, a := range oldList { - if !a.isStopped() && r.tickAnimation(a) { - newList = append(newList, a) + + // tick currently running animations + newList := r.animations[:0] // references same underlying backing array + for _, a := range r.animations { + if s := a.a.Stopped(); !s && r.tickAnimation(a) { + newList = append(newList, a) // still running + } else if !s { + a.a.Stop() // mark as stopped (completed running) } } + + // bring in all pending animations r.animationMutex.Lock() - r.animations = append(newList, r.pendingAnimations...) - r.pendingAnimations = nil - done = len(r.animations) == 0 + for i, a := range r.pendingAnimations { + newList = append(newList, a) + r.pendingAnimations[i] = nil + } + r.pendingAnimations = r.pendingAnimations[:0] r.animationMutex.Unlock() + + done = len(newList) == 0 + for i := len(newList); i < len(r.animations); i++ { + r.animations[i] = nil + } + r.animations = newList } r.animationMutex.Lock() r.runnerStarted = false diff --git a/internal/driver/glfw/animation.go b/internal/driver/glfw/animation.go index ec7d72a180..7162d06447 100644 --- a/internal/driver/glfw/animation.go +++ b/internal/driver/glfw/animation.go @@ -7,5 +7,5 @@ func (d *gLDriver) StartAnimation(a *fyne.Animation) { } func (d *gLDriver) StopAnimation(a *fyne.Animation) { - d.animation.Stop(a) + a.Stop() } diff --git a/internal/driver/mobile/animation.go b/internal/driver/mobile/animation.go index a70504201d..a2a101b87e 100644 --- a/internal/driver/mobile/animation.go +++ b/internal/driver/mobile/animation.go @@ -7,5 +7,5 @@ func (d *mobileDriver) StartAnimation(a *fyne.Animation) { } func (d *mobileDriver) StopAnimation(a *fyne.Animation) { - d.animation.Stop(a) + a.Stop() } From 7c66e5f9f4786499153c1cf08c9f5e29e9dbc231 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 10 Dec 2023 12:39:15 -0800 Subject: [PATCH 2/8] make animation.Start idempotent - no-op if started multiple times --- animation.go | 40 +++++++++++++++++++++++----- internal/animation/animation_test.go | 2 +- internal/animation/runner.go | 4 +-- internal/driver/glfw/animation.go | 4 +++ internal/driver/mobile/animation.go | 4 +++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/animation.go b/animation.go index 035fb3a6de..0857d3c661 100644 --- a/animation.go +++ b/animation.go @@ -1,6 +1,7 @@ package fyne import ( + "sync" "time" ) @@ -34,6 +35,22 @@ var ( AnimationLinear = animationLinear ) +// AnimationState represents the state of an animation. +// +// Since: 2.5 +type AnimationState int + +const ( + // AnimationStateNotStarted represents an animation that has been created but not yet started. + AnimationStateNotStarted AnimationState = iota + + // AnimationStateRunning represents an animation that is running. + AnimationStateRunning + + // AnimationStateStopped represents an animation that has been stopped or has finished running. + AnimationStateStopped +) + // Animation represents an animated element within a Fyne canvas. // These animations may control individual objects or entire scenes. // @@ -45,7 +62,8 @@ type Animation struct { RepeatCount int Tick func(float32) - stopped bool + mutex sync.Mutex + state AnimationState } // NewAnimation creates a very basic animation where the callback function will be called for every @@ -59,20 +77,28 @@ func NewAnimation(d time.Duration, fn func(float32)) *Animation { // Start registers the animation with the application run-loop and starts its execution. func (a *Animation) Start() { - a.stopped = false - CurrentApp().Driver().StartAnimation(a) + a.mutex.Lock() + defer a.mutex.Unlock() + if a.state == AnimationStateRunning { + return + } + a.state = AnimationStateRunning + d := CurrentApp().Driver().(interface{ StartAnimationPrivate(*Animation) }) + d.StartAnimationPrivate(a) } // Stop will end this animation and remove it from the run-loop. func (a *Animation) Stop() { - a.stopped = true + a.mutex.Lock() + defer a.mutex.Unlock() + a.state = AnimationStateStopped } -// Stopped returns true if this animation has been stopped or has completed running. +// State returns the state of this animation. // // Since: 2.5 -func (a *Animation) Stopped() bool { - return a.stopped +func (a *Animation) State() AnimationState { + return a.state } func animationEaseIn(val float32) float32 { diff --git a/internal/animation/animation_test.go b/internal/animation/animation_test.go index 7ea11d1f45..37385c18a2 100644 --- a/internal/animation/animation_test.go +++ b/internal/animation/animation_test.go @@ -49,7 +49,7 @@ func TestGLDriver_StopAnimation(t *testing.T) { } a.Stop() run.animationMutex.Lock() - assert.True(t, a.Stopped(), "animation was not stopped") + assert.True(t, a.State() == fyne.AnimationStateStopped, "animation was not stopped") run.animationMutex.Unlock() } diff --git a/internal/animation/runner.go b/internal/animation/runner.go index 6c6ebaeb4c..b4e5a0ff0d 100644 --- a/internal/animation/runner.go +++ b/internal/animation/runner.go @@ -38,9 +38,9 @@ func (r *Runner) runAnimations() { // tick currently running animations newList := r.animations[:0] // references same underlying backing array for _, a := range r.animations { - if s := a.a.Stopped(); !s && r.tickAnimation(a) { + if stopped := a.a.State() == fyne.AnimationStateStopped; !stopped && r.tickAnimation(a) { newList = append(newList, a) // still running - } else if !s { + } else if !stopped { a.a.Stop() // mark as stopped (completed running) } } diff --git a/internal/driver/glfw/animation.go b/internal/driver/glfw/animation.go index 7162d06447..80997fc103 100644 --- a/internal/driver/glfw/animation.go +++ b/internal/driver/glfw/animation.go @@ -3,6 +3,10 @@ package glfw import "fyne.io/fyne/v2" func (d *gLDriver) StartAnimation(a *fyne.Animation) { + a.Start() +} + +func (d *gLDriver) StartAnimationPrivate(a *fyne.Animation) { d.animation.Start(a) } diff --git a/internal/driver/mobile/animation.go b/internal/driver/mobile/animation.go index a2a101b87e..5b9ef2da4f 100644 --- a/internal/driver/mobile/animation.go +++ b/internal/driver/mobile/animation.go @@ -3,6 +3,10 @@ package mobile import "fyne.io/fyne/v2" func (d *mobileDriver) StartAnimation(a *fyne.Animation) { + a.Start() +} + +func (d *mobileDriver) StartAnimationPrivate(a *fyne.Animation) { d.animation.Start(a) } From 733c5648bdbe06562b26348df836e2c22845518f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 10 Dec 2023 14:03:05 -0800 Subject: [PATCH 3/8] add StartAnimationPrivate to testDriver; add some comments --- internal/animation/runner.go | 6 ++++-- test/testdriver.go | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/animation/runner.go b/internal/animation/runner.go index b4e5a0ff0d..bad7827aa2 100644 --- a/internal/animation/runner.go +++ b/internal/animation/runner.go @@ -36,7 +36,9 @@ func (r *Runner) runAnimations() { <-draw.C // tick currently running animations - newList := r.animations[:0] // references same underlying backing array + // use technique from https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating + // to filter the still-running animations for the next iteration without allocating a new slice + newList := r.animations[:0] for _, a := range r.animations { if stopped := a.a.State() == fyne.AnimationStateStopped; !stopped && r.tickAnimation(a) { newList = append(newList, a) // still running @@ -56,7 +58,7 @@ func (r *Runner) runAnimations() { done = len(newList) == 0 for i := len(newList); i < len(r.animations); i++ { - r.animations[i] = nil + r.animations[i] = nil // nil out extra slice capacity } r.animations = newList } diff --git a/test/testdriver.go b/test/testdriver.go index 146e44e1ff..273490e168 100644 --- a/test/testdriver.go +++ b/test/testdriver.go @@ -112,6 +112,11 @@ func (d *testDriver) StopAnimation(a *fyne.Animation) { // currently no animations in test app, do nothing } +func (d *testDriver) StartAnimationPrivate(a *fyne.Animation) { + /// currently no animations in test app, we just initialise it and leave + a.Tick(1.0) +} + func (d *testDriver) Quit() { // no-op } From e7622cefde489b811e2717c02076f5dd388ce6b0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 10 Dec 2023 14:23:09 -0800 Subject: [PATCH 4/8] make critical section shorter --- internal/animation/runner.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/animation/runner.go b/internal/animation/runner.go index bad7827aa2..11791fa375 100644 --- a/internal/animation/runner.go +++ b/internal/animation/runner.go @@ -19,9 +19,9 @@ 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() - r.pendingAnimations = append(r.pendingAnimations, newAnim(a)) + r.animationMutex.Unlock() + if !r.runnerStarted { r.runnerStarted = true r.runAnimations() From dcb81b3ba65e86bf714746b4a217b96d473a3d4e Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 10 Dec 2023 14:26:35 -0800 Subject: [PATCH 5/8] remove now-unneeded lock in test code --- internal/animation/animation_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/animation/animation_test.go b/internal/animation/animation_test.go index 37385c18a2..be6992f884 100644 --- a/internal/animation/animation_test.go +++ b/internal/animation/animation_test.go @@ -48,9 +48,7 @@ func TestGLDriver_StopAnimation(t *testing.T) { t.Error("animation was not ticked") } a.Stop() - run.animationMutex.Lock() assert.True(t, a.State() == fyne.AnimationStateStopped, "animation was not stopped") - run.animationMutex.Unlock() } func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) { From 26ef0a63014f5a3c830a6928fab7d67a0a5a03bd Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 10 Dec 2023 14:40:47 -0800 Subject: [PATCH 6/8] rename StartAnimationPrivate -> StartAnimationInternal --- animation.go | 4 ++-- internal/driver/glfw/animation.go | 2 +- internal/driver/mobile/animation.go | 2 +- test/testdriver.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/animation.go b/animation.go index 0857d3c661..cef4f20067 100644 --- a/animation.go +++ b/animation.go @@ -83,8 +83,8 @@ func (a *Animation) Start() { return } a.state = AnimationStateRunning - d := CurrentApp().Driver().(interface{ StartAnimationPrivate(*Animation) }) - d.StartAnimationPrivate(a) + d := CurrentApp().Driver().(interface{ StartAnimationInternal(*Animation) }) + d.StartAnimationInternal(a) } // Stop will end this animation and remove it from the run-loop. diff --git a/internal/driver/glfw/animation.go b/internal/driver/glfw/animation.go index 80997fc103..8a865a891b 100644 --- a/internal/driver/glfw/animation.go +++ b/internal/driver/glfw/animation.go @@ -6,7 +6,7 @@ func (d *gLDriver) StartAnimation(a *fyne.Animation) { a.Start() } -func (d *gLDriver) StartAnimationPrivate(a *fyne.Animation) { +func (d *gLDriver) StartAnimationInternal(a *fyne.Animation) { d.animation.Start(a) } diff --git a/internal/driver/mobile/animation.go b/internal/driver/mobile/animation.go index 5b9ef2da4f..2f1f81dd9b 100644 --- a/internal/driver/mobile/animation.go +++ b/internal/driver/mobile/animation.go @@ -6,7 +6,7 @@ func (d *mobileDriver) StartAnimation(a *fyne.Animation) { a.Start() } -func (d *mobileDriver) StartAnimationPrivate(a *fyne.Animation) { +func (d *mobileDriver) StartAnimationInternal(a *fyne.Animation) { d.animation.Start(a) } diff --git a/test/testdriver.go b/test/testdriver.go index 273490e168..2b6ee6f034 100644 --- a/test/testdriver.go +++ b/test/testdriver.go @@ -112,7 +112,7 @@ func (d *testDriver) StopAnimation(a *fyne.Animation) { // currently no animations in test app, do nothing } -func (d *testDriver) StartAnimationPrivate(a *fyne.Animation) { +func (d *testDriver) StartAnimationInternal(a *fyne.Animation) { /// currently no animations in test app, we just initialise it and leave a.Tick(1.0) } From 98b30eb2bdc4dffc0a43e42d87cfbc9d752f3f90 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 9 Jan 2024 16:20:13 -0800 Subject: [PATCH 7/8] use atomic Int64 for animation state --- animation.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/animation.go b/animation.go index cef4f20067..7cb923145a 100644 --- a/animation.go +++ b/animation.go @@ -1,7 +1,7 @@ package fyne import ( - "sync" + "sync/atomic" "time" ) @@ -62,8 +62,7 @@ type Animation struct { RepeatCount int Tick func(float32) - mutex sync.Mutex - state AnimationState + state atomic.Int64 } // NewAnimation creates a very basic animation where the callback function will be called for every @@ -77,28 +76,25 @@ func NewAnimation(d time.Duration, fn func(float32)) *Animation { // Start registers the animation with the application run-loop and starts its execution. func (a *Animation) Start() { - a.mutex.Lock() - defer a.mutex.Unlock() - if a.state == AnimationStateRunning { + old := a.state.Swap(int64(AnimationStateRunning)) + if old == int64(AnimationStateRunning) { return } - a.state = AnimationStateRunning + d := CurrentApp().Driver().(interface{ StartAnimationInternal(*Animation) }) d.StartAnimationInternal(a) } // Stop will end this animation and remove it from the run-loop. func (a *Animation) Stop() { - a.mutex.Lock() - defer a.mutex.Unlock() - a.state = AnimationStateStopped + a.state.Store(int64(AnimationStateStopped)) } // State returns the state of this animation. // // Since: 2.5 func (a *Animation) State() AnimationState { - return a.state + return AnimationState(a.state.Load()) } func animationEaseIn(val float32) float32 { From e47e3b223a2003e1629420180367cb18881a8ee9 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 9 Jan 2024 16:35:12 -0800 Subject: [PATCH 8/8] synchronize on runner started --- internal/animation/animation_test.go | 2 - internal/animation/runner.go | 76 +++++++++++++--------------- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/internal/animation/animation_test.go b/internal/animation/animation_test.go index a6a2a740ec..9b40a94e5b 100644 --- a/internal/animation/animation_test.go +++ b/internal/animation/animation_test.go @@ -88,7 +88,5 @@ 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.Lock() assert.Zero(t, len(run.animations)) - run.animationMutex.Unlock() } diff --git a/internal/animation/runner.go b/internal/animation/runner.go index 11791fa375..394fd0f20e 100644 --- a/internal/animation/runner.go +++ b/internal/animation/runner.go @@ -2,6 +2,7 @@ package animation import ( "sync" + "sync/atomic" "time" "fyne.io/fyne/v2" @@ -9,64 +10,59 @@ import ( // Runner is the main driver for animations package type Runner struct { - animationMutex sync.Mutex - pendingAnimations []*anim - runnerStarted bool + pendingAnimationsMutex sync.Mutex + pendingAnimations []*anim + runnerStarted atomic.Bool animations []*anim // accessed only by runAnimations } // Start will register the passed application and initiate its ticking. func (r *Runner) Start(a *fyne.Animation) { - r.animationMutex.Lock() + r.pendingAnimationsMutex.Lock() r.pendingAnimations = append(r.pendingAnimations, newAnim(a)) - r.animationMutex.Unlock() + r.pendingAnimationsMutex.Unlock() - if !r.runnerStarted { - r.runnerStarted = true - r.runAnimations() + if r.runnerStarted.CompareAndSwap(false, true) { + go r.runAnimations() } } func (r *Runner) runAnimations() { draw := time.NewTicker(time.Second / 60) - go func() { - for done := false; !done; { - <-draw.C - - // tick currently running animations - // use technique from https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating - // to filter the still-running animations for the next iteration without allocating a new slice - newList := r.animations[:0] - for _, a := range r.animations { - if stopped := a.a.State() == fyne.AnimationStateStopped; !stopped && r.tickAnimation(a) { - newList = append(newList, a) // still running - } else if !stopped { - a.a.Stop() // mark as stopped (completed running) - } + for done := false; !done; { + <-draw.C + + // tick currently running animations + // use technique from https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating + // to filter the still-running animations for the next iteration without allocating a new slice + newList := r.animations[:0] + for _, a := range r.animations { + if stopped := a.a.State() == fyne.AnimationStateStopped; !stopped && r.tickAnimation(a) { + newList = append(newList, a) // still running + } else if !stopped { + a.a.Stop() // mark as stopped (completed running) } + } - // bring in all pending animations - r.animationMutex.Lock() - for i, a := range r.pendingAnimations { - newList = append(newList, a) - r.pendingAnimations[i] = nil - } - r.pendingAnimations = r.pendingAnimations[:0] - r.animationMutex.Unlock() + // bring in all pending animations + r.pendingAnimationsMutex.Lock() + for i, a := range r.pendingAnimations { + newList = append(newList, a) + r.pendingAnimations[i] = nil + } + r.pendingAnimations = r.pendingAnimations[:0] + r.pendingAnimationsMutex.Unlock() - done = len(newList) == 0 - for i := len(newList); i < len(r.animations); i++ { - r.animations[i] = nil // nil out extra slice capacity - } - r.animations = newList + done = len(newList) == 0 + for i := len(newList); i < len(r.animations); i++ { + r.animations[i] = nil // nil out extra slice capacity } - r.animationMutex.Lock() - r.runnerStarted = false - r.animationMutex.Unlock() - draw.Stop() - }() + r.animations = newList + } + r.runnerStarted.Store(false) + draw.Stop() } // tickAnimation will process a frame of animation and return true if this should continue animating