ronantakizawa/codereview-bench huggingface dataset:
Repository: fyne-io/fyne
File: widget/list.go (Go)

Code before:
			continue
		}
		l.separators[i].Move(fyne.NewPos(0, child.Position().Y-dividerOff))
		l.separators[i].Resize(fyne.NewSize(l.list.Size().Width, separatorThickness))
		l.separators[i].Show()
	}
}

// invariant: visible is in ascending order of IDs
func (l *listLayout) searchVisible(visible []itemAndID, id ListItemID) (*listItem, bool) {
	// binary search
	low := 0
	high := len(visible) - 1
	for low <= high {
		mid := (low + high) / 2
		if visible[mid].id == id {
			return visible[mid].item, true
		}
		if visible[mid].id > id {
			high = mid - 1
		} else {
			low = mid + 1
		}
	}
	return nil, false
}

func (l *listLayout) nilOldSliceData(objs []fyne.CanvasObject, len, oldLen int) {
	if oldLen > len {
		objs = objs[:oldLen] // gain view into old data
		for i := len; i < oldLen; i++ {
			objs[i] = nil
		}
	}
}


Review comment (performance):
I wonder if it would be cleaner to just use `sort.Search()` here? Might be slightly slower but does it matter?

Code after:
	l.slicePool.Put(wasVisiblePtr)
	l.slicePool.Put(visiblePtr)
}

func (l *listLayout) updateSeparators() {
	if lenChildren := len(l.children); lenChildren > 1 {
		if lenSep := len(l.separators); lenSep > lenChildren {
			l.separators = l.separators[:lenChildren]
		} else {
			for i := lenSep; i < lenChildren; i++ {
				l.separators = append(l.separators, NewSeparator())
			}
		}
	} else {
		l.separators = nil
	}

	separatorThickness := theme.SeparatorThicknessSize()
	dividerOff := (theme.Padding() + separatorThickness) / 2
	for i, child := range l.children {
		if i == 0 {
			continue
		}
		l.separators[i].Move(fyne.NewPos(0, child.Position().Y-dividerOff))
		l.separators[i].Resize(fyne.NewSize(l.list.Size().Width, separatorThickness))
		l.separators[i].Show()
	}
}

// invariant: visible is in ascending order of IDs
func (l *listLayout) searchVisible(visible []itemAndID, id ListItemID) (*listItem, bool) {
	ln := len(visible)
	idx := sort.Search(ln, func(i int) bool { return visible[i].id >= id })
	if idx < ln && visible[idx].id == id {
		return visible[idx].item, true
	}
	return nil, false
}

func (l *listLayout) nilOldSliceData(objs []fyne.CanvasObject, len, oldLen int) {
	if oldLen > len {
		objs = objs[:oldLen] // gain view into old data
		for i := len; i < oldLen; i++ {
			objs[i] = nil
		}
	}
}

func (l *listLayout) nilOldVisibleSliceData(objs []itemAndID, len, oldLen int) {
	if oldLen > len {
		objs = objs[:oldLen] // gain view into old data
Repository: fyne-io/fyne
File: widget/check.go (Go)

Code before:
	c.MouseMoved(me)
}

// MouseOut is called when a desktop pointer exits the widget
func (c *Check) MouseOut() {
	if c.hovered {
		c.hovered = false
		c.Refresh()
	}
}

// MouseMoved is called when a desktop pointer hovers over the widget
func (c *Check) MouseMoved(me *desktop.MouseEvent) {
	if c.Disabled() {
		return
	}

	oldHovered := c.hovered
	if c.minSize.Width == 0 && c.minSize.Height == 0 {
		c.hovered = true
	} else if me.Position.X <= c.minSize.Width && me.Position.Y <= c.minSize.Height {
		c.hovered = true
	} else {
		// mouse outside the active area of the widget
		c.hovered = false
	}

	if oldHovered != c.hovered {
		c.Refresh()
	}
}

// Tapped is called when a pointer tapped event is captured and triggers any change handler
func (c *Check) Tapped(pe *fyne.PointEvent) {
	if c.Disabled() {
		return
	}
	if c.minSize.Height > 0 && c.minSize.Width > 0 &&
		(pe.Position.X > c.minSize.Width || pe.Position.Y > c.minSize.Height) {
		// tapped outside the active area of the widget
		return
	}

	if !c.focused && !fyne.CurrentDevice().IsMobile() {
		impl := c.super()

		if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
			c.Focus(impl.(fyne.Focusable))
		}
	}
	c.SetChecked(!c.Checked)

Review comment (refactor):
You can simplify the code and make it branch-free like the code example below. I think it also is a bit more readable perhaps?
```go
	c.hovered = c.minSize.IsZero() || (me.Position.X <= c.minSize.Width && me.Position.Y <= c.minSize.Height)
```

Code after:
	c.MouseMoved(me)
}

