Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix entry cursor animation #1968

Merged
merged 4 commits into from Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 16 additions & 34 deletions widget/entry.go
Expand Up @@ -4,7 +4,6 @@ import (
"image/color"
"math"
"strings"
"time"
"unicode"

"fyne.io/fyne/v2"
Expand Down Expand Up @@ -57,6 +56,8 @@ type Entry struct {
CursorRow, CursorColumn int
OnCursorChanged func() `json:"-"`

cursorAnim *entryCursorAnimation

focused bool
text *textProvider
placeholder *textProvider
Expand Down Expand Up @@ -161,7 +162,10 @@ func (e *Entry) CreateRenderer() fyne.WidgetRenderer {

box := canvas.NewRectangle(theme.InputBackgroundColor())
line := canvas.NewRectangle(theme.ShadowColor())
cursor := canvas.NewRectangle(color.Transparent)
cursor.Hide()

e.cursorAnim = newEntryCursorAnimation(cursor)
e.content = &entryContent{entry: e}
e.scroll = &widget.Scroll{}
objects := []fyne.CanvasObject{box, line}
Expand Down Expand Up @@ -504,7 +508,9 @@ func (e *Entry) TypedKey(key *fyne.KeyEvent) {
if e.Disabled() {
return
}

if e.cursorAnim != nil {
e.cursorAnim.interrupt()
}
e.propertyLock.RLock()
provider := e.textProvider()
onSubmitted := e.OnSubmitted
Expand Down Expand Up @@ -1280,19 +1286,16 @@ type entryContent struct {
func (e *entryContent) CreateRenderer() fyne.WidgetRenderer {
e.ExtendBaseWidget(e)

cursor := canvas.NewRectangle(color.Transparent)
cursor.Hide()

e.entry.propertyLock.Lock()
defer e.entry.propertyLock.Unlock()
provider := e.entry.textProvider()
placeholder := e.entry.placeholderProvider()
if provider.len() != 0 {
placeholder.Hide()
}
objects := []fyne.CanvasObject{placeholder, provider, cursor}
objects := []fyne.CanvasObject{placeholder, provider, e.entry.cursorAnim.cursor}

r := &entryContentRenderer{cursor, []fyne.CanvasObject{}, nil, objects,
r := &entryContentRenderer{e.entry.cursorAnim.cursor, []fyne.CanvasObject{}, objects,
provider, placeholder, e}
r.updateScrollDirections()
r.Layout(e.size)
Expand Down Expand Up @@ -1321,17 +1324,16 @@ func (e *entryContent) Dragged(d *fyne.DragEvent) {
var _ fyne.WidgetRenderer = (*entryContentRenderer)(nil)

type entryContentRenderer struct {
cursor *canvas.Rectangle
selection []fyne.CanvasObject
cursorAnim *fyne.Animation
objects []fyne.CanvasObject
cursor *canvas.Rectangle
selection []fyne.CanvasObject
objects []fyne.CanvasObject

provider, placeholder *textProvider
content *entryContent
}

func (r *entryContentRenderer) Destroy() {
r.cursorAnim.Stop()
r.content.entry.cursorAnim.stop()
}

func (r *entryContentRenderer) Layout(size fyne.Size) {
Expand Down Expand Up @@ -1383,15 +1385,9 @@ func (r *entryContentRenderer) Refresh() {

if focused {
r.cursor.Show()
if r.cursorAnim == nil {
r.cursorAnim = makeCursorAnimation(r.cursor)
r.cursorAnim.Start()
}
r.content.entry.cursorAnim.start()
} else {
if r.cursorAnim != nil {
r.cursorAnim.Stop()
r.cursorAnim = nil
}
r.content.entry.cursorAnim.stop()
r.cursor.Hide()
}
r.moveCursor()
Expand Down Expand Up @@ -1641,17 +1637,3 @@ func getTextWhitespaceRegion(row []rune, col int) (int, int) {
}
return start, end
}

func makeCursorAnimation(cursor *canvas.Rectangle) *fyne.Animation {
cursorOpaque := theme.PrimaryColor()
r, g, b, _ := theme.PrimaryColor().RGBA()
cursorDim := color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 0x16}
anim := canvas.NewColorRGBAAnimation(cursorDim, cursorOpaque, time.Second/2, func(c color.Color) {
cursor.FillColor = c
cursor.Refresh()
})
anim.RepeatCount = fyne.AnimationRepeatForever
anim.AutoReverse = true

return anim
}
106 changes: 106 additions & 0 deletions widget/entry_cursor_anim.go
@@ -0,0 +1,106 @@
package widget

import (
"image/color"
"sync"
"time"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
)

const cursorInterruptTime = 300 * time.Millisecond

type entryCursorAnimation struct {
mu *sync.RWMutex
cursor *canvas.Rectangle
anim *fyne.Animation
lastInterruptTime time.Time

timeNow func() time.Time // useful for testing
}

func newEntryCursorAnimation(cursor *canvas.Rectangle) *entryCursorAnimation {
a := &entryCursorAnimation{mu: &sync.RWMutex{}, cursor: cursor, timeNow: time.Now}
return a
}

// creates fyne animation
func (a *entryCursorAnimation) createAnim(inverted bool) *fyne.Animation {
cursorOpaque := theme.PrimaryColor()
r, g, b, _ := theme.PrimaryColor().RGBA()
cursorDim := color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 0x16}
start, end := color.Color(cursorDim), color.Color(cursorOpaque)
if inverted {
start, end = color.Color(cursorOpaque), color.Color(cursorDim)
}
interrupted := false
anim := canvas.NewColorRGBAAnimation(start, end, time.Second/2, func(c color.Color) {
a.mu.RLock()
shouldInterrupt := a.timeNow().Sub(a.lastInterruptTime) <= cursorInterruptTime
a.mu.RUnlock()
if shouldInterrupt {
if !interrupted {
a.cursor.FillColor = cursorOpaque
a.cursor.Refresh()
interrupted = true
}
return
}
if interrupted {
a.mu.Lock()
a.anim.Stop()
if !inverted {
a.anim = a.createAnim(true)
}
interrupted = false
a.mu.Unlock()
go func() {
a.mu.RLock()
canStart := a.anim != nil
a.mu.RUnlock()
if canStart {
a.anim.Start()
}
}()
return
}
a.cursor.FillColor = c
a.cursor.Refresh()
})

anim.RepeatCount = fyne.AnimationRepeatForever
anim.AutoReverse = true
return anim
}

// starts cursor animation.
func (a *entryCursorAnimation) start() {
a.mu.Lock()
isStopped := a.anim == nil
if isStopped {
a.anim = a.createAnim(false)
}
a.mu.Unlock()
if isStopped {
a.anim.Start()
}
}

// temporarily stops the animation by "cursorInterruptTime".
func (a *entryCursorAnimation) interrupt() {
a.mu.Lock()
a.lastInterruptTime = a.timeNow()
a.mu.Unlock()
}

// stops cursor animation.
func (a *entryCursorAnimation) stop() {
a.mu.Lock()
if a.anim != nil {
a.anim.Stop()
a.anim = nil
}
a.mu.Unlock()
}
73 changes: 73 additions & 0 deletions widget/entry_cursor_anim_test.go
@@ -0,0 +1,73 @@
package widget

import (
"image/color"
"runtime"
"testing"
"time"

"fyne.io/fyne/v2/canvas"
_ "fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/theme"
"github.com/stretchr/testify/assert"
)

func TestEntryCursorAnim(t *testing.T) {
cursorOpaque := theme.PrimaryColor()
r, g, b, _ := theme.PrimaryColor().RGBA()
cursorDim := color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 0x16}

alphaEquals := func(color1, color2 color.Color) bool {
_, _, _, a1 := color1.RGBA()
_, _, _, a2 := color2.RGBA()
return a1 == a2
}

cursor := canvas.NewRectangle(color.Black)
a := newEntryCursorAnimation(cursor)

a.start()
a.anim.Tick(0.0)
assert.True(t, alphaEquals(cursorDim, a.cursor.FillColor))
a.anim.Tick(1.0)
assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor))

a.interrupt()
a.anim.Tick(0.0)
assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor))
a.anim.Tick(0.5)
assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor))
a.anim.Tick(1.0)
assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor))

a.timeNow = func() time.Time {
return time.Now().Add(cursorInterruptTime)
}
// animation should be restarted inverting the colors
a.anim.Tick(0.0)
runtime.Gosched()
time.Sleep(10 * time.Millisecond) // ensure go routine for restart animation is executed
a.anim.Tick(0.0)
assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor))
a.anim.Tick(1.0)
assert.True(t, alphaEquals(cursorDim, a.cursor.FillColor))

a.timeNow = time.Now
a.interrupt()
a.anim.Tick(0.0)
assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor))

a.timeNow = func() time.Time {
return time.Now().Add(cursorInterruptTime)
}
a.anim.Tick(0.0)
runtime.Gosched()
time.Sleep(10 * time.Millisecond) // ensure go routine for restart animation is executed
a.anim.Tick(0.0)
assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor))
a.anim.Tick(1.0)
assert.True(t, alphaEquals(cursorDim, a.cursor.FillColor))

a.stop()
assert.Nil(t, a.anim)
}