Skip to content

Commit

Permalink
fix: Expire gradient cache based on texture size
Browse files Browse the repository at this point in the history
Avoid caching gradients and reusing generated textures when their size
change.

This change adds an optional `CacheExpirable` interface for Canvas
objects to implement, allowing the painter to know when the cached
textures shouldn't be used.

Fixes #4608.
  • Loading branch information
adamantike committed Mar 3, 2024
1 parent 4f9d61b commit 6f1bf29
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 0 deletions.
8 changes: 8 additions & 0 deletions canvas/gradient.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ func (g *LinearGradient) Move(pos fyne.Position) {
repaint(g)
}

func (g *LinearGradient) IsCacheExpired(size fyne.Size) bool {
return g.Size() != size
}

// Refresh causes this gradient to be redrawn with its configured state.
func (g *LinearGradient) Refresh() {
Refresh(g)
Expand Down Expand Up @@ -136,6 +140,10 @@ func (g *RadialGradient) Move(pos fyne.Position) {
repaint(g)
}

func (g *RadialGradient) IsCacheExpired(size fyne.Size) bool {
return g.Size() != size
}

// Refresh causes this gradient to be redrawn with its configured state.
func (g *RadialGradient) Refresh() {
Refresh(g)
Expand Down
21 changes: 21 additions & 0 deletions canvas/gradient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"image/draw"
"testing"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
internalTest "fyne.io/fyne/v2/internal/test"
"fyne.io/fyne/v2/test"
Expand Down Expand Up @@ -224,6 +225,16 @@ func TestNewLinearGradient_315(t *testing.T) {
assert.Equal(t, color.NRGBA{0, 0, 0, 0x7f}, img.At(0, 50))
}

func TestLinearGradient_IsCacheExpired(t *testing.T) {
gradient := canvas.NewLinearGradient(color.Black, color.Transparent, 45.0)
origSize := gradient.Size()
newSize := fyne.NewSize(100, 100)
gradient.Resize(newSize)

assert.True(t, gradient.IsCacheExpired(origSize))
assert.False(t, gradient.IsCacheExpired(newSize))
}

func TestNewRadialGradient(t *testing.T) {
circle := canvas.NewRadialGradient(color.Black, color.Transparent)

Expand Down Expand Up @@ -291,6 +302,16 @@ func TestNewRadialGradient(t *testing.T) {
assert.Equal(t, color.NRGBA{0, 0, 0, 0x83}, imgCircleOffset.At(1, 5))
}

func TestRadialGradient_IsCacheExpired(t *testing.T) {
gradient := canvas.NewRadialGradient(color.Black, color.Transparent)
origSize := gradient.Size()
newSize := fyne.NewSize(100, 100)
gradient.Resize(newSize)

assert.True(t, gradient.IsCacheExpired(origSize))
assert.False(t, gradient.IsCacheExpired(newSize))
}

func TestGradient_colorComputation(t *testing.T) {
bg := internalTest.NewCheckedImage(50, 50, 1, 2)
bounds := image.Rect(0, 0, 49, 49)
Expand Down
6 changes: 6 additions & 0 deletions canvasobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,9 @@ type Tabbable interface {
type Tappable interface {
Tapped(*PointEvent)
}

// CacheExpirable describes any CanvasObject that can expire its cache.
// Its methods could gain more arguments, as objects require other information to determine if the cache is expired.
type CacheExpirable interface {
IsCacheExpired(Size) bool
}
45 changes: 45 additions & 0 deletions internal/cache/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,51 @@ func (r *dummyWidgetRenderer) Objects() []fyne.CanvasObject {
func (r *dummyWidgetRenderer) Refresh() {
}

type dummyObject struct {
size fyne.Size
pos fyne.Position
hidden bool
}

func (d *dummyObject) Hide() {
d.hidden = true
}

func (d *dummyObject) MinSize() fyne.Size {
return fyne.NewSize(5, 5)
}

func (d *dummyObject) Move(pos fyne.Position) {
d.pos = pos
}

func (d *dummyObject) Position() fyne.Position {
return d.pos
}

func (d *dummyObject) Refresh() {
}

func (d *dummyObject) Resize(size fyne.Size) {
d.size = size
}

func (d *dummyObject) Show() {
d.hidden = false
}

func (d *dummyObject) Size() fyne.Size {
return d.size
}

func (d *dummyObject) Visible() bool {
return !d.hidden
}

func (d *dummyObject) IsCacheExpired(size fyne.Size) bool {
return d.Size() != size
}

type timeMock struct {
now time.Time
}
Expand Down
9 changes: 9 additions & 0 deletions internal/cache/texture_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ func GetTexture(obj fyne.CanvasObject) (TextureType, bool) {
return NoTexture, false
}
texInfo := t.(*textureInfo)
if ce, ok := obj.(fyne.CacheExpirable); ok {
if ce.IsCacheExpired(texInfo.size) {
return NoTexture, false
}
}
texInfo.setAlive()
return texInfo.texture, true
}
Expand Down Expand Up @@ -57,6 +62,9 @@ func RangeTexturesFor(canvas fyne.Canvas, f func(fyne.CanvasObject)) {
func SetTexture(obj fyne.CanvasObject, texture TextureType, canvas fyne.Canvas) {
texInfo := &textureInfo{texture: texture}
texInfo.canvas = canvas
if obj != nil {
texInfo.size = obj.Size()
}
texInfo.setAlive()
textures.Store(obj, texInfo)
}
Expand All @@ -65,4 +73,5 @@ func SetTexture(obj fyne.CanvasObject, texture TextureType, canvas fyne.Canvas)
type textureCacheBase struct {
expiringCacheNoLock
canvas fyne.Canvas
size fyne.Size
}
35 changes: 35 additions & 0 deletions internal/cache/texture_common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cache

import (
"testing"

"fyne.io/fyne/v2"
"github.com/stretchr/testify/assert"
)

func TestGetTexture_noCache(t *testing.T) {
obj := &dummyObject{}
texture, ok := GetTexture(obj)
assert.False(t, ok)
assert.Equal(t, texture, NoTexture)
}

func TestGetTexture_existingCache(t *testing.T) {
obj := &dummyObject{}
origTexture := TextureType(42)

Check failure on line 19 in internal/cache/texture_common_test.go

View workflow job for this annotation

GitHub Actions / mobile_tests (1.19.x)

cannot convert 42 (untyped int constant) to type gl.Texture

Check failure on line 19 in internal/cache/texture_common_test.go

View workflow job for this annotation

GitHub Actions / mobile_tests (1.21.x)

cannot convert 42 (untyped int constant) to type gl.Texture
SetTexture(obj, origTexture, nil)
texture, ok := GetTexture(obj)
assert.True(t, ok)
assert.Equal(t, texture, origTexture)
}

func TestGetTexture_expiredCache(t *testing.T) {
obj := &dummyObject{}
origTexture := TextureType(42)

Check failure on line 28 in internal/cache/texture_common_test.go

View workflow job for this annotation

GitHub Actions / mobile_tests (1.19.x)

cannot convert 42 (untyped int constant) to type gl.Texture

Check failure on line 28 in internal/cache/texture_common_test.go

View workflow job for this annotation

GitHub Actions / mobile_tests (1.21.x)

cannot convert 42 (untyped int constant) to type gl.Texture
SetTexture(obj, origTexture, nil)

obj.Resize(fyne.NewSize(100, 100))
texture, ok := GetTexture(obj)
assert.False(t, ok)
assert.Equal(t, texture, NoTexture)
}

0 comments on commit 6f1bf29

Please sign in to comment.