go.*,text,font{,/opentype},app,gpu,widget{,/material}: [API] load system fonts

This commit updates the text package to be able to load system fonts. As a consequence,
application authors may choose to provide no fonts manually, and it's
also possible that the system provides none (WASM, for instance, currently provides no
system fonts). As such, the text stack needed some minor tweaks to handle this case by
displaying blank spaces where text should be rather than crashing when no faces are
available.

Internally, we are dropping the old method of choosing faces and instead relying solely
on the new font matching logic in go-text. I chose to do this because maintaining two
different sets of logic with a hierarchical relationship proved to be really complex,
and also the go-text logic seems to produce higher-quality choices.

The breaking API change from this commit is the new way of constructing a text shaper
using text.ShaperOptions. Providing no options will result in a shaper that uses solely
system fonts. The various options can be used to disable system font loading and to
provide an already-parsed collection of fonts as per Gio's old API.

The material.NewTheme function now accepts no arguments instead of a font collection.
Users wanting to provide a collection can simply provide a new shaper configured how
they would like:

    theme := material.NewTheme()
	theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))

This commit touches many packages to fix up their construction of text shapers, mostly in
test code. The changes to the tests in package widget deserve special note:
Changing our font resolution logic caused the tofu characters within the
test strings to use a different font's tofu. This isn't a problem, but shifted
the layout of the shaped text a little bit. I've updated the numbers to expect
the new glyph positions.

