From 93919432ed0f57a589a83ab1883b50cc2f1dfc94 Mon Sep 17 00:00:00 2001 From: FPabl0 Date: Wed, 17 Feb 2021 00:54:24 -0500 Subject: [PATCH 1/3] fix entry cursor animation by adding an object to handle interruptions triggered by TypedKey method in widget.Entry, added entry cursor animation test --- widget/entry.go | 50 +++++---------- widget/entry_cursor_anim.go | 106 +++++++++++++++++++++++++++++++ widget/entry_cursor_anim_test.go | 72 +++++++++++++++++++++ 3 files changed, 194 insertions(+), 34 deletions(-) create mode 100644 widget/entry_cursor_anim.go create mode 100644 widget/entry_cursor_anim_test.go diff --git a/widget/entry.go b/widget/entry.go index cde5caadcc..d50d988dd1 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -4,7 +4,6 @@ import ( "image/color" "math" "strings" - "time" "unicode" "fyne.io/fyne/v2" @@ -57,6 +56,8 @@ type Entry struct { CursorRow, CursorColumn int OnCursorChanged func() `json:"-"` + cursorAnim *entryCursorAnimation + focused bool text *textProvider placeholder *textProvider @@ -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.NewScroll(e.content) objects := []fyne.CanvasObject{box, line, e.scroll} @@ -497,7 +501,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 @@ -1222,9 +1228,6 @@ 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() @@ -1232,9 +1235,9 @@ func (e *entryContent) CreateRenderer() fyne.WidgetRenderer { 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) @@ -1263,17 +1266,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) { @@ -1323,15 +1325,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() @@ -1579,17 +1575,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 -} diff --git a/widget/entry_cursor_anim.go b/widget/entry_cursor_anim.go new file mode 100644 index 0000000000..5fcfc83e13 --- /dev/null +++ b/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() +} diff --git a/widget/entry_cursor_anim_test.go b/widget/entry_cursor_anim_test.go new file mode 100644 index 0000000000..d0b85ad5b4 --- /dev/null +++ b/widget/entry_cursor_anim_test.go @@ -0,0 +1,72 @@ +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() + if a1 == a2 { + return true + } + return false + } + + 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) + runtime.Gosched() + 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) + 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) + runtime.Gosched() + assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor)) + + a.timeNow = func() time.Time { + return time.Now().Add(cursorInterruptTime) + } + 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) +} From 3f3a6444c7f0f44c088dc3ee587d1e2d28dad510 Mon Sep 17 00:00:00 2001 From: FPabl0 Date: Wed, 17 Feb 2021 01:23:47 -0500 Subject: [PATCH 2/3] fix entry cursor animation test --- widget/entry_cursor_anim_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/widget/entry_cursor_anim_test.go b/widget/entry_cursor_anim_test.go index d0b85ad5b4..366dfbea6b 100644 --- a/widget/entry_cursor_anim_test.go +++ b/widget/entry_cursor_anim_test.go @@ -20,10 +20,7 @@ func TestEntryCursorAnim(t *testing.T) { alphaEquals := func(color1, color2 color.Color) bool { _, _, _, a1 := color1.RGBA() _, _, _, a2 := color2.RGBA() - if a1 == a2 { - return true - } - return false + return a1 == a2 } cursor := canvas.NewRectangle(color.Black) @@ -37,7 +34,6 @@ func TestEntryCursorAnim(t *testing.T) { a.interrupt() a.anim.Tick(0.0) - runtime.Gosched() assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor)) a.anim.Tick(0.5) assert.True(t, alphaEquals(cursorOpaque, a.cursor.FillColor)) @@ -49,6 +45,8 @@ func TestEntryCursorAnim(t *testing.T) { } // animation should be restarted inverting the colors a.anim.Tick(0.0) + runtime.Gosched() // 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)) @@ -56,13 +54,14 @@ func TestEntryCursorAnim(t *testing.T) { a.timeNow = time.Now a.interrupt() a.anim.Tick(0.0) - runtime.Gosched() 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() // 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)) From 11ad61a05ba348d97d324d5cb30782af5685fb04 Mon Sep 17 00:00:00 2001 From: FPabl0 Date: Wed, 17 Feb 2021 01:54:33 -0500 Subject: [PATCH 3/3] fix entry cursor animation test for windows --- widget/entry_cursor_anim_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/widget/entry_cursor_anim_test.go b/widget/entry_cursor_anim_test.go index 366dfbea6b..d25d1d3c63 100644 --- a/widget/entry_cursor_anim_test.go +++ b/widget/entry_cursor_anim_test.go @@ -45,7 +45,8 @@ func TestEntryCursorAnim(t *testing.T) { } // animation should be restarted inverting the colors a.anim.Tick(0.0) - runtime.Gosched() // ensure go routine for restart animation is executed + 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) @@ -60,7 +61,8 @@ func TestEntryCursorAnim(t *testing.T) { return time.Now().Add(cursorInterruptTime) } a.anim.Tick(0.0) - runtime.Gosched() // ensure go routine for restart animation is executed + 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)