mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
go.*,text,font{,/opentype},app,gpu,widget{,/material}: [API] load system fonts
This commit updates the text package to be able to load system fonts. As a consequence,
application authors may choose to provide no fonts manually, and it's
also possible that the system provides none (WASM, for instance, currently provides no
system fonts). As such, the text stack needed some minor tweaks to handle this case by
displaying blank spaces where text should be rather than crashing when no faces are
available.
Internally, we are dropping the old method of choosing faces and instead relying solely
on the new font matching logic in go-text. I chose to do this because maintaining two
different sets of logic with a hierarchical relationship proved to be really complex,
and also the go-text logic seems to produce higher-quality choices.
The breaking API change from this commit is the new way of constructing a text shaper
using text.ShaperOptions. Providing no options will result in a shaper that uses solely
system fonts. The various options can be used to disable system font loading and to
provide an already-parsed collection of fonts as per Gio's old API.
The material.NewTheme function now accepts no arguments instead of a font collection.
Users wanting to provide a collection can simply provide a new shaper configured how
they would like:
theme := material.NewTheme()
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
This commit touches many packages to fix up their construction of text shapers, mostly in
test code. The changes to the tests in package widget deserve special note:
Changing our font resolution logic caused the tofu characters within the
test strings to use a different font's tofu. This isn't a problem, but shifted
the layout of the shaped text a little bit. I've updated the numbers to expect
the new glyph positions.
Fixes: https://todo.sr.ht/~eliasnaur/gio/309
Fixes: https://todo.sr.ht/~eliasnaur/gio/184
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
+1
-1
@@ -29,7 +29,7 @@ func FuzzIME(f *testing.F) {
|
||||
f.Add([]byte("20007800002\x02000"))
|
||||
f.Add([]byte("200A02000990\x19002\x17\x0200"))
|
||||
f.Fuzz(func(t *testing.T, cmds []byte) {
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||
e := new(widget.Editor)
|
||||
e.Focus()
|
||||
|
||||
|
||||
+3
-1
@@ -26,6 +26,7 @@ import (
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
@@ -140,7 +141,8 @@ func NewWindow(options ...Option) *Window {
|
||||
debug.Parse()
|
||||
// Measure decoration height.
|
||||
deco := new(widget.Decorations)
|
||||
theme := material.NewTheme(gofont.Regular())
|
||||
theme := material.NewTheme()
|
||||
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
|
||||
decoStyle := material.Decorations(theme, deco, 0, "")
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
@@ -33,7 +34,8 @@ func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, *material.T
|
||||
Ops: ops,
|
||||
Constraints: layout.Exact(sz),
|
||||
}
|
||||
th := material.NewTheme(gofont.Collection())
|
||||
th := material.NewTheme()
|
||||
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||
return gtx, w, th
|
||||
}
|
||||
|
||||
|
||||
+165
-165
@@ -6,12 +6,15 @@ import (
|
||||
"bytes"
|
||||
"image"
|
||||
"io"
|
||||
"sort"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-text/typesetting/di"
|
||||
"github.com/go-text/typesetting/font"
|
||||
"github.com/go-text/typesetting/fontscan"
|
||||
"github.com/go-text/typesetting/language"
|
||||
"github.com/go-text/typesetting/opentype/api"
|
||||
"github.com/go-text/typesetting/opentype/api/metadata"
|
||||
"github.com/go-text/typesetting/shaping"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/image/math/fixed"
|
||||
@@ -19,6 +22,8 @@ import (
|
||||
|
||||
"gioui.org/f32"
|
||||
giofont "gioui.org/font"
|
||||
"gioui.org/font/opentype"
|
||||
"gioui.org/internal/debug"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
@@ -152,112 +157,17 @@ type runLayout struct {
|
||||
truncator bool
|
||||
}
|
||||
|
||||
// faceOrderer chooses the order in which faces should be applied to text.
|
||||
type faceOrderer struct {
|
||||
def giofont.Font
|
||||
faceScratch []font.Face
|
||||
fontDefaultOrder map[giofont.Font]int
|
||||
defaultOrderedFonts []giofont.Font
|
||||
faces map[giofont.Font]font.Face
|
||||
faceToIndex map[font.Face]int
|
||||
fonts []giofont.Font
|
||||
}
|
||||
|
||||
func (f *faceOrderer) insert(fnt giofont.Font, face font.Face) {
|
||||
if len(f.fonts) == 0 {
|
||||
f.def = fnt
|
||||
}
|
||||
if f.fontDefaultOrder == nil {
|
||||
f.fontDefaultOrder = make(map[giofont.Font]int)
|
||||
}
|
||||
if f.faces == nil {
|
||||
f.faces = make(map[giofont.Font]font.Face)
|
||||
f.faceToIndex = make(map[font.Face]int)
|
||||
}
|
||||
f.fontDefaultOrder[fnt] = len(f.faceScratch)
|
||||
f.defaultOrderedFonts = append(f.defaultOrderedFonts, fnt)
|
||||
f.faceScratch = append(f.faceScratch, face)
|
||||
f.fonts = append(f.fonts, fnt)
|
||||
f.faces[fnt] = face
|
||||
f.faceToIndex[face] = f.fontDefaultOrder[fnt]
|
||||
}
|
||||
|
||||
// resetFontOrder restores the fonts to a predictable order. It should be invoked
|
||||
// before any operation searching the fonts.
|
||||
func (c *faceOrderer) resetFontOrder() {
|
||||
copy(c.fonts, c.defaultOrderedFonts)
|
||||
}
|
||||
|
||||
func (c *faceOrderer) indexFor(face font.Face) int {
|
||||
return c.faceToIndex[face]
|
||||
}
|
||||
|
||||
func (c *faceOrderer) faceFor(idx int) font.Face {
|
||||
if idx < len(c.defaultOrderedFonts) {
|
||||
return c.faces[c.defaultOrderedFonts[idx]]
|
||||
}
|
||||
panic("face index not found")
|
||||
}
|
||||
|
||||
// TODO(whereswaldon): this function could sort all faces by appropriateness for the
|
||||
// given font characteristics. This would ensure that (if possible) text using a
|
||||
// fallback font would select similar weights and emphases to the primary font.
|
||||
func (c *faceOrderer) sortedFacesForStyle(font giofont.Font) []font.Face {
|
||||
c.resetFontOrder()
|
||||
primary, ok := c.fontForStyle(font)
|
||||
if !ok {
|
||||
font.Typeface = c.def.Typeface
|
||||
primary, ok = c.fontForStyle(font)
|
||||
if !ok {
|
||||
primary = c.def
|
||||
}
|
||||
}
|
||||
return c.sorted(primary)
|
||||
}
|
||||
|
||||
// fontForStyle returns the closest existing font to the requested font within the
|
||||
// same typeface.
|
||||
func (c *faceOrderer) fontForStyle(font giofont.Font) (giofont.Font, bool) {
|
||||
if closest, ok := closestFont(font, c.fonts); ok {
|
||||
return closest, true
|
||||
}
|
||||
font.Style = giofont.Regular
|
||||
if closest, ok := closestFont(font, c.fonts); ok {
|
||||
return closest, true
|
||||
}
|
||||
return font, false
|
||||
}
|
||||
|
||||
// faces returns a slice of faces with primary as the first element and
|
||||
// the remaining faces ordered by insertion order.
|
||||
func (f *faceOrderer) sorted(primary giofont.Font) []font.Face {
|
||||
// If we find primary, put it first, and omit it from the below sort.
|
||||
lowest := 0
|
||||
for i := range f.fonts {
|
||||
if f.fonts[i] == primary {
|
||||
if i != 0 {
|
||||
f.fonts[0], f.fonts[i] = f.fonts[i], f.fonts[0]
|
||||
}
|
||||
lowest = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
sorting := f.fonts[lowest:]
|
||||
sort.Slice(sorting, func(i, j int) bool {
|
||||
a := sorting[i]
|
||||
b := sorting[j]
|
||||
return f.fontDefaultOrder[a] < f.fontDefaultOrder[b]
|
||||
})
|
||||
for i, font := range f.fonts {
|
||||
f.faceScratch[i] = f.faces[font]
|
||||
}
|
||||
return f.faceScratch
|
||||
}
|
||||
|
||||
// shaperImpl implements the shaping and line-wrapping of opentype fonts.
|
||||
type shaperImpl struct {
|
||||
// Fields for tracking fonts/faces.
|
||||
orderer faceOrderer
|
||||
fontMap *fontscan.FontMap
|
||||
faces []font.Face
|
||||
faceToIndex map[font.Font]int
|
||||
faceMeta []giofont.Font
|
||||
defaultFaces []string
|
||||
logger interface {
|
||||
Printf(format string, args ...any)
|
||||
}
|
||||
|
||||
// Shaping and wrapping state.
|
||||
shaper shaping.HarfbuzzShaper
|
||||
@@ -274,11 +184,60 @@ type shaperImpl struct {
|
||||
bitmapGlyphCache bitmapCache
|
||||
}
|
||||
|
||||
// debugLogger only logs messages if debug.Text is true.
|
||||
type debugLogger struct {
|
||||
*log.Logger
|
||||
}
|
||||
|
||||
func newDebugLogger() debugLogger {
|
||||
return debugLogger{Logger: log.New(log.Writer(), "[text] ", log.Default().Flags())}
|
||||
}
|
||||
|
||||
func (d debugLogger) Printf(format string, args ...any) {
|
||||
if debug.Text.Load() {
|
||||
d.Logger.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
|
||||
var shaper shaperImpl
|
||||
shaper.logger = newDebugLogger()
|
||||
shaper.fontMap = fontscan.NewFontMap(shaper.logger)
|
||||
shaper.faceToIndex = make(map[font.Font]int)
|
||||
if systemFonts {
|
||||
str, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
shaper.logger.Printf("failed resolving font cache dir: %v", err)
|
||||
shaper.logger.Printf("skipping system font load")
|
||||
}
|
||||
if err := shaper.fontMap.UseSystemFonts(str); err != nil {
|
||||
shaper.logger.Printf("failed loading system fonts: %v", err)
|
||||
}
|
||||
}
|
||||
for _, f := range collection {
|
||||
shaper.Load(f)
|
||||
shaper.defaultFaces = append(shaper.defaultFaces, string(f.Font.Typeface))
|
||||
}
|
||||
shaper.shaper.SetFontCacheSize(32)
|
||||
return &shaper
|
||||
}
|
||||
|
||||
// Load registers the provided FontFace with the shaper, if it is compatible.
|
||||
// It returns whether the face is now available for use. FontFaces are prioritized
|
||||
// in the order in which they are loaded, with the first face being the default.
|
||||
func (s *shaperImpl) Load(f FontFace) {
|
||||
s.orderer.insert(f.Font, f.Face.Face())
|
||||
s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font))
|
||||
s.addFace(f.Face.Face(), f.Font)
|
||||
}
|
||||
|
||||
func (s *shaperImpl) addFace(f font.Face, md giofont.Font) {
|
||||
if _, ok := s.faceToIndex[f.Font]; ok {
|
||||
return
|
||||
}
|
||||
idx := len(s.faces)
|
||||
s.faceToIndex[f.Font] = idx
|
||||
s.faces = append(s.faces, f)
|
||||
s.faceMeta = append(s.faceMeta, md)
|
||||
}
|
||||
|
||||
// splitByScript divides the inputs into new, smaller inputs on script boundaries
|
||||
@@ -362,9 +321,26 @@ func (s *shaperImpl) splitBidi(input shaping.Input) []shaping.Input {
|
||||
return splitInputs
|
||||
}
|
||||
|
||||
// ResolveFace allows shaperImpl to implement shaping.FontMap, wrapping its fontMap
|
||||
// field and ensuring that any faces loaded as part of the search are registered with
|
||||
// ids so that they can be referred to by a GlyphID.
|
||||
func (s *shaperImpl) ResolveFace(r rune) font.Face {
|
||||
face := s.fontMap.ResolveFace(r)
|
||||
if face != nil {
|
||||
family, aspect := s.fontMap.FontMetadata(face.Font)
|
||||
md := opentype.DescriptionToFont(metadata.Description{
|
||||
Family: family,
|
||||
Aspect: aspect,
|
||||
})
|
||||
s.addFace(face, md)
|
||||
return face
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitByFaces divides the inputs by font coverage in the provided faces. It will use the slice provided in buf
|
||||
// as the backing storage of the returned slice if buf is non-nil.
|
||||
func (s *shaperImpl) splitByFaces(inputs []shaping.Input, faces []font.Face, buf []shaping.Input) []shaping.Input {
|
||||
func (s *shaperImpl) splitByFaces(inputs []shaping.Input, buf []shaping.Input) []shaping.Input {
|
||||
var split []shaping.Input
|
||||
if buf == nil {
|
||||
split = make([]shaping.Input, 0, len(inputs))
|
||||
@@ -372,34 +348,78 @@ func (s *shaperImpl) splitByFaces(inputs []shaping.Input, faces []font.Face, buf
|
||||
split = buf
|
||||
}
|
||||
for _, input := range inputs {
|
||||
split = append(split, shaping.SplitByFontGlyphs(input, faces)...)
|
||||
split = append(split, shaping.SplitByFace(input, s)...)
|
||||
}
|
||||
return split
|
||||
}
|
||||
|
||||
// shapeText invokes the text shaper and returns the raw text data in the shaper's native
|
||||
// format. It does not wrap lines.
|
||||
func (s *shaperImpl) shapeText(faces []font.Face, ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output {
|
||||
if len(faces) < 1 {
|
||||
return nil
|
||||
}
|
||||
func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output {
|
||||
lcfg := langConfig{
|
||||
Language: language.NewLanguage(lc.Language),
|
||||
Direction: mapDirection(lc.Direction),
|
||||
}
|
||||
// Create an initial input.
|
||||
input := toInput(faces[0], ppem, lcfg, txt)
|
||||
input := toInput(nil, ppem, lcfg, txt)
|
||||
if input.RunStart == input.RunEnd && len(s.faces) > 0 {
|
||||
// Give the empty string a face. This is a necessary special case because
|
||||
// the face splitting process works by resolving faces for each rune, and
|
||||
// the empty string contains no runes.
|
||||
input.Face = s.faces[0]
|
||||
}
|
||||
// Break input on font glyph coverage.
|
||||
inputs := s.splitBidi(input)
|
||||
inputs = s.splitByFaces(inputs, faces, s.splitScratch1[:0])
|
||||
inputs = s.splitByFaces(inputs, s.splitScratch1[:0])
|
||||
inputs = splitByScript(inputs, lcfg.Direction, s.splitScratch2[:0])
|
||||
// Shape all inputs.
|
||||
if needed := len(inputs) - len(s.outScratchBuf); needed > 0 {
|
||||
s.outScratchBuf = slices.Grow(s.outScratchBuf, needed)
|
||||
}
|
||||
s.outScratchBuf = s.outScratchBuf[:len(inputs)]
|
||||
for i := range inputs {
|
||||
s.outScratchBuf[i] = s.shaper.Shape(inputs[i])
|
||||
s.outScratchBuf = s.outScratchBuf[:0]
|
||||
for _, input := range inputs {
|
||||
if input.Face != nil {
|
||||
s.outScratchBuf = append(s.outScratchBuf, s.shaper.Shape(input))
|
||||
} else {
|
||||
s.outScratchBuf = append(s.outScratchBuf, shaping.Output{
|
||||
// Use the text size as the advance of the entire fake run so that
|
||||
// it doesn't occupy zero space.
|
||||
Advance: input.Size,
|
||||
Size: input.Size,
|
||||
Glyphs: []shaping.Glyph{
|
||||
{
|
||||
Width: input.Size,
|
||||
Height: input.Size,
|
||||
XBearing: 0,
|
||||
YBearing: 0,
|
||||
XAdvance: input.Size,
|
||||
YAdvance: input.Size,
|
||||
XOffset: 0,
|
||||
YOffset: 0,
|
||||
ClusterIndex: input.RunStart,
|
||||
RuneCount: input.RunEnd - input.RunStart,
|
||||
GlyphCount: 1,
|
||||
GlyphID: 0,
|
||||
Mask: 0,
|
||||
},
|
||||
},
|
||||
LineBounds: shaping.Bounds{
|
||||
Ascent: input.Size,
|
||||
Descent: 0,
|
||||
Gap: 0,
|
||||
},
|
||||
GlyphBounds: shaping.Bounds{
|
||||
Ascent: input.Size,
|
||||
Descent: 0,
|
||||
Gap: 0,
|
||||
},
|
||||
Direction: input.Direction,
|
||||
Runes: shaping.Range{
|
||||
Offset: input.RunStart,
|
||||
Count: input.RunEnd - input.RunStart,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return s.outScratchBuf
|
||||
}
|
||||
@@ -416,22 +436,26 @@ func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy {
|
||||
}
|
||||
|
||||
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
|
||||
func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
|
||||
func (s *shaperImpl) shapeAndWrapText(params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
|
||||
wc := shaping.WrapConfig{
|
||||
TruncateAfterLines: params.MaxLines,
|
||||
TextContinues: params.forceTruncate,
|
||||
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
|
||||
}
|
||||
s.fontMap.SetQuery(fontscan.Query{
|
||||
Families: []string{string(params.Font.Typeface)},
|
||||
Aspect: opentype.FontToDescription(params.Font).Aspect,
|
||||
})
|
||||
if wc.TruncateAfterLines > 0 {
|
||||
if len(params.Truncator) == 0 {
|
||||
params.Truncator = "…"
|
||||
}
|
||||
// We only permit a single run as the truncator, regardless of whether more were generated.
|
||||
// Just use the first one.
|
||||
wc.Truncator = s.shapeText(faces, params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
|
||||
wc.Truncator = s.shapeText(params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
|
||||
}
|
||||
// Wrap outputs into lines.
|
||||
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(faces, params.PxPerEm, params.Locale, txt)))
|
||||
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(params.PxPerEm, params.Locale, txt)))
|
||||
}
|
||||
|
||||
// replaceControlCharacters replaces problematic unicode
|
||||
@@ -494,7 +518,7 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
||||
if hasNewline {
|
||||
txt = txt[:len(txt)-1]
|
||||
}
|
||||
ls, truncated := s.shapeAndWrapText(s.orderer.sortedFacesForStyle(params.Font), params, replaceControlCharacters(txt))
|
||||
ls, truncated := s.shapeAndWrapText(params, replaceControlCharacters(txt))
|
||||
|
||||
didTruncate := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
|
||||
|
||||
@@ -508,7 +532,7 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
||||
textLines := make([]line, len(ls))
|
||||
maxHeight := fixed.Int26_6(0)
|
||||
for i := range ls {
|
||||
otLine := toLine(&s.orderer, ls[i], params.Locale.Direction)
|
||||
otLine := toLine(s.faceToIndex, ls[i], params.Locale.Direction)
|
||||
if otLine.lineHeight > maxHeight {
|
||||
maxHeight = otLine.lineHeight
|
||||
}
|
||||
@@ -597,7 +621,13 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
|
||||
x = g.X
|
||||
}
|
||||
ppem, faceIdx, gid := splitGlyphID(g.ID)
|
||||
face := s.orderer.faceFor(faceIdx)
|
||||
if faceIdx >= len(s.faces) {
|
||||
continue
|
||||
}
|
||||
face := s.faces[faceIdx]
|
||||
if face == nil {
|
||||
continue
|
||||
}
|
||||
scaleFactor := fixedToFloat(ppem) / float32(face.Upem())
|
||||
glyphData := face.GlyphData(gid)
|
||||
switch glyphData := glyphData.(type) {
|
||||
@@ -671,7 +701,13 @@ func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp {
|
||||
x = g.X
|
||||
}
|
||||
_, faceIdx, gid := splitGlyphID(g.ID)
|
||||
face := s.orderer.faceFor(faceIdx)
|
||||
if faceIdx >= len(s.faces) {
|
||||
continue
|
||||
}
|
||||
face := s.faces[faceIdx]
|
||||
if face == nil {
|
||||
continue
|
||||
}
|
||||
glyphData := face.GlyphData(gid)
|
||||
switch glyphData := glyphData.(type) {
|
||||
case api.GlyphBitmap:
|
||||
@@ -799,7 +835,7 @@ func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph {
|
||||
}
|
||||
|
||||
// toLine converts the output into a Line with the provided dominant text direction.
|
||||
func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line {
|
||||
func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirection) line {
|
||||
if len(o) < 1 {
|
||||
return line{}
|
||||
}
|
||||
@@ -813,8 +849,12 @@ func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line
|
||||
if run.Size > maxSize {
|
||||
maxSize = run.Size
|
||||
}
|
||||
var font font.Font
|
||||
if run.Face != nil {
|
||||
font = run.Face.Font
|
||||
}
|
||||
line.runs[i] = runLayout{
|
||||
Glyphs: toGioGlyphs(run.Glyphs, run.Size, orderer.indexFor(run.Face)),
|
||||
Glyphs: toGioGlyphs(run.Glyphs, run.Size, faceToIndex[font]),
|
||||
Runes: Range{
|
||||
Count: run.Runes.Count,
|
||||
Offset: line.runeCount,
|
||||
@@ -916,43 +956,3 @@ func computeVisualOrder(l *line) {
|
||||
x += l.runs[runIdx].Advance
|
||||
}
|
||||
}
|
||||
|
||||
// closestFont returns the closest Font in available by weight.
|
||||
// In case of equality the lighter weight will be returned.
|
||||
func closestFont(lookup giofont.Font, available []giofont.Font) (giofont.Font, bool) {
|
||||
found := false
|
||||
var match giofont.Font
|
||||
for _, cf := range available {
|
||||
if cf == lookup {
|
||||
return lookup, true
|
||||
}
|
||||
if cf.Typeface != lookup.Typeface || cf.Style != lookup.Style {
|
||||
continue
|
||||
}
|
||||
if !found {
|
||||
found = true
|
||||
match = cf
|
||||
continue
|
||||
}
|
||||
cDist := weightDistance(lookup.Weight, cf.Weight)
|
||||
mDist := weightDistance(lookup.Weight, match.Weight)
|
||||
if cDist < mDist {
|
||||
match = cf
|
||||
} else if cDist == mDist && cf.Weight < match.Weight {
|
||||
match = cf
|
||||
}
|
||||
}
|
||||
return match, found
|
||||
}
|
||||
|
||||
// weightDistance returns the distance value between two font weights.
|
||||
func weightDistance(wa giofont.Weight, wb giofont.Weight) int {
|
||||
// Avoid dealing with negative Weight values.
|
||||
a := int(wa) + 400
|
||||
b := int(wb) + 400
|
||||
diff := a - b
|
||||
if diff < 0 {
|
||||
return -diff
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
+19
-106
@@ -30,11 +30,12 @@ var arabic = system.Locale{
|
||||
}
|
||||
|
||||
func testShaper(faces ...giofont.Face) *shaperImpl {
|
||||
shaper := shaperImpl{}
|
||||
ff := make([]FontFace, 0, len(faces))
|
||||
for _, face := range faces {
|
||||
shaper.Load(FontFace{Face: face})
|
||||
ff = append(ff, FontFace{Face: face})
|
||||
}
|
||||
return &shaper
|
||||
shaper := newShaperImpl(false, ff)
|
||||
return shaper
|
||||
}
|
||||
|
||||
func TestEmptyString(t *testing.T) {
|
||||
@@ -64,6 +65,18 @@ func TestEmptyString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoFaces(t *testing.T) {
|
||||
ppem := fixed.I(200)
|
||||
shaper := testShaper()
|
||||
|
||||
// Ensure shaping text with no faces does not panic.
|
||||
shaper.LayoutRunes(Parameters{
|
||||
PxPerEm: ppem,
|
||||
MaxWidth: 2000,
|
||||
Locale: english,
|
||||
}, []rune("✨ⷽℎ↞⋇ⱜ⪫⢡⽛⣦␆Ⱨⳏ⳯⒛⭣╎⌞⟻⢇┃➡⬎⩱⸇ⷎ⟅▤⼶⇺⩳⎏⤬⬞ⴈ⋠⿶⢒₍☟⽂ⶦ⫰⭢⌹∼▀⾯⧂❽⩏ⓖ⟅⤔⍇␋⽓ₑ⢳⠑❂⊪⢘⽨⃯▴ⷿ"))
|
||||
}
|
||||
|
||||
func TestAlignWidth(t *testing.T) {
|
||||
lines := []line{
|
||||
{width: fixed.I(50)},
|
||||
@@ -288,13 +301,13 @@ func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize,
|
||||
rtlSource = string(complexRunes[:runeLimit])
|
||||
}
|
||||
}
|
||||
simpleText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
|
||||
simpleText, _ := shaper.shapeAndWrapText(Parameters{
|
||||
PxPerEm: fixed.I(fontSize),
|
||||
MaxWidth: lineWidth,
|
||||
Locale: locale,
|
||||
}, []rune(simpleSource))
|
||||
simpleText = copyLines(simpleText)
|
||||
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
|
||||
complexText, _ := shaper.shapeAndWrapText(Parameters{
|
||||
PxPerEm: fixed.I(fontSize),
|
||||
MaxWidth: lineWidth,
|
||||
Locale: locale,
|
||||
@@ -378,7 +391,7 @@ func TestToLine(t *testing.T) {
|
||||
totalInputGlyphs += len(run.Glyphs)
|
||||
totalInputRunes += run.Runes.Count
|
||||
}
|
||||
output := toLine(&shaper.orderer, input, tc.dir)
|
||||
output := toLine(shaper.faceToIndex, input, tc.dir)
|
||||
if output.bounds.Min == (fixed.Point26_6{}) {
|
||||
t.Errorf("line %d: Bounds.Min not populated", i)
|
||||
}
|
||||
@@ -662,106 +675,6 @@ func TestTextAppend(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClosestFontByWeight(t *testing.T) {
|
||||
const (
|
||||
testTF1 giofont.Typeface = "MockFace"
|
||||
testTF2 giofont.Typeface = "TestFace"
|
||||
testTF3 giofont.Typeface = "AnotherFace"
|
||||
)
|
||||
fonts := []giofont.Font{
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Normal},
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Bold},
|
||||
{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Thin},
|
||||
}
|
||||
weightOnlyTests := []struct {
|
||||
Lookup giofont.Weight
|
||||
Expected giofont.Weight
|
||||
}{
|
||||
// Test for existing weights.
|
||||
{Lookup: giofont.Normal, Expected: giofont.Normal},
|
||||
{Lookup: giofont.Light, Expected: giofont.Light},
|
||||
{Lookup: giofont.Bold, Expected: giofont.Bold},
|
||||
// Test for missing weights.
|
||||
{Lookup: giofont.Thin, Expected: giofont.Light},
|
||||
{Lookup: giofont.ExtraLight, Expected: giofont.Light},
|
||||
{Lookup: giofont.Medium, Expected: giofont.Normal},
|
||||
{Lookup: giofont.SemiBold, Expected: giofont.Bold},
|
||||
{Lookup: giofont.ExtraBold, Expected: giofont.Bold},
|
||||
}
|
||||
for _, test := range weightOnlyTests {
|
||||
got, ok := closestFont(giofont.Font{Typeface: testTF1, Weight: test.Lookup}, fonts)
|
||||
if !ok {
|
||||
t.Errorf("expected closest font for %v to exist", test.Lookup)
|
||||
}
|
||||
if got.Weight != test.Expected {
|
||||
t.Errorf("got weight %v, expected %v", got.Weight, test.Expected)
|
||||
}
|
||||
}
|
||||
fonts = []giofont.Font{
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Bold},
|
||||
{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Bold},
|
||||
}
|
||||
otherTests := []struct {
|
||||
Lookup giofont.Font
|
||||
Expected giofont.Font
|
||||
ExpectedToFail bool
|
||||
}{
|
||||
// Test for existing fonts.
|
||||
{
|
||||
Lookup: giofont.Font{Typeface: testTF1, Weight: giofont.Light},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
||||
},
|
||||
{
|
||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
},
|
||||
// Test for missing fonts.
|
||||
{
|
||||
Lookup: giofont.Font{Typeface: testTF1, Weight: giofont.Normal},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
||||
},
|
||||
{
|
||||
Lookup: giofont.Font{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
Expected: giofont.Font{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Bold},
|
||||
},
|
||||
{
|
||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Thin},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
},
|
||||
{
|
||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Bold},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
},
|
||||
{
|
||||
Lookup: giofont.Font{Typeface: testTF2, Weight: giofont.Normal},
|
||||
ExpectedToFail: true,
|
||||
},
|
||||
{
|
||||
Lookup: giofont.Font{Typeface: testTF2, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
ExpectedToFail: true,
|
||||
},
|
||||
}
|
||||
for _, test := range otherTests {
|
||||
got, ok := closestFont(test.Lookup, fonts)
|
||||
if test.ExpectedToFail {
|
||||
if ok {
|
||||
t.Errorf("expected closest font for %v to not exist", test.Lookup)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("expected closest font for %v to exist", test.Lookup)
|
||||
}
|
||||
if got != test.Expected {
|
||||
t.Errorf("got %v, expected %v", got, test.Expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlyphIDPacking(t *testing.T) {
|
||||
const maxPPem = fixed.Int26_6((1 << sizebits) - 1)
|
||||
type testcase struct {
|
||||
|
||||
+40
-5
@@ -201,6 +201,11 @@ type GlyphID uint64
|
||||
|
||||
// Shaper converts strings of text into glyphs that can be displayed.
|
||||
type Shaper struct {
|
||||
config struct {
|
||||
disableSystemFonts bool
|
||||
collection []FontFace
|
||||
}
|
||||
initialized bool
|
||||
shaper shaperImpl
|
||||
pathCache pathCache
|
||||
bitmapShapeCache bitmapShapeCache
|
||||
@@ -223,26 +228,53 @@ type Shaper struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// ShaperOptions configure text shapers.
|
||||
type ShaperOption func(*Shaper)
|
||||
|
||||
// NoSystemFonts can be used to disable system font loading.
|
||||
func NoSystemFonts() ShaperOption {
|
||||
return func(s *Shaper) {
|
||||
s.config.disableSystemFonts = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithCollection can be used to provide a collection of pre-loaded fonts to the shaper.
|
||||
func WithCollection(collection []FontFace) ShaperOption {
|
||||
return func(s *Shaper) {
|
||||
s.config.collection = collection
|
||||
}
|
||||
}
|
||||
|
||||
// NewShaper constructs a shaper with the provided collection of font faces
|
||||
// available.
|
||||
func NewShaper(collection []FontFace) *Shaper {
|
||||
func NewShaper(options ...ShaperOption) *Shaper {
|
||||
l := &Shaper{}
|
||||
for _, f := range collection {
|
||||
l.shaper.Load(f)
|
||||
for _, opt := range options {
|
||||
opt(l)
|
||||
}
|
||||
l.shaper.shaper.SetFontCacheSize(32)
|
||||
l.reader = bufio.NewReader(nil)
|
||||
l.init()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Shaper) init() {
|
||||
if l.initialized {
|
||||
return
|
||||
}
|
||||
l.initialized = true
|
||||
l.reader = bufio.NewReader(nil)
|
||||
l.shaper = *newShaperImpl(!l.config.disableSystemFonts, l.config.collection)
|
||||
}
|
||||
|
||||
// Layout text from an io.Reader according to a set of options. Results can be retrieved by
|
||||
// iteratively calling NextGlyph.
|
||||
func (l *Shaper) Layout(params Parameters, txt io.Reader) {
|
||||
l.init()
|
||||
l.layoutText(params, txt, "")
|
||||
}
|
||||
|
||||
// LayoutString is Layout for strings.
|
||||
func (l *Shaper) LayoutString(params Parameters, str string) {
|
||||
l.init()
|
||||
l.layoutText(params, nil, str)
|
||||
}
|
||||
|
||||
@@ -368,6 +400,7 @@ func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte
|
||||
// NextGlyph returns the next glyph from the most recent shaping operation, if
|
||||
// any. If there are no more glyphs, ok will be false.
|
||||
func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
||||
l.init()
|
||||
if l.done {
|
||||
return Glyph{}, false
|
||||
}
|
||||
@@ -535,6 +568,7 @@ func splitGlyphID(g GlyphID) (fixed.Int26_6, int, font.GID) {
|
||||
// of all vector glyphs.
|
||||
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
|
||||
func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
|
||||
l.init()
|
||||
key := l.pathCache.hashGlyphs(gs)
|
||||
shape, ok := l.pathCache.Get(key, gs)
|
||||
if ok {
|
||||
@@ -551,6 +585,7 @@ func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
|
||||
// same gs slice.
|
||||
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
|
||||
func (l *Shaper) Bitmaps(gs []Glyph) op.CallOp {
|
||||
l.init()
|
||||
key := l.bitmapShapeCache.hashGlyphs(gs)
|
||||
call, ok := l.bitmapShapeCache.Get(key, gs)
|
||||
if ok {
|
||||
|
||||
+7
-7
@@ -22,7 +22,7 @@ func TestWrappingTruncation(t *testing.T) {
|
||||
textInput := "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua.\n"
|
||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||
collection := []FontFace{{Face: ltrFace}}
|
||||
cache := NewShaper(collection)
|
||||
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||
cache.LayoutString(Parameters{
|
||||
Alignment: Middle,
|
||||
PxPerEm: fixed.I(10),
|
||||
@@ -89,7 +89,7 @@ func TestWrappingForcedTruncation(t *testing.T) {
|
||||
textInput := "Lorem ipsum\ndolor sit\namet"
|
||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||
collection := []FontFace{{Face: ltrFace}}
|
||||
cache := NewShaper(collection)
|
||||
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||
cache.LayoutString(Parameters{
|
||||
Alignment: Middle,
|
||||
PxPerEm: fixed.I(10),
|
||||
@@ -165,7 +165,7 @@ func TestShapingNewlineHandling(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) {
|
||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||
collection := []FontFace{{Face: ltrFace}}
|
||||
cache := NewShaper(collection)
|
||||
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||
checkGlyphs := func() {
|
||||
glyphs := []Glyph{}
|
||||
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
|
||||
@@ -234,7 +234,7 @@ func TestShapingNewlineHandling(t *testing.T) {
|
||||
func TestCacheEmptyString(t *testing.T) {
|
||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||
collection := []FontFace{{Face: ltrFace}}
|
||||
cache := NewShaper(collection)
|
||||
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||
cache.LayoutString(Parameters{
|
||||
Alignment: Middle,
|
||||
PxPerEm: fixed.I(10),
|
||||
@@ -273,7 +273,7 @@ func TestCacheEmptyString(t *testing.T) {
|
||||
func TestCacheAlignment(t *testing.T) {
|
||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||
collection := []FontFace{{Face: ltrFace}}
|
||||
cache := NewShaper(collection)
|
||||
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||
params := Parameters{
|
||||
Alignment: Start,
|
||||
PxPerEm: fixed.I(10),
|
||||
@@ -339,7 +339,7 @@ func TestCacheGlyphConverstion(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cache := NewShaper(collection)
|
||||
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||
cache.LayoutString(Parameters{
|
||||
PxPerEm: fixed.I(10),
|
||||
MaxWidth: 200,
|
||||
@@ -481,7 +481,7 @@ func TestShapeStringRuneAccounting(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(setup.kind, func(t *testing.T) {
|
||||
shaper := NewShaper(gofont.Collection())
|
||||
shaper := NewShaper(NoSystemFonts(), WithCollection(gofont.Collection()))
|
||||
setup.do(shaper, tc.params, tc.input)
|
||||
|
||||
glyphs := []Glyph{}
|
||||
|
||||
+16
-16
@@ -108,7 +108,7 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
key.FocusEvent{Focus: true},
|
||||
},
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
e := new(Editor)
|
||||
@@ -187,7 +187,7 @@ func TestEditorConfigurations(t *testing.T) {
|
||||
Constraints: layout.Exact(image.Pt(300, 300)),
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog"
|
||||
@@ -241,7 +241,7 @@ func TestEditor(t *testing.T) {
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
|
||||
@@ -349,7 +349,7 @@ func TestEditorRTL(t *testing.T) {
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Locale: arabic,
|
||||
}
|
||||
cache := text.NewShaper(arabicCollection)
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(arabicCollection))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
|
||||
@@ -419,14 +419,14 @@ func TestEditorLigature(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Skipf("failed parsing test font: %v", err)
|
||||
}
|
||||
cache := text.NewShaper([]font.FontFace{
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
|
||||
{
|
||||
Font: font.Font{
|
||||
Typeface: "Roboto",
|
||||
},
|
||||
Face: face,
|
||||
},
|
||||
})
|
||||
}))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
|
||||
@@ -540,7 +540,7 @@ func TestEditorDimensions(t *testing.T) {
|
||||
Queue: tq,
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
@@ -587,7 +587,7 @@ func TestEditorCaretConsistency(t *testing.T) {
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
|
||||
@@ -679,7 +679,7 @@ func TestEditorMoveWord(t *testing.T) {
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
e.SetText(t)
|
||||
@@ -784,7 +784,7 @@ func TestEditorInsert(t *testing.T) {
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
e.SetText(t)
|
||||
@@ -874,7 +874,7 @@ func TestEditorDeleteWord(t *testing.T) {
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
e.SetText(t)
|
||||
@@ -928,7 +928,7 @@ g 2 4 6 8 g
|
||||
Ops: new(op.Ops),
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
font := font.Font{}
|
||||
fontSize := unit.Sp(10)
|
||||
|
||||
@@ -1026,7 +1026,7 @@ func TestSelectMove(t *testing.T) {
|
||||
Ops: new(op.Ops),
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
font := font.Font{}
|
||||
fontSize := unit.Sp(10)
|
||||
|
||||
@@ -1114,7 +1114,7 @@ func TestEditor_MaxLen(t *testing.T) {
|
||||
key.SelectionEvent{Start: 4, End: 4},
|
||||
),
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
@@ -1145,7 +1145,7 @@ func TestEditor_Filter(t *testing.T) {
|
||||
key.SelectionEvent{Start: 4, End: 4},
|
||||
),
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
@@ -1169,7 +1169,7 @@ func TestEditor_Submit(t *testing.T) {
|
||||
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
|
||||
),
|
||||
}
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
+47
-26
@@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user