Fixes: https://todo.sr.ht/~eliasnaur/gio/309
Fixes: https://todo.sr.ht/~eliasnaur/gio/184
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-06-14 09:54:55 -04:00
committed by Elias Naur
parent babe7a292b
commit 43c47f0883
16 changed files with 327 additions and 347 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ func FuzzIME(f *testing.F) {
f.Add([]byte("20007800002\x02000"))
f.Add([]byte("200A02000990\x19002\x17\x0200"))
f.Fuzz(func(t *testing.T, cmds []byte) {
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.WithCollection(gofont.Collection()))
e := new(widget.Editor)
e.Focus()
+3 -1
View File
@@ -26,6 +26,7 @@ import (
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
@@ -140,7 +141,8 @@ func NewWindow(options ...Option) *Window {
debug.Parse()
// Measure decoration height.
deco := new(widget.Decorations)
theme := material.NewTheme(gofont.Regular())
theme := material.NewTheme()
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
decoStyle := material.Decorations(theme, deco, 0, "")
gtx := layout.Context{
Ops: new(op.Ops),
+3 -1
View File
@@ -3,7 +3,9 @@ Package font provides type describing font faces attributes.
*/
package font
import "github.com/go-text/typesetting/font"
import (
"github.com/go-text/typesetting/font"
)
// A FontFace is a Font and a matching Face.
type FontFace struct {
+1 -1
View File
@@ -6,7 +6,7 @@ require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.6
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433
github.com/go-text/typesetting v0.0.0-20230717141307-09c70c30a055
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.5.0
+11 -3
View File
@@ -5,9 +5,17 @@ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJG
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433 h1:Pdyvqsfi1QYgFfZa4R8otBOtgO+CGyBDMEG8cM3jwvE=
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA=
github.com/go-text/typesetting-utils v0.0.0-20230412163830-89e4bcfa3ecc h1:9Kf84pnrmmjdRzZIkomfjowmGUhHs20jkrWYw/I6CYc=
github.com/go-text/typesetting v0.0.0-20230707164345-04125e0ababa h1:Xmgc5QeEosRYq+xA98softsWg59xsxTxVkz3H0/YGrg=
github.com/go-text/typesetting v0.0.0-20230707164345-04125e0ababa/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting v0.0.0-20230710184056-47dfb050bdaf h1:O193R8qIrI3mQ4mvqvY0jQZG34baUYquSMkPVQ1WPKo=
github.com/go-text/typesetting v0.0.0-20230710184056-47dfb050bdaf/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting v0.0.0-20230712125615-69b99aaa3a6e h1:d7u1AMCEr4ICoqZfbPCKOTRr+x3c8mitMgn+JaPPgLU=
github.com/go-text/typesetting v0.0.0-20230712125615-69b99aaa3a6e/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting v0.0.0-20230712171355-59e4e8ecaa7b h1:kNuq6ZZRTU8algnUUBqbwUnlHo1U2mbnpi6SaZPnnn0=
github.com/go-text/typesetting v0.0.0-20230712171355-59e4e8ecaa7b/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting v0.0.0-20230717141307-09c70c30a055 h1:aUv3DYpk2eraRoyB7QZkyxgTVF7DrWcUui93iFRYO+8=
github.com/go-text/typesetting v0.0.0-20230717141307-09c70c30a055/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+3 -1
View File
@@ -15,6 +15,7 @@ import (
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/widget/material"
)
@@ -33,7 +34,8 @@ func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, *material.T
Ops: ops,
Constraints: layout.Exact(sz),
}
th := material.NewTheme(gofont.Collection())
th := material.NewTheme()
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
return gtx, w, th
}
+165 -165
View File
@@ -6,12 +6,15 @@ import (
"bytes"
"image"
"io"
"sort"
"log"
"os"
"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"
"github.com/go-text/typesetting/opentype/api/metadata"
"github.com/go-text/typesetting/shaping"
"golang.org/x/exp/slices"
"golang.org/x/image/math/fixed"
@@ -19,6 +22,8 @@ import (
"gioui.org/f32"
giofont "gioui.org/font"
"gioui.org/font/opentype"
"gioui.org/internal/debug"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/op/clip"
@@ -152,112 +157,17 @@ type runLayout struct {
truncator bool
}
// faceOrderer chooses the order in which faces should be applied to text.
type faceOrderer struct {
def giofont.Font
faceScratch []font.Face
fontDefaultOrder map[giofont.Font]int
defaultOrderedFonts []giofont.Font
faces map[giofont.Font]font.Face
faceToIndex map[font.Face]int
fonts []giofont.Font
}
func (f *faceOrderer) insert(fnt giofont.Font, face font.Face) {
if len(f.fonts) == 0 {
f.def = fnt
}
if f.fontDefaultOrder == nil {
f.fontDefaultOrder = make(map[giofont.Font]int)
}
if f.faces == nil {
f.faces = make(map[giofont.Font]font.Face)
f.faceToIndex = make(map[font.Face]int)
}
f.fontDefaultOrder[fnt] = len(f.faceScratch)
f.defaultOrderedFonts = append(f.defaultOrderedFonts, fnt)
f.faceScratch = append(f.faceScratch, face)
f.fonts = append(f.fonts, fnt)
f.faces[fnt] = face
f.faceToIndex[face] = f.fontDefaultOrder[fnt]
}
// resetFontOrder restores the fonts to a predictable order. It should be invoked
// before any operation searching the fonts.
func (c *faceOrderer) resetFontOrder() {
copy(c.fonts, c.defaultOrderedFonts)
}
func (c *faceOrderer) indexFor(face font.Face) int {
return c.faceToIndex[face]
}
func (c *faceOrderer) faceFor(idx int) font.Face {
if idx < len(c.defaultOrderedFonts) {
return c.faces[c.defaultOrderedFonts[idx]]
}
panic("face index not found")
}
// TODO(whereswaldon): this function could sort all faces by appropriateness for the
// given font characteristics. This would ensure that (if possible) text using a
// fallback font would select similar weights and emphases to the primary font.
func (c *faceOrderer) sortedFacesForStyle(font giofont.Font) []font.Face {
c.resetFontOrder()
primary, ok := c.fontForStyle(font)
if !ok {
font.Typeface = c.def.Typeface
primary, ok = c.fontForStyle(font)
if !ok {
primary = c.def
}
}
return c.sorted(primary)
}
// fontForStyle returns the closest existing font to the requested font within the
// same typeface.
func (c *faceOrderer) fontForStyle(font giofont.Font) (giofont.Font, bool) {
if closest, ok := closestFont(font, c.fonts); ok {
return closest, true
}
font.Style = giofont.Regular
if closest, ok := closestFont(font, c.fonts); ok {
return closest, true
}
return font, false
}
// faces returns a slice of faces with primary as the first element and
// the remaining faces ordered by insertion order.
func (f *faceOrderer) sorted(primary giofont.Font) []font.Face {
// If we find primary, put it first, and omit it from the below sort.
lowest := 0
for i := range f.fonts {
if f.fonts[i] == primary {
if i != 0 {
f.fonts[0], f.fonts[i] = f.fonts[i], f.fonts[0]
}
lowest = 1
break
}
}
sorting := f.fonts[lowest:]
sort.Slice(sorting, func(i, j int) bool {
a := sorting[i]
b := sorting[j]
return f.fontDefaultOrder[a] < f.fontDefaultOrder[b]
})
for i, font := range f.fonts {
f.faceScratch[i] = f.faces[font]
}
return f.faceScratch
}
// shaperImpl implements the shaping and line-wrapping of opentype fonts.
type shaperImpl struct {
// Fields for tracking fonts/faces.
orderer faceOrderer
fontMap *fontscan.FontMap
faces []font.Face
faceToIndex map[font.Font]int
faceMeta []giofont.Font
defaultFaces []string
logger interface {
Printf(format string, args ...any)
}
// Shaping and wrapping state.
shaper shaping.HarfbuzzShaper
@@ -274,11 +184,60 @@ type shaperImpl struct {
bitmapGlyphCache bitmapCache
}
// debugLogger only logs messages if debug.Text is true.
type debugLogger struct {
*log.Logger
}
func newDebugLogger() debugLogger {
return debugLogger{Logger: log.New(log.Writer(), "[text] ", log.Default().Flags())}
}
func (d debugLogger) Printf(format string, args ...any) {
if debug.Text.Load() {
d.Logger.Printf(format, args...)
}
}
func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
var shaper shaperImpl
shaper.logger = newDebugLogger()
shaper.fontMap = fontscan.NewFontMap(shaper.logger)
shaper.faceToIndex = make(map[font.Font]int)
if systemFonts {
str, err := os.UserCacheDir()
if err != nil {
shaper.logger.Printf("failed resolving font cache dir: %v", err)
shaper.logger.Printf("skipping system font load")
}
if err := shaper.fontMap.UseSystemFonts(str); err != nil {
shaper.logger.Printf("failed loading system fonts: %v", err)
}
}
for _, f := range collection {
shaper.Load(f)
shaper.defaultFaces = append(shaper.defaultFaces, string(f.Font.Typeface))
}
shaper.shaper.SetFontCacheSize(32)
return &shaper
}
// Load registers the provided FontFace with the shaper, if it is compatible.
// It returns whether the face is now available for use. FontFaces are prioritized
// in the order in which they are loaded, with the first face being the default.
func (s *shaperImpl) Load(f FontFace) {
s.orderer.insert(f.Font, f.Face.Face())
s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font))
s.addFace(f.Face.Face(), f.Font)
}
func (s *shaperImpl) addFace(f font.Face, md giofont.Font) {
if _, ok := s.faceToIndex[f.Font]; ok {
return
}
idx := len(s.faces)
s.faceToIndex[f.Font] = idx
s.faces = append(s.faces, f)
s.faceMeta = append(s.faceMeta, md)
}
// splitByScript divides the inputs into new, smaller inputs on script boundaries
@@ -362,9 +321,26 @@ func (s *shaperImpl) splitBidi(input shaping.Input) []shaping.Input {
return splitInputs
}
// ResolveFace allows shaperImpl to implement shaping.FontMap, wrapping its fontMap
// field and ensuring that any faces loaded as part of the search are registered with
// ids so that they can be referred to by a GlyphID.
func (s *shaperImpl) ResolveFace(r rune) font.Face {
face := s.fontMap.ResolveFace(r)
if face != nil {
family, aspect := s.fontMap.FontMetadata(face.Font)
md := opentype.DescriptionToFont(metadata.Description{
Family: family,
Aspect: aspect,
})
s.addFace(face, md)
return face
}
return nil
}
// splitByFaces divides the inputs by font coverage in the provided faces. It will use the slice provided in buf
// as the backing storage of the returned slice if buf is non-nil.
func (s *shaperImpl) splitByFaces(inputs []shaping.Input, faces []font.Face, buf []shaping.Input) []shaping.Input {
func (s *shaperImpl) splitByFaces(inputs []shaping.Input, buf []shaping.Input) []shaping.Input {
var split []shaping.Input
if buf == nil {
split = make([]shaping.Input, 0, len(inputs))
@@ -372,34 +348,78 @@ func (s *shaperImpl) splitByFaces(inputs []shaping.Input, faces []font.Face, buf
split = buf
}
for _, input := range inputs {
split = append(split, shaping.SplitByFontGlyphs(input, faces)...)
split = append(split, shaping.SplitByFace(input, s)...)
}
return split
}
// shapeText invokes the text shaper and returns the raw text data in the shaper's native
// format. It does not wrap lines.
func (s *shaperImpl) shapeText(faces []font.Face, ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output {
if len(faces) < 1 {
return nil
}
func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output {
lcfg := langConfig{
Language: language.NewLanguage(lc.Language),
Direction: mapDirection(lc.Direction),
}
// Create an initial input.
input := toInput(faces[0], ppem, lcfg, txt)
input := toInput(nil, ppem, lcfg, txt)
if input.RunStart == input.RunEnd && len(s.faces) > 0 {
// Give the empty string a face. This is a necessary special case because
// the face splitting process works by resolving faces for each rune, and
// the empty string contains no runes.
input.Face = s.faces[0]
}
// Break input on font glyph coverage.
inputs := s.splitBidi(input)
inputs = s.splitByFaces(inputs, faces, s.splitScratch1[:0])
inputs = s.splitByFaces(inputs, s.splitScratch1[:0])
inputs = splitByScript(inputs, lcfg.Direction, s.splitScratch2[:0])
// Shape all inputs.
if needed := len(inputs) - len(s.outScratchBuf); needed > 0 {
s.outScratchBuf = slices.Grow(s.outScratchBuf, needed)
}
s.outScratchBuf = s.outScratchBuf[:len(inputs)]
for i := range inputs {
s.outScratchBuf[i] = s.shaper.Shape(inputs[i])
s.outScratchBuf = s.outScratchBuf[:0]
for _, input := range inputs {
if input.Face != nil {
s.outScratchBuf = append(s.outScratchBuf, s.shaper.Shape(input))
} else {
s.outScratchBuf = append(s.outScratchBuf, shaping.Output{
// Use the text size as the advance of the entire fake run so that
// it doesn't occupy zero space.
Advance: input.Size,
Size: input.Size,
Glyphs: []shaping.Glyph{
{
Width: input.Size,
Height: input.Size,
XBearing: 0,
YBearing: 0,
XAdvance: input.Size,
YAdvance: input.Size,
XOffset: 0,
YOffset: 0,
ClusterIndex: input.RunStart,
RuneCount: input.RunEnd - input.RunStart,
GlyphCount: 1,
GlyphID: 0,
Mask: 0,
},
},
LineBounds: shaping.Bounds{
Ascent: input.Size,
Descent: 0,
Gap: 0,
},
GlyphBounds: shaping.Bounds{
Ascent: input.Size,
Descent: 0,
Gap: 0,
},
Direction: input.Direction,
Runes: shaping.Range{
Offset: input.RunStart,
Count: input.RunEnd - input.RunStart,
},
})
}
}
return s.outScratchBuf
}
@@ -416,22 +436,26 @@ func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy {
}
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
func (s *shaperImpl) shapeAndWrapText(params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
wc := shaping.WrapConfig{
TruncateAfterLines: params.MaxLines,
TextContinues: params.forceTruncate,
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
}
s.fontMap.SetQuery(fontscan.Query{
Families: []string{string(params.Font.Typeface)},
Aspect: opentype.FontToDescription(params.Font).Aspect,
})
if wc.TruncateAfterLines > 0 {
if len(params.Truncator) == 0 {
params.Truncator = "…"
}
// We only permit a single run as the truncator, regardless of whether more were generated.
// Just use the first one.
wc.Truncator = s.shapeText(faces, params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
wc.Truncator = s.shapeText(params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
}
// Wrap outputs into lines.
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(faces, params.PxPerEm, params.Locale, txt)))
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(params.PxPerEm, params.Locale, txt)))
}
// replaceControlCharacters replaces problematic unicode
@@ -494,7 +518,7 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
if hasNewline {
txt = txt[:len(txt)-1]
}
ls, truncated := s.shapeAndWrapText(s.orderer.sortedFacesForStyle(params.Font), params, replaceControlCharacters(txt))
ls, truncated := s.shapeAndWrapText(params, replaceControlCharacters(txt))
didTruncate := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
@@ -508,7 +532,7 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
textLines := make([]line, len(ls))
maxHeight := fixed.Int26_6(0)
for i := range ls {
otLine := toLine(&s.orderer, ls[i], params.Locale.Direction)
otLine := toLine(s.faceToIndex, ls[i], params.Locale.Direction)
if otLine.lineHeight > maxHeight {
maxHeight = otLine.lineHeight
}
@@ -597,7 +621,13 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
x = g.X
}
ppem, faceIdx, gid := splitGlyphID(g.ID)
face := s.orderer.faceFor(faceIdx)
if faceIdx >= len(s.faces) {
continue
}
face := s.faces[faceIdx]
if face == nil {
continue
}
scaleFactor := fixedToFloat(ppem) / float32(face.Upem())
glyphData := face.GlyphData(gid)
switch glyphData := glyphData.(type) {
@@ -671,7 +701,13 @@ func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp {
x = g.X
}
_, faceIdx, gid := splitGlyphID(g.ID)
face := s.orderer.faceFor(faceIdx)
if faceIdx >= len(s.faces) {
continue
}
face := s.faces[faceIdx]
if face == nil {
continue
}
glyphData := face.GlyphData(gid)
switch glyphData := glyphData.(type) {
case api.GlyphBitmap:
@@ -799,7 +835,7 @@ func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph {
}
// toLine converts the output into a Line with the provided dominant text direction.
func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line {
func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirection) line {
if len(o) < 1 {
return line{}
}
@@ -813,8 +849,12 @@ func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line
if run.Size > maxSize {
maxSize = run.Size
}
var font font.Font
if run.Face != nil {
font = run.Face.Font
}
line.runs[i] = runLayout{
Glyphs: toGioGlyphs(run.Glyphs, run.Size, orderer.indexFor(run.Face)),
Glyphs: toGioGlyphs(run.Glyphs, run.Size, faceToIndex[font]),
Runes: Range{
Count: run.Runes.Count,
Offset: line.runeCount,
@@ -916,43 +956,3 @@ func computeVisualOrder(l *line) {
x += l.runs[runIdx].Advance
}
}
// closestFont returns the closest Font in available by weight.
// In case of equality the lighter weight will be returned.
func closestFont(lookup giofont.Font, available []giofont.Font) (giofont.Font, bool) {
found := false
var match giofont.Font
for _, cf := range available {
if cf == lookup {
return lookup, true
}
if cf.Typeface != lookup.Typeface || cf.Style != lookup.Style {
continue
}
if !found {
found = true
match = cf
continue
}
cDist := weightDistance(lookup.Weight, cf.Weight)
mDist := weightDistance(lookup.Weight, match.Weight)
if cDist < mDist {
match = cf
} else if cDist == mDist && cf.Weight < match.Weight {
match = cf
}
}
return match, found
}
// weightDistance returns the distance value between two font weights.
func weightDistance(wa giofont.Weight, wb giofont.Weight) int {
// Avoid dealing with negative Weight values.
a := int(wa) + 400
b := int(wb) + 400
diff := a - b
if diff < 0 {
return -diff
}
return diff
}
+19 -106
View File
@@ -30,11 +30,12 @@ var arabic = system.Locale{
}
func testShaper(faces ...giofont.Face) *shaperImpl {
shaper := shaperImpl{}
ff := make([]FontFace, 0, len(faces))
for _, face := range faces {
shaper.Load(FontFace{Face: face})
ff = append(ff, FontFace{Face: face})
}
return &shaper
shaper := newShaperImpl(false, ff)
return shaper
}
func TestEmptyString(t *testing.T) {
@@ -64,6 +65,18 @@ func TestEmptyString(t *testing.T) {
}
}
func TestNoFaces(t *testing.T) {
ppem := fixed.I(200)
shaper := testShaper()
// Ensure shaping text with no faces does not panic.
shaper.LayoutRunes(Parameters{
PxPerEm: ppem,
MaxWidth: 2000,
Locale: english,
}, []rune("✨ⷽℎ↞⋇ⱜ⪫⢡⽛⣦␆Ⱨⳏ⳯⒛⭣╎⌞⟻⢇┃➡⬎⩱⸇ⷎ⟅▤⼶⇺⩳⎏⤬⬞ⴈ⋠⿶⢒₍☟⽂ⶦ⫰⭢⌹∼▀⾯⧂❽⩏ⓖ⟅⤔⍇␋⽓ₑ⢳⠑❂⊪⢘⽨⃯▴ⷿ"))
}
func TestAlignWidth(t *testing.T) {
lines := []line{
{width: fixed.I(50)},
@@ -288,13 +301,13 @@ func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize,
rtlSource = string(complexRunes[:runeLimit])
}
}
simpleText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
simpleText, _ := shaper.shapeAndWrapText(Parameters{
PxPerEm: fixed.I(fontSize),
MaxWidth: lineWidth,
Locale: locale,
}, []rune(simpleSource))
simpleText = copyLines(simpleText)
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
complexText, _ := shaper.shapeAndWrapText(Parameters{
PxPerEm: fixed.I(fontSize),
MaxWidth: lineWidth,
Locale: locale,
@@ -378,7 +391,7 @@ func TestToLine(t *testing.T) {
totalInputGlyphs += len(run.Glyphs)
totalInputRunes += run.Runes.Count
}
output := toLine(&shaper.orderer, input, tc.dir)
output := toLine(shaper.faceToIndex, input, tc.dir)
if output.bounds.Min == (fixed.Point26_6{}) {
t.Errorf("line %d: Bounds.Min not populated", i)
}
@@ -662,106 +675,6 @@ func TestTextAppend(t *testing.T) {
}
}
func TestClosestFontByWeight(t *testing.T) {
const (
testTF1 giofont.Typeface = "MockFace"
testTF2 giofont.Typeface = "TestFace"
testTF3 giofont.Typeface = "AnotherFace"
)
fonts := []giofont.Font{
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Normal},
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Bold},
{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Thin},
}
weightOnlyTests := []struct {
Lookup giofont.Weight
Expected giofont.Weight
}{
// Test for existing weights.
{Lookup: giofont.Normal, Expected: giofont.Normal},
{Lookup: giofont.Light, Expected: giofont.Light},
{Lookup: giofont.Bold, Expected: giofont.Bold},
// Test for missing weights.
{Lookup: giofont.Thin, Expected: giofont.Light},
{Lookup: giofont.ExtraLight, Expected: giofont.Light},
{Lookup: giofont.Medium, Expected: giofont.Normal},
{Lookup: giofont.SemiBold, Expected: giofont.Bold},
{Lookup: giofont.ExtraBold, Expected: giofont.Bold},
}
for _, test := range weightOnlyTests {
got, ok := closestFont(giofont.Font{Typeface: testTF1, Weight: test.Lookup}, fonts)
if !ok {
t.Errorf("expected closest font for %v to exist", test.Lookup)
}
if got.Weight != test.Expected {
t.Errorf("got weight %v, expected %v", got.Weight, test.Expected)
}
}
fonts = []giofont.Font{
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Bold},
{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Bold},
}
otherTests := []struct {
Lookup giofont.Font
Expected giofont.Font
ExpectedToFail bool
}{
// Test for existing fonts.
{
Lookup: giofont.Font{Typeface: testTF1, Weight: giofont.Light},
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
},
{
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
},
// Test for missing fonts.
{
Lookup: giofont.Font{Typeface: testTF1, Weight: giofont.Normal},
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
},
{
Lookup: giofont.Font{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Normal},
Expected: giofont.Font{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Bold},
},
{
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Thin},
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
},
{
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Bold},
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
},
{
Lookup: giofont.Font{Typeface: testTF2, Weight: giofont.Normal},
ExpectedToFail: true,
},
{
Lookup: giofont.Font{Typeface: testTF2, Style: giofont.Italic, Weight: giofont.Normal},
ExpectedToFail: true,
},
}
for _, test := range otherTests {
got, ok := closestFont(test.Lookup, fonts)
if test.ExpectedToFail {
if ok {
t.Errorf("expected closest font for %v to not exist", test.Lookup)
} else {
continue
}
}
if !ok {
t.Errorf("expected closest font for %v to exist", test.Lookup)
}
if got != test.Expected {
t.Errorf("got %v, expected %v", got, test.Expected)
}
}
}
func TestGlyphIDPacking(t *testing.T) {
const maxPPem = fixed.Int26_6((1 << sizebits) - 1)
type testcase struct {
+40 -5
View File
@@ -201,6 +201,11 @@ type GlyphID uint64
// Shaper converts strings of text into glyphs that can be displayed.
type Shaper struct {
config struct {
disableSystemFonts bool
collection []FontFace
}
initialized bool
shaper shaperImpl
pathCache pathCache
bitmapShapeCache bitmapShapeCache
@@ -223,26 +228,53 @@ type Shaper struct {
err error
}
// ShaperOptions configure text shapers.
type ShaperOption func(*Shaper)
// NoSystemFonts can be used to disable system font loading.
func NoSystemFonts() ShaperOption {
return func(s *Shaper) {
s.config.disableSystemFonts = true
}
}
// WithCollection can be used to provide a collection of pre-loaded fonts to the shaper.
func WithCollection(collection []FontFace) ShaperOption {
return func(s *Shaper) {
s.config.collection = collection
}
}
// NewShaper constructs a shaper with the provided collection of font faces
// available.
func NewShaper(collection []FontFace) *Shaper {
func NewShaper(options ...ShaperOption) *Shaper {
l := &Shaper{}
for _, f := range collection {
l.shaper.Load(f)
for _, opt := range options {
opt(l)
}
l.shaper.shaper.SetFontCacheSize(32)
l.reader = bufio.NewReader(nil)
l.init()
return l
}
func (l *Shaper) init() {
if l.initialized {
return
}
l.initialized = true
l.reader = bufio.NewReader(nil)
l.shaper = *newShaperImpl(!l.config.disableSystemFonts, l.config.collection)
}
// Layout text from an io.Reader according to a set of options. Results can be retrieved by
// iteratively calling NextGlyph.
func (l *Shaper) Layout(params Parameters, txt io.Reader) {
l.init()
l.layoutText(params, txt, "")
}
// LayoutString is Layout for strings.
func (l *Shaper) LayoutString(params Parameters, str string) {
l.init()
l.layoutText(params, nil, str)
}
@@ -368,6 +400,7 @@ func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte
// NextGlyph returns the next glyph from the most recent shaping operation, if
// any. If there are no more glyphs, ok will be false.
func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
l.init()
if l.done {
return Glyph{}, false
}
@@ -535,6 +568,7 @@ func splitGlyphID(g GlyphID) (fixed.Int26_6, int, font.GID) {
// of all vector glyphs.
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
l.init()
key := l.pathCache.hashGlyphs(gs)
shape, ok := l.pathCache.Get(key, gs)
if ok {
@@ -551,6 +585,7 @@ func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
// same gs slice.
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
func (l *Shaper) Bitmaps(gs []Glyph) op.CallOp {
l.init()
key := l.bitmapShapeCache.hashGlyphs(gs)
call, ok := l.bitmapShapeCache.Get(key, gs)
if ok {
+7 -7
View File
@@ -22,7 +22,7 @@ func TestWrappingTruncation(t *testing.T) {
textInput := "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua.\n"
ltrFace, _ := opentype.Parse(goregular.TTF)
collection := []FontFace{{Face: ltrFace}}
cache := NewShaper(collection)
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
cache.LayoutString(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
@@ -89,7 +89,7 @@ func TestWrappingForcedTruncation(t *testing.T) {
textInput := "Lorem ipsum\ndolor sit\namet"
ltrFace, _ := opentype.Parse(goregular.TTF)
collection := []FontFace{{Face: ltrFace}}
cache := NewShaper(collection)
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
cache.LayoutString(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
@@ -165,7 +165,7 @@ func TestShapingNewlineHandling(t *testing.T) {
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) {
ltrFace, _ := opentype.Parse(goregular.TTF)
collection := []FontFace{{Face: ltrFace}}
cache := NewShaper(collection)
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
checkGlyphs := func() {
glyphs := []Glyph{}
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
@@ -234,7 +234,7 @@ func TestShapingNewlineHandling(t *testing.T) {
func TestCacheEmptyString(t *testing.T) {
ltrFace, _ := opentype.Parse(goregular.TTF)
collection := []FontFace{{Face: ltrFace}}
cache := NewShaper(collection)
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
cache.LayoutString(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
@@ -273,7 +273,7 @@ func TestCacheEmptyString(t *testing.T) {
func TestCacheAlignment(t *testing.T) {
ltrFace, _ := opentype.Parse(goregular.TTF)
collection := []FontFace{{Face: ltrFace}}
cache := NewShaper(collection)
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
params := Parameters{
Alignment: Start,
PxPerEm: fixed.I(10),
@@ -339,7 +339,7 @@ func TestCacheGlyphConverstion(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
cache := NewShaper(collection)
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
cache.LayoutString(Parameters{
PxPerEm: fixed.I(10),
MaxWidth: 200,
@@ -481,7 +481,7 @@ func TestShapeStringRuneAccounting(t *testing.T) {
},
} {
t.Run(setup.kind, func(t *testing.T) {
shaper := NewShaper(gofont.Collection())
shaper := NewShaper(NoSystemFonts(), WithCollection(gofont.Collection()))
setup.do(shaper, tc.params, tc.input)
glyphs := []Glyph{}
+16 -16
View File
@@ -108,7 +108,7 @@ func TestEditorReadOnly(t *testing.T) {
key.FocusEvent{Focus: true},
},
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e := new(Editor)
@@ -187,7 +187,7 @@ func TestEditorConfigurations(t *testing.T) {
Constraints: layout.Exact(image.Pt(300, 300)),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog"
@@ -241,7 +241,7 @@ func TestEditor(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
@@ -349,7 +349,7 @@ func TestEditorRTL(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: arabic,
}
cache := text.NewShaper(arabicCollection)
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(arabicCollection))
fontSize := unit.Sp(10)
font := font.Font{}
@@ -419,14 +419,14 @@ func TestEditorLigature(t *testing.T) {
if err != nil {
t.Skipf("failed parsing test font: %v", err)
}
cache := text.NewShaper([]font.FontFace{
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
{
Font: font.Font{
Typeface: "Roboto",
},
Face: face,
},
})
}))
fontSize := unit.Sp(10)
font := font.Font{}
@@ -540,7 +540,7 @@ func TestEditorDimensions(t *testing.T) {
Queue: tq,
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
@@ -587,7 +587,7 @@ func TestEditorCaretConsistency(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
@@ -679,7 +679,7 @@ func TestEditorMoveWord(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.SetText(t)
@@ -784,7 +784,7 @@ func TestEditorInsert(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.SetText(t)
@@ -874,7 +874,7 @@ func TestEditorDeleteWord(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.SetText(t)
@@ -928,7 +928,7 @@ g 2 4 6 8 g
Ops: new(op.Ops),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
font := font.Font{}
fontSize := unit.Sp(10)
@@ -1026,7 +1026,7 @@ func TestSelectMove(t *testing.T) {
Ops: new(op.Ops),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
font := font.Font{}
fontSize := unit.Sp(10)
@@ -1114,7 +1114,7 @@ func TestEditor_MaxLen(t *testing.T) {
key.SelectionEvent{Start: 4, End: 4},
),
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
@@ -1145,7 +1145,7 @@ func TestEditor_Filter(t *testing.T) {
key.SelectionEvent{Start: 4, End: 4},
),
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
@@ -1169,7 +1169,7 @@ func TestEditor_Submit(t *testing.T) {
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
),
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
+47 -26
View File
@@ -20,7 +20,7 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string
ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF)
shaper := text.NewShaper([]font.FontFace{
shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
{
Font: font.Font{Typeface: "LTR"},
Face: ltrFace,
@@ -29,12 +29,11 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string
Font: font.Font{Typeface: "RTL"},
Face: rtlFace,
},
})
}))
// bidiSource is crafted to contain multiple consecutive RTL runs (by
// changing scripts within the RTL).
bidiSource := "The quick سماء שלום لا fox تمط שלום غير the lazy dog."
ltrParams := text.Parameters{
Font: font.Font{Typeface: "LTR"},
PxPerEm: fixed.I(fontSize),
MaxWidth: lineWidth,
MinWidth: lineWidth,
@@ -42,7 +41,6 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string
}
rtlParams := text.Parameters{
Alignment: text.End,
Font: font.Font{Typeface: "RTL"},
PxPerEm: fixed.I(fontSize),
MaxWidth: lineWidth,
MinWidth: lineWidth,
@@ -69,7 +67,7 @@ func makeAccountingTestText(str string, fontSize, lineWidth int) (txt []text.Gly
ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF)
shaper := text.NewShaper([]font.FontFace{{
shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{{
Font: font.Font{Typeface: "LTR"},
Face: ltrFace,
},
@@ -77,7 +75,7 @@ func makeAccountingTestText(str string, fontSize, lineWidth int) (txt []text.Gly
Font: font.Font{Typeface: "RTL"},
Face: rtlFace,
},
})
}))
params := text.Parameters{
PxPerEm: fixed.I(fontSize),
MaxWidth: lineWidth,
@@ -95,7 +93,7 @@ func getGlyphs(fontSize, minWidth, lineWidth int, align text.Alignment, str stri
ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF)
shaper := text.NewShaper([]font.FontFace{{
shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{{
Font: font.Font{Typeface: "LTR"},
Face: ltrFace,
},
@@ -103,7 +101,7 @@ func getGlyphs(fontSize, minWidth, lineWidth int, align text.Alignment, str stri
Font: font.Font{Typeface: "RTL"},
Face: rtlFace,
},
})
}))
params := text.Parameters{
PxPerEm: fixed.I(fontSize),
Alignment: align,
@@ -230,8 +228,11 @@ func TestIndexPositionBidi(t *testing.T) {
glyphs: bidiLTRText,
expectedXs: []fixed.Int26_6{
0, 626, 1196, 1766, 2051, 2621, 3191, 3444, 3956, 4468, 4753, 7133, 6330, 5738, 5440, 5019, 4753, // Positions on line 0.
3953, 3185, 2417, 1649, 881, 596, 298, 0, 3953, 4238, 4523, 5093, 5605, 5890, 7905, 7599, 7007, 6156, 5890, // Positions on line 1.
4660, 3892, 3124, 2356, 1588, 1303, 788, 406, 0, 4660, 4945, 5235, 5805, 6375, 6660, 6934, 7504, 8016, 8528, 8813, // Positions on line 2.
0, 570, 1140, 1710, 2034, // Positions on line 3.
},
},
@@ -239,9 +240,13 @@ func TestIndexPositionBidi(t *testing.T) {
name: "bidi rtl",
glyphs: bidiRTLText,
expectedXs: []fixed.Int26_6{
5368, 5994, 6564, 7134, 7419, 7989, 8559, 8812, 9324, 9836, 5368, 5102, 4299, 3707, 3409, 2988, 2722, 2108, 1494, 880, 266, 0, // Positions on line 0.
8801, 8503, 8205, 7939, 6572, 6857, 7427, 7939, 6572, 6306, 6000, 5408, 4557, 4291, 3677, 3063, 2449, 1835, 1569, 1054, 672, 266, 0, // Positions on line 1.
274, 564, 1134, 1704, 1989, 2263, 2833, 3345, 3857, 4142, 4712, 5282, 5852, 274, 0, // Positions on line 2.
2665, 3291, 3861, 4431, 4716, 5286, 5856, 6109, 6621, 7133, 2665, 2380, 1577, 985, 687, 266, 0, // Positions on line 0.
7886, 7118, 6350, 5582, 4814, 4529, 4231, 3933, 3667, 2300, 2585, 3155, 3667, 2300, 2015, 1709, 1117, 266, 0, // Positions on line 1.
8794, 8026, 7258, 6490, 5722, 5437, 4922, 4540, 4134, 3868, 0, 290, 860, 1430, 1715, 1989, 2559, 3071, 3583, 3868, // Positions on line 2.
324, 894, 1464, 2034, 324, 0, // Positions on line 3.
},
},
} {
@@ -352,27 +357,35 @@ func TestIndexPositionLines(t *testing.T) {
{
xOff: fixed.Int26_6(0),
yOff: 22,
glyphs: 20,
width: fixed.Int26_6(9836),
glyphs: 15,
width: fixed.Int26_6(7133),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 41,
glyphs: 19,
width: fixed.Int26_6(8801),
glyphs: 15,
width: fixed.Int26_6(7886),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 60,
glyphs: 13,
width: fixed.Int26_6(5852),
glyphs: 18,
width: fixed.Int26_6(8794),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 80,
glyphs: 4,
width: fixed.Int26_6(2034),
ascent: fixed.Int26_6(968),
descent: fixed.Int26_6(216),
},
},
},
{
@@ -420,29 +433,37 @@ func TestIndexPositionLines(t *testing.T) {
glyphs: bidiRTLTextOpp,
expectedLines: []lineInfo{
{
xOff: fixed.Int26_6(404),
xOff: fixed.Int26_6(3107),
yOff: 22,
glyphs: 20,
width: fixed.Int26_6(9836),
glyphs: 15,
width: fixed.Int26_6(7133),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(1439),
xOff: fixed.Int26_6(2354),
yOff: 41,
glyphs: 19,
width: fixed.Int26_6(8801),
glyphs: 15,
width: fixed.Int26_6(7886),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(4388),
xOff: fixed.Int26_6(1446),
yOff: 60,
glyphs: 13,
width: fixed.Int26_6(5852),
glyphs: 18,
width: fixed.Int26_6(8794),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(8206),
yOff: 80,
glyphs: 4,
width: fixed.Int26_6(2034),
ascent: fixed.Int26_6(968),
descent: fixed.Int26_6(216),
},
},
},
} {
+1 -2
View File
@@ -5,7 +5,6 @@ import (
"testing"
"time"
"gioui.org/font/gofont"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
@@ -45,7 +44,7 @@ func TestListAnchorStrategies(t *testing.T) {
var list widget.List
list.Axis = layout.Vertical
elements := 100
th := material.NewTheme(gofont.Collection())
th := material.NewTheme()
materialList := material.List(th, &list)
indicatorWidth := gtx.Dp(materialList.Width())
+3 -5
View File
@@ -7,7 +7,6 @@ import (
"golang.org/x/exp/shiny/materialdesign/icons"
"gioui.org/font"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
@@ -47,10 +46,9 @@ type Theme struct {
FingerSize unit.Dp
}
func NewTheme(fontCollection []font.FontFace) *Theme {
t := &Theme{
Shaper: text.NewShaper(fontCollection),
}
// NewTheme constructs a theme (and underlying text shaper).
func NewTheme() *Theme {
t := &Theme{Shaper: &text.Shaper{}}
t.Palette = Palette{
Fg: rgb(0x000000),
Bg: rgb(0xffffff),
+2 -2
View File
@@ -37,7 +37,7 @@ func TestSelectableMove(t *testing.T) {
Ops: new(op.Ops),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fnt := font.Font{}
fontSize := unit.Sp(10)
@@ -82,7 +82,7 @@ func TestSelectableConfigurations(t *testing.T) {
Constraints: layout.Exact(image.Pt(300, 300)),
Locale: english,
}
cache := text.NewShaper(gofont.Collection())
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog"
+5 -5
View File
@@ -86,7 +86,7 @@ func BenchmarkLabelStatic(b *testing.B) {
},
Locale: locale,
}
cache := text.NewShaper(benchFonts)
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
if render {
win, _ = headless.NewWindow(size.X, size.Y)
defer win.Release()
@@ -118,7 +118,7 @@ func BenchmarkLabelDynamic(b *testing.B) {
},
Locale: locale,
}
cache := text.NewShaper(benchFonts)
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
if render {
win, _ = headless.NewWindow(size.X, size.Y)
defer win.Release()
@@ -153,7 +153,7 @@ func BenchmarkEditorStatic(b *testing.B) {
},
Locale: locale,
}
cache := text.NewShaper(benchFonts)
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
if render {
win, _ = headless.NewWindow(size.X, size.Y)
defer win.Release()
@@ -186,7 +186,7 @@ func BenchmarkEditorDynamic(b *testing.B) {
},
Locale: locale,
}
cache := text.NewShaper(benchFonts)
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
if render {
win, _ = headless.NewWindow(size.X, size.Y)
defer win.Release()
@@ -224,7 +224,7 @@ func FuzzEditorEditing(f *testing.F) {
},
Locale: arabic,
}
cache := text.NewShaper(benchFonts)
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
fontSize := unit.Sp(10)
font := font.Font{}
e := Editor{}