// MouseOut is called when a desktop pointer exits the widget
func (c *Check) MouseOut() {
	if c.hovered {
		c.hovered = false
		c.Refresh()
	}
}

// MouseMoved is called when a desktop pointer hovers over the widget
func (c *Check) MouseMoved(me *desktop.MouseEvent) {
	if c.Disabled() {
		return
	}

	oldHovered := c.hovered

	// only hovered if cached minSize has not been initialized (test code)
	// or the pointer is within the "active" area of the widget (its minSize)
	c.hovered = c.minSize.IsZero() ||
		(me.Position.X <= c.minSize.Width && me.Position.Y <= c.minSize.Height)

	if oldHovered != c.hovered {
		c.Refresh()
	}
}

// Tapped is called when a pointer tapped event is captured and triggers any change handler
func (c *Check) Tapped(pe *fyne.PointEvent) {
	if c.Disabled() {
		return
	}
	if !c.minSize.IsZero() &&
		(pe.Position.X > c.minSize.Width || pe.Position.Y > c.minSize.Height) {
		// tapped outside the active area of the widget
		return
	}

	if !c.focused && !fyne.CurrentDevice().IsMobile() {
		impl := c.super()

		if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
			c.Focus(impl.(fyne.Focusable))
		}
	}
	c.SetChecked(!c.Checked)
}

// MinSize returns the size that this widget should not shrink below
Repository: fyne-io/fyne
File: cmd/fyne_demo/main.go (Go)

Code before:
			obj.(*widget.Label).SetText(t.Title)
			if unsupportedTutorial(t) {
				obj.(*widget.Label).TextStyle = fyne.TextStyle{Italic: true}
			} else {
				obj.(*widget.Label).TextStyle = fyne.TextStyle{}
			}
		},
		OnSelected: func(uid string) {
			if t, ok := tutorials.Tutorials[uid]; ok {
				if unsupportedTutorial(t) {
					return
				}
				a.Preferences().SetString(preferenceCurrentTutorial, uid)
				setTutorial(t)
			}
		},
	}

	if loadPrevious {
		currentPref := a.Preferences().StringWithFallback(preferenceCurrentTutorial, "welcome")
		tree.Select(currentPref)
	}

	themes := container.NewGridWithColumns(2,
		widget.NewButton("Dark", func() {
			a.Settings().SetTheme(test.DarkTheme())
		}),
		widget.NewButton("Light", func() {
			a.Settings().SetTheme(test.LightTheme())
		}),
	)

	return container.NewBorder(nil, themes, nil, nil, tree)
}

func shortcutFocused(s fyne.Shortcut, w fyne.Window) {
	switch sh := s.(type) {
	case *fyne.ShortcutCopy:
		sh.Clipboard = w.Clipboard()
	case *fyne.ShortcutCut:
		sh.Clipboard = w.Clipboard()
	case *fyne.ShortcutPaste:
		sh.Clipboard = w.Clipboard()
	}
	if focused, ok := w.Canvas().Focused().(fyne.Shortcutable); ok {
		focused.TypedShortcut(s)
	}
}


Review comment (refactor):
Hmm, not sure how I feel about importing internal/test in fyne demo. I think in this case the duplicated code of the `forcedVariant` theme is good, since it A) makes fyne_demo depend on nothing but the public APIs, so it is a "true" demo of what is possible for 3rd party apps, and B) does show a code example for a custom theme, which is a common user question now, esp. after the deprecation of DarkTheme and LightTheme

