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{}