Skip to content

Commit

Permalink
Merge pull request #4721 from andydotxyz/fix/textoptimisations
Browse files Browse the repository at this point in the history
  • Loading branch information
andydotxyz authored May 23, 2024
2 parents 9810934 + e9f6383 commit fb1b46b
Show file tree
Hide file tree
Showing 21 changed files with 219 additions and 69 deletions.
184 changes: 138 additions & 46 deletions internal/painter/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import (
"github.com/go-text/render"
"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/fontscan"
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/opentype/api/metadata"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/theme"
)

Expand All @@ -26,40 +30,103 @@ const (
fontTabSpaceSize = 10
)

var (
fm *fontscan.FontMap
mapLock = sync.Mutex{}
load sync.Once
)

func loadMap() {
fm = fontscan.NewFontMap(noopLogger{})
err := loadSystemFonts(fm)
if err != nil {
fm = nil // just don't fallback
}
}

func lookupLangFont(family string, aspect metadata.Aspect) font.Face {
mapLock.Lock()
defer mapLock.Unlock()
load.Do(loadMap)
if fm == nil {
return nil
}

fm.SetQuery(fontscan.Query{Families: []string{family}, Aspect: aspect})
l, _ := fontscan.NewLangID(language.Language(lang.SystemLocale().LanguageString()))
return fm.ResolveFaceForLang(l)
}

func lookupRuneFont(r rune, family string, aspect metadata.Aspect) font.Face {
mapLock.Lock()
defer mapLock.Unlock()
load.Do(loadMap)
if fm == nil {
return nil
}

fm.SetQuery(fontscan.Query{Families: []string{family}, Aspect: aspect})
return fm.ResolveFace(r)
}

func lookupFaces(theme, fallback fyne.Resource, family string, style fyne.TextStyle) (faces *dynamicFontMap) {
f1 := loadMeasureFont(theme)
if theme == fallback {
faces = &dynamicFontMap{family: family, faces: []font.Face{f1}}
} else {
f2 := loadMeasureFont(fallback)
faces = &dynamicFontMap{family: family, faces: []font.Face{f1, f2}}
}

aspect := metadata.Aspect{Style: metadata.StyleNormal}
if style.Italic {
aspect.Style = metadata.StyleItalic
}
if style.Bold {
aspect.Weight = metadata.WeightBold
}

local := lookupLangFont(family, aspect)
if local != nil {
faces.addFace(local)
}

return faces
}

// 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 {
val, ok := fontCache.Load(style)
if !ok {
var f1, f2 font.Face
var faces *dynamicFontMap
switch {
case style.Monospace:
f1 = loadMeasureFont(theme.TextMonospaceFont())
f2 = loadMeasureFont(theme.DefaultTextMonospaceFont())
faces = lookupFaces(theme.TextMonospaceFont(), theme.DefaultTextMonospaceFont(), fontscan.Monospace, style)
case style.Bold:
if style.Italic {
f1 = loadMeasureFont(theme.TextBoldItalicFont())
f2 = loadMeasureFont(theme.DefaultTextBoldItalicFont())
faces = lookupFaces(theme.TextBoldItalicFont(), theme.DefaultTextBoldItalicFont(), fontscan.SansSerif, style)
} else {
f1 = loadMeasureFont(theme.TextBoldFont())
f2 = loadMeasureFont(theme.DefaultTextBoldFont())
faces = lookupFaces(theme.TextBoldFont(), theme.DefaultTextBoldFont(), fontscan.SansSerif, style)
}
case style.Italic:
f1 = loadMeasureFont(theme.TextItalicFont())
f2 = loadMeasureFont(theme.DefaultTextItalicFont())
faces = lookupFaces(theme.TextItalicFont(), theme.DefaultTextItalicFont(), fontscan.SansSerif, style)
case style.Symbol:
f1 = loadMeasureFont(theme.SymbolFont())
f2 = loadMeasureFont(theme.DefaultSymbolFont())
th := theme.SymbolFont()
fallback := theme.DefaultSymbolFont()
f1 := loadMeasureFont(th)

if th == fallback {
faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []font.Face{f1}}
} else {
f2 := loadMeasureFont(fallback)
faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []font.Face{f1, f2}}
}
default:
f1 = loadMeasureFont(theme.TextFont())
f2 = loadMeasureFont(theme.DefaultTextFont())
faces = lookupFaces(theme.TextFont(), theme.DefaultTextFont(), fontscan.SansSerif, style)
}