Code after:
			if unsupportedTutorial(t) {
				obj.(*widget.Label).TextStyle = fyne.TextStyle{Italic: true}
			} else {
				obj.(*widget.Label).TextStyle = fyne.TextStyle{}
			}
		},
		OnSelected: func(uid string) {
			if t, ok := tutorials.Tutorials[uid]; ok {
				if unsupportedTutorial(t) {
					return
				}
				a.Preferences().SetString(preferenceCurrentTutorial, uid)
				setTutorial(t)
			}
		},
	}

	if loadPrevious {
		currentPref := a.Preferences().StringWithFallback(preferenceCurrentTutorial, "welcome")
		tree.Select(currentPref)
	}

	themes := container.NewGridWithColumns(2,
		widget.NewButton("Dark", func() {
			a.Settings().SetTheme(&forcedVariant{Theme: theme.DefaultTheme(), variant: theme.VariantDark})
		}),
		widget.NewButton("Light", func() {
			a.Settings().SetTheme(&forcedVariant{Theme: theme.DefaultTheme(), variant: theme.VariantLight})
		}),
	)

	return container.NewBorder(nil, themes, nil, nil, tree)
}

func shortcutFocused(s fyne.Shortcut, w fyne.Window) {
	switch sh := s.(type) {
	case *fyne.ShortcutCopy:
		sh.Clipboard = w.Clipboard()
	case *fyne.ShortcutCut:
		sh.Clipboard = w.Clipboard()
	case *fyne.ShortcutPaste:
		sh.Clipboard = w.Clipboard()
	}
	if focused, ok := w.Canvas().Focused().(fyne.Shortcutable); ok {
		focused.TypedShortcut(s)
	}
}
Repository: fyne-io/fyne
File: widget/entry.go (Go)

Code before:
func (e *Entry) Redo() {
	e.propertyLock.Lock()
	newText, action := e.undoStack.Redo(e.Text)
	e.propertyLock.Unlock()
	modify, ok := action.(*entryModifyAction)
	if !ok {
		return
	}
	pos := modify.Position
	if !modify.Delete {
		pos += len(modify.Text)
	}
	e.updateTextAndRefresh(newText)
	e.propertyLock.Lock()
	e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos)
	e.propertyLock.Unlock()
	e.Refresh()
}

func (e *Entry) Refresh() {
	e.propertyLock.Lock()
	e.minCache = nil
	e.propertyLock.Unlock()

	e.BaseWidget.Refresh()
}

// SelectedText returns the text currently selected in this Entry.
// If there is no selection it will return the empty string.
func (e *Entry) SelectedText() string {
	e.propertyLock.RLock()
	defer e.propertyLock.RUnlock()

	if !e.selecting {
		return ""
	}

	start, stop := e.selection()
	if start == stop {
		return ""
	}
	r := ([]rune)(e.textProvider().String())
	return string(r[start:stop])
}

// SetMinRowsVisible forces a multi-line entry to show `count` number of rows without scrolling.
// This is not a validation or requirement, it just impacts the minimum visible size.
// Use this carefully as Fyne apps can run on small screens so you may wish to add a scroll container if
// this number is high. Default is 3.
//
// Since: 2.2

Review comment (performance):
I'm not saying that it need to be done now or should be done, but maybe it would make sense to have some kind of `MinSize` cache built in to the basewidget? I suppose it is something we might want to do for all widgets more or less (unless the computation is very cheap).

Code after:
	e.propertyLock.Unlock()
	modify, ok := action.(*entryModifyAction)
	if !ok {
		return
	}
	pos := modify.Position
	if !modify.Delete {
		pos += len(modify.Text)
	}
	e.updateTextAndRefresh(newText)
	e.propertyLock.Lock()
	e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos)
	e.propertyLock.Unlock()
	e.Refresh()
}

func (e *Entry) Refresh() {
	e.minCache.Store(fyne.Size{})

	e.BaseWidget.Refresh()
}

// SelectedText returns the text currently selected in this Entry.
// If there is no selection it will return the empty string.
func (e *Entry) SelectedText() string {
	e.propertyLock.RLock()
	defer e.propertyLock.RUnlock()

	if !e.selecting {
		return ""
	}

	start, stop := e.selection()
	if start == stop {
		return ""
	}
	r := ([]rune)(e.textProvider().String())
	return string(r[start:stop])
}

