Skip to content

Commit

Permalink
Merge pull request fyne-io#4906 from andydotxyz/feature/textfont
Browse files Browse the repository at this point in the history
Add support for setting a custom resource as the font source for text
  • Loading branch information
andydotxyz authored Jun 10, 2024
2 parents 832fe62 + 17e7b10 commit 55b4f95
Show file tree
Hide file tree
Showing 14 changed files with 91 additions and 38 deletions.
10 changes: 9 additions & 1 deletion canvas/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ type Text struct {
Text string // The string content of this Text
TextSize float32 // Size of the text - if the Canvas scale is 1.0 this will be equivalent to point size
TextStyle fyne.TextStyle // The style of the text content

// FontSource defines a resource that can be used instead of the theme for looking up the font.
// When a font source is set the `TextStyle` may not be effective, as it will be limited to the styles
// present in the data provided.
//
// Since: 2.5
FontSource fyne.Resource
}

// Hide will set this text to not be visible
Expand All @@ -33,7 +40,8 @@ func (t *Text) Hide() {
// MinSize returns the minimum size of this text object based on its font size and content.
// This is normally determined by the render implementation.
func (t *Text) MinSize() fyne.Size {
return fyne.MeasureText(t.Text, t.TextSize, t.TextStyle)
s, _ := fyne.CurrentApp().Driver().RenderedTextSize(t.Text, t.TextSize, t.TextStyle, t.FontSource)
return s
}

// Move the text to a new position, relative to its parent / canvas
Expand Down
17 changes: 17 additions & 0 deletions canvas/text_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package canvas_test

import (
"image"
"image/color"
"testing"

Expand All @@ -12,6 +13,22 @@ import (
"github.com/stretchr/testify/assert"
)

func TestText_FontSource(t *testing.T) {
text := canvas.NewText("Test", color.NRGBA{0, 0, 0, 0xff})
c := test.NewWindow(text).Canvas()

text.FontSource = test.Theme().Font(fyne.TextStyle{Bold: true})
img1 := c.Capture()
text.FontSource = test.Theme().Font(fyne.TextStyle{Italic: true})
img2 := c.Capture()
assert.NotEqual(t, img1.(*image.NRGBA).Pix, img2.(*image.NRGBA).Pix)

text.FontSource = fyne.NewStaticResource("corrupt", []byte{})
assert.NotPanics(t, func() {
test.NewWindow(text).Canvas().Capture()
})
}

func TestText_MinSize(t *testing.T) {
text := canvas.NewText("Test", color.NRGBA{0, 0, 0, 0xff})
min := text.MinSize()
Expand Down
3 changes: 2 additions & 1 deletion driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ type Driver interface {

// RenderedTextSize returns the size required to render the given string of specified
// font size and style. It also returns the height to text baseline, measured from the top.
RenderedTextSize(text string, fontSize float32, style TextStyle) (size Size, baseline float32)
// If the source is specified it will be used, otherwise the current theme will be asked for the font.
RenderedTextSize(text string, fontSize float32, style TextStyle, source Resource) (size Size, baseline float32)

// CanvasForObject returns the canvas that is associated with a given CanvasObject.
CanvasForObject(CanvasObject) Canvas
Expand Down
23 changes: 16 additions & 7 deletions internal/cache/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ type fontMetric struct {
}

type fontSizeEntry struct {
text string
size float32
style fyne.TextStyle
text string
size float32
style fyne.TextStyle
custom string
}

// GetFontMetrics looks up a calculated size and baseline required for the specified text parameters.
func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle) (size fyne.Size, base float32) {
ent := fontSizeEntry{text, fontSize, style}
func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, base float32) {
name := ""
if source != nil {
name = source.Name()
}
ent := fontSizeEntry{text, fontSize, style, name}
fontSizeLock.RLock()
ret, ok := fontSizeCache[ent]
fontSizeLock.RUnlock()
Expand All @@ -38,8 +43,12 @@ func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle) (size f
}

// SetFontMetrics stores a calculated font size and baseline for parameters that were missing from the cache.
func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, size fyne.Size, base float32) {
ent := fontSizeEntry{text, fontSize, style}
func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource, size fyne.Size, base float32) {
name := ""
if source != nil {
name = source.Name()
}
ent := fontSizeEntry{text, fontSize, style, name}
metric := &fontMetric{size: size, baseLine: base}
metric.setAlive()
fontSizeLock.Lock()
Expand Down
6 changes: 3 additions & 3 deletions internal/cache/text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ func TestTextCacheGet(t *testing.T) {
ResetThemeCaches()
assert.Equal(t, 0, len(fontSizeCache))

bound, base := GetFontMetrics("hi", 10, fyne.TextStyle{})
bound, base := GetFontMetrics("hi", 10, fyne.TextStyle{}, nil)
assert.True(t, bound.IsZero())
assert.Equal(t, float32(0), base)

SetFontMetrics("hi", 10, fyne.TextStyle{}, fyne.NewSize(10, 10), 8)
SetFontMetrics("hi", 10, fyne.TextStyle{}, nil, fyne.NewSize(10, 10), 8)
assert.Equal(t, 1, len(fontSizeCache))

bound, base = GetFontMetrics("hi", 10, fyne.TextStyle{})
bound, base = GetFontMetrics("hi", 10, fyne.TextStyle{}, nil)
assert.Equal(t, fyne.NewSize(10, 10), bound)
assert.Equal(t, float32(8), base)
}
4 changes: 2 additions & 2 deletions internal/driver/glfw/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ func toOSIcon(icon []byte) ([]byte, error) {
return buf.Bytes(), nil
}

func (d *gLDriver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle) (size fyne.Size, baseline float32) {
return painter.RenderedTextSize(text, textSize, style)
func (d *gLDriver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) {
return painter.RenderedTextSize(text, textSize, style, source)
}

func (d *gLDriver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas {
Expand Down
4 changes: 2 additions & 2 deletions internal/driver/mobile/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ func (d *driver) currentWindow() *window {
return last
}

func (d *driver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle) (size fyne.Size, baseline float32) {
return painter.RenderedTextSize(text, textSize, style)
func (d *driver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) {
return painter.RenderedTextSize(text, textSize, style, source)
}

func (d *driver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas {
Expand Down
35 changes: 26 additions & 9 deletions internal/painter/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,26 @@ func lookupFaces(theme, fallback fyne.Resource, family string, style fyne.TextSt
}

// CachedFontFace returns a Font face held in memory. These are loaded from the current theme.
func CachedFontFace(style fyne.TextStyle, fontDP float32, texScale float32) *FontCacheItem {
func CachedFontFace(style fyne.TextStyle, source fyne.Resource, fontDP float32, texScale float32) *FontCacheItem {
if source != nil {
val, ok := fontCustomCache.Load(source)
if !ok {
face := loadMeasureFont(source)
if face == nil {
face = loadMeasureFont(theme.TextFont())
}
faces := &dynamicFontMap{family: source.Name(), faces: []font.Face{face}}

val = &FontCacheItem{Fonts: faces}
fontCustomCache.Store(source, val)
}
return val.(*FontCacheItem)
}

val, ok := fontCache.Load(style)
if !ok {
var faces *dynamicFontMap

switch {
case style.Monospace:
faces = lookupFaces(theme.TextMonospaceFont(), theme.DefaultTextMonospaceFont(), fontscan.Monospace, style)
Expand Down Expand Up @@ -137,8 +153,8 @@ func CachedFontFace(style fyne.TextStyle, fontDP float32, texScale float32) *Fon

// ClearFontCache is used to remove cached fonts in the case that we wish to re-load Font faces
func ClearFontCache() {

fontCache = &sync.Map{}
fontCustomCache = &sync.Map{}
}

// DrawString draws a string into an image.
Expand Down Expand Up @@ -184,14 +200,14 @@ func MeasureString(f shaping.Fontmap, s string, textSize float32, style fyne.Tex

// RenderedTextSize looks up how big a string would be if drawn on screen.
// It also returns the distance from top to the text baseline.
func RenderedTextSize(text string, fontSize float32, style fyne.TextStyle) (size fyne.Size, baseline float32) {
size, base := cache.GetFontMetrics(text, fontSize, style)
func RenderedTextSize(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) {
size, base := cache.GetFontMetrics(text, fontSize, style, source)
if base != 0 {
return size, base
}

size, base = measureText(text, fontSize, style)
cache.SetFontMetrics(text, fontSize, style, size, base)
size, base = measureText(text, fontSize, style, source)
cache.SetFontMetrics(text, fontSize, style, source, size, base)
return size, base
}

Expand All @@ -203,8 +219,8 @@ func float32ToFixed266(f float32) fixed.Int26_6 {
return fixed.Int26_6(float64(f) * (1 << 6))
}

func measureText(text string, fontSize float32, style fyne.TextStyle) (fyne.Size, float32) {
face := CachedFontFace(style, fontSize, 1)
func measureText(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (fyne.Size, float32) {
face := CachedFontFace(style, source, fontSize, 1)
return MeasureString(face.Fonts, text, fontSize, style)
}

Expand Down Expand Up @@ -313,7 +329,8 @@ type FontCacheItem struct {
Fonts shaping.Fontmap
}

var fontCache = &sync.Map{} // map[fyne.TextStyle]*FontCacheItem
var fontCache = &sync.Map{} // map[fyne.TextStyle]*FontCacheItem
var fontCustomCache = &sync.Map{} // map[string]*FontCacheItem for custom resources

type noopLogger struct{}

Expand Down
10 changes: 5 additions & 5 deletions internal/painter/font_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestCachedFontFace(t *testing.T) {
},
} {
t.Run(name, func(t *testing.T) {
got := painter.CachedFontFace(tt.style, 14, 1)
got := painter.CachedFontFace(tt.style, nil, 14, 1)
for _, r := range tt.runes {
f := got.Fonts.ResolveFace(r)
assert.NotNil(t, f, "symbol Font should include: %c", r)
Expand Down Expand Up @@ -77,7 +77,7 @@ func TestDrawString(t *testing.T) {
} {
t.Run(name, func(t *testing.T) {
img := image.NewNRGBA(image.Rect(0, 0, 300, 100))
f := painter.CachedFontFace(tt.style, tt.size, 1)
f := painter.CachedFontFace(tt.style, nil, tt.size, 1)

fontMap := &intTest.FontMap{f.Fonts.ResolveFace(' ')} // first (ascii) font
painter.DrawString(img, tt.string, tt.color, fontMap, tt.size, 1, fyne.TextStyle{TabWidth: tt.tabWidth})
Expand Down Expand Up @@ -117,7 +117,7 @@ func TestMeasureString(t *testing.T) {
},
} {
t.Run(name, func(t *testing.T) {
faces := painter.CachedFontFace(tt.style, tt.size, 1)
faces := painter.CachedFontFace(tt.style, nil, tt.size, 1)
fontMap := &intTest.FontMap{faces.Fonts.ResolveFace(' ')} // first (ascii) font
got, _ := painter.MeasureString(fontMap, tt.string, tt.size, fyne.TextStyle{TabWidth: tt.tabWidth})
assert.Equal(t, tt.want, got.Width)
Expand All @@ -126,8 +126,8 @@ func TestMeasureString(t *testing.T) {
}

func TestRenderedTextSize(t *testing.T) {
size1, baseline1 := painter.RenderedTextSize("Hello World!", 20, fyne.TextStyle{})
size2, baseline2 := painter.RenderedTextSize("\rH\re\rl\rl\ro\r \rW\ro\rr\rl\rd\r!\r", 20, fyne.TextStyle{})
size1, baseline1 := painter.RenderedTextSize("Hello World!", 20, fyne.TextStyle{}, nil)
size2, baseline2 := painter.RenderedTextSize("\rH\re\rl\rl\ro\r \rW\ro\rr\rl\rd\r!\r", 20, fyne.TextStyle{}, nil)
assert.Equal(t, int(size1.Width), int(size2.Width))
assert.Equal(t, size1.Height, size2.Height)
assert.Equal(t, baseline1, baseline2)
Expand Down
2 changes: 1 addition & 1 deletion internal/painter/gl/texture.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (p *painter) newGlTextTexture(obj fyne.CanvasObject) Texture {
height := int(math.Ceil(float64(p.textureScale(bounds.Height))))
img := image.NewNRGBA(image.Rect(0, 0, width, height))

face := paint.CachedFontFace(text.TextStyle, text.TextSize*p.canvas.Scale(), p.texScale)
face := paint.CachedFontFace(text.TextStyle, text.FontSource, text.TextSize*p.canvas.Scale(), p.texScale)
paint.DrawString(img, text.Text, color, face.Fonts, text.TextSize, p.pixScale, text.TextStyle)
return p.imgToTexture(img, canvas.ImageScaleSmooth)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/painter/software/draw.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func drawText(c fyne.Canvas, text *canvas.Text, pos fyne.Position, base *image.N
color = theme.ForegroundColor()
}

face := painter.CachedFontFace(text.TextStyle, text.TextSize*c.Scale(), 1)
face := painter.CachedFontFace(text.TextStyle, text.FontSource, text.TextSize*c.Scale(), 1)
painter.DrawString(txtImg, text.Text, color, face.Fonts, text.TextSize, c.Scale(), text.TextStyle)

size := text.Size()
Expand Down
4 changes: 2 additions & 2 deletions test/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ func (d *driver) Device() fyne.Device {
}

// RenderedTextSize looks up how bit a string would be if drawn on screen
func (d *driver) RenderedTextSize(text string, size float32, style fyne.TextStyle) (fyne.Size, float32) {
return painter.RenderedTextSize(text, size, style)
func (d *driver) RenderedTextSize(text string, size float32, style fyne.TextStyle, source fyne.Resource) (fyne.Size, float32) {
return painter.RenderedTextSize(text, size, style, source)
}

func (d *driver) Run() {
Expand Down
3 changes: 2 additions & 1 deletion text.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ type TextStyle struct {
}

// MeasureText uses the current driver to calculate the size of text when rendered.
// The font used will be read from the current app's theme.
func MeasureText(text string, size float32, style TextStyle) Size {
s, _ := CurrentApp().Driver().RenderedTextSize(text, size, style)
s, _ := CurrentApp().Driver().RenderedTextSize(text, size, style, nil)
return s
}
6 changes: 3 additions & 3 deletions widget/richtext.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ func (r *textRenderer) layoutRow(texts []fyne.CanvasObject, align fyne.TextAlign
for i, text := range texts {
var size fyne.Size
if txt, ok := text.(*canvas.Text); ok {
s, base := fyne.CurrentApp().Driver().RenderedTextSize(txt.Text, txt.TextSize, txt.TextStyle)
s, base := fyne.CurrentApp().Driver().RenderedTextSize(txt.Text, txt.TextSize, txt.TextStyle, txt.FontSource)
if base > tallestBaseline {
if tallestBaseline > 0 {
realign = true
Expand All @@ -799,7 +799,7 @@ func (r *textRenderer) layoutRow(texts []fyne.CanvasObject, align fyne.TextAlign
} else if c, ok := text.(*fyne.Container); ok {
wid := c.Objects[0]
if link, ok := wid.(*Hyperlink); ok {
s, base := fyne.CurrentApp().Driver().RenderedTextSize(link.Text, textSize, link.TextStyle)
s, base := fyne.CurrentApp().Driver().RenderedTextSize(link.Text, textSize, link.TextStyle, nil)
if base > tallestBaseline {
if tallestBaseline > 0 {
realign = true
Expand Down Expand Up @@ -1110,7 +1110,7 @@ func splitLines(seg *TextSegment) []rowBoundary {
}

func truncateLimit(s string, text *canvas.Text, limit int, ellipsis []rune) (int, bool) {
face := paint.CachedFontFace(text.TextStyle, text.TextSize, 1.0)
face := paint.CachedFontFace(text.TextStyle, text.FontSource, text.TextSize, 1.0)

runes := []rune(s)
in := shaping.Input{
Expand Down

0 comments on commit 55b4f95

Please sign in to comment.