if f1 == nil {
f1 = f2
}
faces := []font.Face{f1, f2}
if emoji := theme.DefaultEmojiFont(); emoji != nil {
faces = append(faces, loadMeasureFont(emoji))
if emoji := theme.DefaultEmojiFont(); !style.Symbol && emoji != nil {
faces.addFace(loadMeasureFont(emoji)) // TODO only one emoji - maybe others too
}
val = &FontCacheItem{Fonts: faces}
fontCache.Store(style, val)
Expand All @@ -75,29 +142,22 @@ func ClearFontCache() {
}

// DrawString draws a string into an image.
func DrawString(dst draw.Image, s string, color color.Color, f []font.Face, fontSize, scale float32, tabWidth int) {
func DrawString(dst draw.Image, s string, color color.Color, f shaping.Fontmap, fontSize, scale float32, style fyne.TextStyle) {
r := render.Renderer{
FontSize: fontSize,
PixScale: scale,
Color: color,
}

// TODO avoid shaping twice!
sh := &shaping.HarfbuzzShaper{}
out := sh.Shape(shaping.Input{
Text: []rune(s),
RunStart: 0,
RunEnd: len(s),
Face: f[0],
Size: fixed.I(int(fontSize * r.PixScale)),
})

advance := float32(0)
y := int(math.Ceil(float64(fixed266ToFloat32(out.LineBounds.Ascent))))
walkString(f, s, float32ToFixed266(fontSize), tabWidth, &advance, scale, func(run shaping.Output, x float32) {
y := math.MinInt
walkString(f, s, float32ToFixed266(fontSize), style, &advance, scale, func(run shaping.Output, x float32) {
if y == math.MinInt {
y = int(math.Ceil(float64(fixed266ToFloat32(run.LineBounds.Ascent) * r.PixScale)))
}
if len(run.Glyphs) == 1 {
if run.Glyphs[0].GlyphID == 0 {
r.DrawStringAt(string([]rune{0xfffd}), dst, int(x), y, f[0])
r.DrawStringAt(string([]rune{0xfffd}), dst, int(x), y, f.ResolveFace(0xfffd))
return
}
}
Expand All @@ -118,8 +178,8 @@ func loadMeasureFont(data fyne.Resource) font.Face {

// MeasureString returns how far dot would advance by drawing s with f.
// Tabs are translated into a dot location change.
func MeasureString(f []font.Face, s string, textSize float32, tabWidth int) (size fyne.Size, advance float32) {
return walkString(f, s, float32ToFixed266(textSize), tabWidth, &advance, 1, func(shaping.Output, float32) {})
func MeasureString(f shaping.Fontmap, s string, textSize float32, style fyne.TextStyle) (size fyne.Size, advance float32) {
return walkString(f, s, float32ToFixed266(textSize), style, &advance, 1, func(shaping.Output, float32) {})
}

// RenderedTextSize looks up how big a string would be if drawn on screen.
Expand All @@ -145,7 +205,7 @@ func float32ToFixed266(f float32) fixed.Int26_6 {

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

func tabStop(spacew, x float32, tabWidth int) float32 {
Expand All @@ -158,7 +218,7 @@ func tabStop(spacew, x float32, tabWidth int) float32 {
return tabw * float32(tabs)
}

func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth int, advance *float32, scale float32,
func walkString(faces shaping.Fontmap, s string, textSize fixed.Int26_6, style fyne.TextStyle, advance *float32, scale float32,
cb func(run shaping.Output, x float32)) (size fyne.Size, base float32) {
s = strings.ReplaceAll(s, "\r", "")

Expand All @@ -168,7 +228,7 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in
RunStart: 0,
RunEnd: 1,
Direction: di.DirectionLTR,
Face: faces[0],
Face: faces.ResolveFace(' '),
Size: textSize,
}
shaper := &shaping.HarfbuzzShaper{}
Expand All @@ -180,7 +240,10 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in

x := float32(0)
spacew := scale * fontTabSpaceSize
ins := shaping.SplitByFontGlyphs(in, faces)
if style.Monospace {
spacew = scale * fixed266ToFloat32(out.Advance)
}
ins := shaping.SplitByFace(in, faces)
for _, in := range ins {
inEnd := in.RunEnd

Expand All @@ -189,10 +252,9 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in
if r == '\t' {
if pending {
in.RunEnd = i
out = shaper.Shape(in)
x = shapeCallback(shaper, in, out, x, scale, cb)
x = shapeCallback(shaper, in, x, scale, cb)
}
x = tabStop(spacew, x, tabWidth)
x = tabStop(spacew, x, style.TabWidth)

in.RunStart = i + 1
in.RunEnd = inEnd
Expand All @@ -202,16 +264,16 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in
}
}

x = shapeCallback(shaper, in, out, x, scale, cb)
x = shapeCallback(shaper, in, x, scale, cb)
}

*advance = x
return fyne.NewSize(*advance, fixed266ToFloat32(out.LineBounds.LineThickness())),
fixed266ToFloat32(out.LineBounds.Ascent)
}

func shapeCallback(shaper shaping.Shaper, in shaping.Input, out shaping.Output, x, scale float32, cb func(shaping.Output, float32)) float32 {
out = shaper.Shape(in)
func shapeCallback(shaper shaping.Shaper, in shaping.Input, x, scale float32, cb func(shaping.Output, float32)) float32 {
out := shaper.Shape(in)
glyphs := out.Glyphs
start := 0
pending := false
Expand Down Expand Up @@ -248,7 +310,37 @@ func shapeCallback(shaper shaping.Shaper, in shaping.Input, out shaping.Output,
}

type FontCacheItem struct {
Fonts []font.Face
Fonts shaping.Fontmap
}

var fontCache = &sync.Map{} // map[fyne.TextStyle]*FontCacheItem

type noopLogger struct{}

func (n noopLogger) Printf(string, ...interface{}) {}

type dynamicFontMap struct {
faces []font.Face
family string
}

func (d *dynamicFontMap) ResolveFace(r rune) font.Face {

for _, f := range d.faces {
if _, ok := f.NominalGlyph(r); ok {
return f
}
}

toAdd := lookupRuneFont(r, d.family, metadata.Aspect{})
if toAdd != nil {
d.addFace(toAdd)
return toAdd
}

return d.faces[0]
}

func (d *dynamicFontMap) addFace(f font.Face) {
d.faces = append(d.faces, f)
}
8 changes: 8 additions & 0 deletions internal/painter/font_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//go:build test

package painter

import "github.com/go-text/typesetting/fontscan"

//
//func Test_compositeFace_Close(t *testing.T) {
// chosenFont := &truetype.Font{}
Expand Down Expand Up @@ -334,3 +338,7 @@ package painter
// f.IndexInvoked = true
// return f.IndexFunc(r)
//}

func loadSystemFonts(fm *fontscan.FontMap) error {
return nil
}
21 changes: 21 additions & 0 deletions internal/painter/font_prod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//go:build !test

package painter

import (
"os"
"path/filepath"
"runtime"

"github.com/go-text/typesetting/fontscan"
)

func loadSystemFonts(fm *fontscan.FontMap) error {
cacheDir := ""
if runtime.GOOS == "android" {
parent := os.Getenv("FILESDIR")
cacheDir = filepath.Join(parent, "fontcache")
}

return fm.UseSystemFonts(cacheDir)
}
14 changes: 9 additions & 5 deletions internal/painter/font_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/internal/painter"
intTest "fyne.io/fyne/v2/internal/test"
"fyne.io/fyne/v2/test"
)

Expand All @@ -28,8 +29,8 @@ func TestCachedFontFace(t *testing.T) {
t.Run(name, func(t *testing.T) {
got := painter.CachedFontFace(tt.style, 14, 1)
for _, r := range tt.runes {
_, ok := got.Fonts[0].NominalGlyph(r)
assert.True(t, ok, "symbol Font should include: %c", r)
f := got.Fonts.ResolveFace(r)
assert.NotNil(t, f, "symbol Font should include: %c", r)
}
})
}
Expand Down Expand Up @@ -77,7 +78,9 @@ 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)
painter.DrawString(img, tt.string, tt.color, f.Fonts, tt.size, 1, tt.tabWidth)

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})
test.AssertImageMatches(t, "font/"+tt.want, img)
})
}
Expand Down Expand Up @@ -114,8 +117,9 @@ func TestMeasureString(t *testing.T) {
},
} {
t.Run(name, func(t *testing.T) {
face := painter.CachedFontFace(tt.style, tt.size, 1)
got, _ := painter.MeasureString(face.Fonts, tt.string, tt.size, tt.tabWidth)
faces := painter.CachedFontFace(tt.style, 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 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 {
img := image.NewNRGBA(image.Rect(0, 0, width, height))

face := paint.CachedFontFace(text.TextStyle, text.TextSize*p.canvas.Scale(), p.texScale)
paint.DrawString(img, text.Text, color, face.Fonts, text.TextSize, p.pixScale, text.TextStyle.TabWidth)
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 @@ -143,7 +143,7 @@ func drawText(c fyne.Canvas, text *canvas.Text, pos fyne.Position, base *image.N
}

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

size := text.Size()
offsetX := float32(0)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit fb1b46b

Please sign in to comment.