// SetMinRowsVisible forces a multi-line entry to show `count` number of rows without scrolling.
// This is not a validation or requirement, it just impacts the minimum visible size.
// Use this carefully as Fyne apps can run on small screens so you may wish to add a scroll container if
// this number is high. Default is 3.
//
// Since: 2.2
func (e *Entry) SetMinRowsVisible(count int) {
	e.multiLineRows = count
	e.Refresh()
}
Repository: fyne-io/fyne
File: dialog/file.go (Go)

Code before:
package dialog

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/storage"
	"fyne.io/fyne/v2/storage/repository"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

type ViewLayout int

const (
	defaultView ViewLayout = iota
	ListView
	GridView
)

const viewLayoutKey = "fyne:fileDialogViewLayout"

type textWidget interface {
	fyne.Widget
	SetText(string)
}

type favoriteItem struct {
	locName string
	locIcon fyne.Resource
	loc     fyne.URI
}

type fileDialogPanel interface {
	fyne.Widget

	Unselect(int)
}


Review comment (suggestion):
Public comments are needed for all the new exported types and constants, including the "Since: 2.5" line (for the type, not the constants).

Code after:
package dialog

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/lang"
	"fyne.io/fyne/v2/storage"
	"fyne.io/fyne/v2/storage/repository"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

// ViewLayout can be passed to SetView() to set the view of
// a FileDialog
//
// Since: 2.5
type ViewLayout int

const (
	defaultView ViewLayout = iota
	ListView
	GridView
)

const viewLayoutKey = "fyne:fileDialogViewLayout"

type textWidget interface {
	fyne.Widget
	SetText(string)
}

type favoriteItem struct {
	locName string
	locIcon fyne.Resource
	loc     fyne.URI
}

type fileDialogPanel interface {
Repository: fyne-io/fyne
File: dialog/file.go (Go)

Code before:
			parent := id == 0 && len(dir.Path()) < len(f.dir.Path())
			_, isDir := dir.(fyne.ListableURI)
			o.(*fileDialogItem).setLocation(dir, isDir || parent, parent)
		}
	}
	choose := func(id int) {
		if file, ok := f.getDataItem(id); ok {
			f.selectedID = id
			f.setSelected(file, id)
		}
	}

	var toggleViewButtonIcon fyne.Resource
	if f.view == GridView {
		grid := widget.NewGridWrap(count, template, update)
		grid.OnSelected = choose
		f.files = grid
		toggleViewButtonIcon = theme.ListIcon()
	} else {
		list := widget.NewList(count, template, update)
		list.OnSelected = choose
		f.files = list
		toggleViewButtonIcon = theme.GridIcon()
	}

	f.toggleViewButton.SetIcon(toggleViewButtonIcon)

	if f.dir != nil {
		f.refreshDir(f.dir)
	}
	f.filesScroll.Content = container.NewPadded(f.files)
	f.filesScroll.Refresh()
}

func (f *fileDialog) getDataItem(id int) (fyne.URI, bool) {
	f.dataLock.RLock()
	defer f.dataLock.RUnlock()

	if id >= len(f.data) {
		return nil, false
	}

	return f.data[id], true
}

// effectiveStartingDir calculates the directory at which the file dialog should
// open, based on the values of startingDirectory, CWD, home, and any error
// conditions which occur.
//
// Order of precedence is:
//

Review comment (suggestion):
I think it's 2 lines less code if you just call `SetIcon` instead of adding the `toggleViewButtonIcon` variable in this context

Code after:
	update := func(id widget.GridWrapItemID, o fyne.CanvasObject) {
		if dir, ok := f.getDataItem(id); ok {
			parent := id == 0 && len(dir.Path()) < len(f.dir.Path())
			_, isDir := dir.(fyne.ListableURI)
			o.(*fileDialogItem).setLocation(dir, isDir || parent, parent)
		}
	}
	choose := func(id int) {
		if file, ok := f.getDataItem(id); ok {
			f.selectedID = id
			f.setSelected(file, id)
		}
	}

	if f.view == GridView {
		grid := widget.NewGridWrap(count, template, update)
		grid.OnSelected = choose
		f.files = grid
		f.toggleViewButton.SetIcon(theme.ListIcon())
	} else {
		list := widget.NewList(count, template, update)
		list.OnSelected = choose
		f.files = list
		f.toggleViewButton.SetIcon(theme.GridIcon())
	}

	if f.dir != nil {
		f.refreshDir(f.dir)
	}
	f.filesScroll.Content = container.NewPadded(f.files)
	f.filesScroll.Refresh()
}

