From 43c47f08835cbc3db1614ac4af2c70a048493cb9 Mon Sep 17 00:00:00 2001 From: Chris Waldon Date: Wed, 14 Jun 2023 09:54:55 -0400 Subject: [PATCH] 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 --- app/ime_test.go | 2 +- app/window.go | 4 +- font/font.go | 4 +- go.mod | 2 +- go.sum | 14 +- gpu/internal/rendertest/bench_test.go | 4 +- text/gotext.go | 330 +++++++++++++------------- text/gotext_test.go | 125 ++-------- text/shaper.go | 45 +++- text/shaper_test.go | 14 +- widget/editor_test.go | 32 +-- widget/index_test.go | 73 ++++-- widget/material/list_test.go | 3 +- widget/material/theme.go | 8 +- widget/selectable_test.go | 4 +- widget/text_bench_test.go | 10 +- 16 files changed, 327 insertions(+), 347 deletions(-) diff --git a/app/ime_test.go b/app/ime_test.go index 61399c48..13abf902 100644 --- a/app/ime_test.go +++ b/app/ime_test.go @@ -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() diff --git a/app/window.go b/app/window.go index 93b3247a..342d2f62 100644 --- a/app/window.go +++ b/app/window.go @@ -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), diff --git a/font/font.go b/font/font.go index 17628dfc..03d2485c 100644 --- a/font/font.go +++ b/font/font.go @@ -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 { diff --git a/go.mod b/go.mod index 82fee842..733f01d5 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 935e3963..c6d4115f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/gpu/internal/rendertest/bench_test.go b/gpu/internal/rendertest/bench_test.go index 3ac066c7..7baece05 100644 --- a/gpu/internal/rendertest/bench_test.go +++ b/gpu/internal/rendertest/bench_test.go @@ -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 } diff --git a/text/gotext.go b/text/gotext.go index 6d729da8..00b039e1 100644 --- a/text/gotext.go +++ b/text/gotext.go @@ -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 -} diff --git a/text/gotext_test.go b/text/gotext_test.go index aec3af5a..71356c0b 100644 --- a/text/gotext_test.go +++ b/text/gotext_test.go @@ -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 { diff --git a/text/shaper.go b/text/shaper.go index bcab39e3..f5d347fe 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -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 { diff --git a/text/shaper_test.go b/text/shaper_test.go index 05cb694f..909fc2d0 100644 --- a/text/shaper_test.go +++ b/text/shaper_test.go @@ -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{} diff --git a/widget/editor_test.go b/widget/editor_test.go index 119f21ff..707b6080 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -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{}) diff --git a/widget/index_test.go b/widget/index_test.go index ee86410a..7442245a 100644 --- a/widget/index_test.go +++ b/widget/index_test.go @@ -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), + }, }, }, } { diff --git a/widget/material/list_test.go b/widget/material/list_test.go index a4fd370a..873e28d8 100644 --- a/widget/material/list_test.go +++ b/widget/material/list_test.go @@ -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()) diff --git a/widget/material/theme.go b/widget/material/theme.go index 3e641c61..42f31130 100644 --- a/widget/material/theme.go +++ b/widget/material/theme.go @@ -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), diff --git a/widget/selectable_test.go b/widget/selectable_test.go index 45978a6f..0e4d751a 100644 --- a/widget/selectable_test.go +++ b/widget/selectable_test.go @@ -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" diff --git a/widget/text_bench_test.go b/widget/text_bench_test.go index 17a0ed82..a0d8e855 100644 --- a/widget/text_bench_test.go +++ b/widget/text_bench_test.go @@ -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{}