func (f *fileDialog) getDataItem(id int) (fyne.URI, bool) {
	f.dataLock.RLock()
	defer f.dataLock.RUnlock()

	if id >= len(f.data) {
		return nil, false
	}

	return f.data[id], true
}

// effectiveStartingDir calculates the directory at which the file dialog should
// open, based on the values of startingDirectory, CWD, home, and any error
// conditions which occur.
//
// Order of precedence is:
//
//   - file.startingDirectory if non-empty, os.Stat()-able, and uses the file://
Repository: fyne-io/fyne
File: dialog/base.go (Go)

Code before:

// SetOnClosed allows to set a callback function that is called when
// the dialog is closed
func (d *dialog) SetOnClosed(closed func()) {
	// if there is already a callback set, remember it and call both
	originalCallback := d.callback

	d.callback = func(response bool) {
		closed()
		if originalCallback != nil {
			originalCallback(response)
		}
	}
}

func (d *dialog) hideWithResponse(resp bool) {
	d.win.Hide()
	if d.callback != nil {
		d.callback(resp)
	}
}

func (d *dialog) create(buttons fyne.CanvasObject) {
	label := widget.NewLabelWithStyle(d.title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})

	image := &canvas.Image{Resource: d.icon}
	if d.icon == nil {
		image.Hide()
	}

	content := container.New(&dialogLayout{d: d},
		image,
		newThemedBackground(),
		d.content,
		buttons,
		label,
	)

	d.win = widget.NewModalPopUp(content, d.parent.Canvas())
}

func (d *dialog) setButtons(buttons fyne.CanvasObject) {
	d.win.Content.(*fyne.Container).Objects[3] = buttons
	d.win.Refresh()
}

// The method .create() needs to be called before the dialog cna be shown.
func newDialog(title, message string, icon fyne.Resource, callback func(bool), parent fyne.Window) *dialog {
	d := &dialog{content: newCenterLabel(message), title: title, icon: icon, parent: parent}
	d.callback = callback


Review comment (performance):
If the dialog is immutable once created (ie if it doesn't have an image it will never have one), why not avoid adding the image entirely, or if it's easier code-wise, place a `layout.NewSpacer()` in place of the image in the container, as it's a less expensive object. That's what I often use as an "empty" canvas object.

Code after:
}

// SetOnClosed allows to set a callback function that is called when
// the dialog is closed
func (d *dialog) SetOnClosed(closed func()) {
	// if there is already a callback set, remember it and call both
	originalCallback := d.callback

	d.callback = func(response bool) {
		closed()
		if originalCallback != nil {
			originalCallback(response)
		}
	}
}

func (d *dialog) hideWithResponse(resp bool) {
	d.win.Hide()
	if d.callback != nil {
		d.callback(resp)
	}
}

func (d *dialog) create(buttons fyne.CanvasObject) {
	label := widget.NewLabelWithStyle(d.title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})

	var image fyne.CanvasObject
	if d.icon != nil {
		image = &canvas.Image{Resource: d.icon}
	} else {
		image = &layout.Spacer{}
	}

	content := container.New(&dialogLayout{d: d},
		image,
		newThemedBackground(),
		d.content,
		buttons,
		label,
	)

	d.win = widget.NewModalPopUp(content, d.parent.Canvas())
}

func (d *dialog) setButtons(buttons fyne.CanvasObject) {
	d.win.Content.(*fyne.Container).Objects[3] = buttons
	d.win.Refresh()
}

// The method .create() needs to be called before the dialog cna be shown.
func newDialog(title, message string, icon fyne.Resource, callback func(bool), parent fyne.Window) *dialog {
Repository: fyne-io/fyne
File: widget/list.go (Go)

Code before:
//
// Since: 2.1
func (l *List) UnselectAll() {
	if len(l.selected) == 0 {
		return
	}

	selected := l.selected
	l.selected = nil
	l.Refresh()
	if f := l.OnUnselected; f != nil {
		for _, id := range selected {
			f(id)
		}
	}
}

func (l *List) contentMinSize() fyne.Size {
	l.propertyLock.Lock()
	defer l.propertyLock.Unlock()
	items := 0
	if f := l.Length; f == nil {
		return fyne.NewSize(0, 0)
	} else {
		items = f()
	}

	separatorThickness := theme.Padding()
	if l.itemHeights == nil || len(l.itemHeights) == 0 {
		return fyne.NewSize(l.itemMin.Width,
			(l.itemMin.Height+separatorThickness)*float32(items)-separatorThickness)
	}

	height := float32(0)
	templateHeight := l.itemMin.Height
	for item := 0; item < items; item++ {
		itemHeight, ok := l.itemHeights[item]
		if ok {
			height += itemHeight
		} else {
			height += templateHeight
		}
	}

	return fyne.NewSize(l.itemMin.Width, height+separatorThickness*float32(items-1))
}

// fills l.visibleRowHeights and also returns offY and minRow
func (l *listLayout) calculateVisibleRowHeights(itemHeight float32, length int) (offY float32, minRow int) {
	rowOffset := float32(0)
	isVisible := false

Review comment (suggestion):
```suggestion
	if l.Length == nil  {
		return fyne.NewSize(0, 0)
	}
	items := l.Length()
```

Code after:
//
// Since: 2.1
func (l *List) UnselectAll() {
	if len(l.selected) == 0 {
		return
	}

	selected := l.selected
	l.selected = nil
	l.Refresh()
	if f := l.OnUnselected; f != nil {
		for _, id := range selected {
			f(id)
		}
	}
}

func (l *List) contentMinSize() fyne.Size {
	l.propertyLock.Lock()
	defer l.propertyLock.Unlock()
	if l.Length == nil {
		return fyne.NewSize(0, 0)
	}
	items := l.Length()

	separatorThickness := theme.Padding()
	if l.itemHeights == nil || len(l.itemHeights) == 0 {
		return fyne.NewSize(l.itemMin.Width,
			(l.itemMin.Height+separatorThickness)*float32(items)-separatorThickness)
	}

	height := float32(0)
	templateHeight := l.itemMin.Height
	for item := 0; item < items; item++ {
		itemHeight, ok := l.itemHeights[item]
		if ok {
			height += itemHeight
		} else {
			height += templateHeight
		}
	}

	return fyne.NewSize(l.itemMin.Width, height+separatorThickness*float32(items-1))
}

// fills l.visibleRowHeights and also returns offY and minRow
func (l *listLayout) calculateVisibleRowHeights(itemHeight float32, length int) (offY float32, minRow int) {
	rowOffset := float32(0)
	isVisible := false
	l.visibleRowHeights = l.visibleRowHeights[:0]
Repository: fyne-io/fyne
File: internal/animation/runner.go (Go)

Code before:
		} else {
			item.setStopped()
		}
	}
	r.pendingAnimations = newList
}

// 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() {
	if !r.runnerStarted {
		return
	}

	done := false
	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)
		}
	}
	r.animationMutex.Lock()
	r.animations = append(newList, r.pendingAnimations...)
	r.pendingAnimations = nil
	done = len(r.animations) == 0
	r.animationMutex.Unlock()

	if done {
		r.animationMutex.Lock()
		r.runnerStarted = false
		r.animationMutex.Unlock()
	}
}

// tickAnimation will process a frame of animation and return true if this should continue animating
func (r *Runner) tickAnimation(a *anim) bool {
	if time.Now().After(a.end) {
		if a.reverse {
			a.a.Tick(0.0)
			if a.repeatsLeft == 0 {
				return false
			}
			a.reverse = false
		} else {
			a.a.Tick(1.0)
			if a.a.AutoReverse {
				a.reverse = true
			}

Review comment (suggestion):
The runner can get a whole lot cleaner once we assume the new threading model. While Start and Stop can still be called when invoking user code within a Tick, we will know they won't be called elsewhere, so we can do some simplification and cleanup around the synchronization between Start, Stop and Tick. That can be a follow-up PR though.

Code after:
// 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
}

// 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() {
	if !r.runnerStarted {
		return
	}

	done := r.runOneFrame()

	if done {
		r.animationMutex.Lock()
		r.runnerStarted = false
		r.animationMutex.Unlock()
	}
}

func (r *Runner) runOneFrame() (done bool) {
	r.animationMutex.Lock()
	oldList := r.animations
	r.animationMutex.Unlock()