Files
gio-patched/text/shaper.go
T
Chris Waldon 1e5a3696f5 deps,text,widget,font/opentype: [API] add harfbuzz-powered text shaper
This commit introduces a new text shaping infrastructure
powered by Benoit Kugler's Go source-port of harfbuzz.
This shaper can properly display complex scripts and RTL
text. This commit changes the signature of the text.Shaper
function, which is a breaking API change.

The new functionality is available via opentype.ParseHarfbuzz,
which configures a text.Shaper leveraging the new backend.

References: https://todo.sr.ht/~eliasnaur/gio/146
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-03-18 08:01:44 +01:00

181 lines
4.4 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package text
import (
"encoding/binary"
"hash/maphash"
"io"
"strings"
"golang.org/x/image/math/fixed"
"gioui.org/io/system"
"gioui.org/op/clip"
)
// Shaper implements layout and shaping of text.
type Shaper interface {
// Layout a text according to a set of options.
Layout(font Font, size fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]Line, error)
// LayoutString is Layout for strings.
LayoutString(font Font, size fixed.Int26_6, maxWidth int, lc system.Locale, str string) []Line
// Shape a line of text and return a clipping operation for its outline.
Shape(font Font, size fixed.Int26_6, layout Layout) clip.PathSpec
}
// A FontFace is a Font and a matching Face.
type FontFace struct {
Font Font
Face Face
}
// Cache implements cached layout and shaping of text from a set of
// registered fonts.
//
// If a font matches no registered shape, Cache falls back to the
// first registered face.
//
// The LayoutString and ShapeString results are cached and re-used if
// possible.
type Cache struct {
def Typeface
faces map[Font]*faceCache
}
type faceCache struct {
face Face
layoutCache layoutCache
pathCache pathCache
seed maphash.Seed
}
func (c *Cache) lookup(font Font) *faceCache {
f := c.faceForStyle(font)
if f == nil {
font.Typeface = c.def
f = c.faceForStyle(font)
}
return f
}
func (c *Cache) faceForStyle(font Font) *faceCache {
if closest, ok := c.closestFont(font); ok {
return c.faces[closest]
}
font.Style = Regular
if closest, ok := c.closestFont(font); ok {
return c.faces[closest]
}
return nil
}
// closestFont returns the closest Font by weight, in case of equality the
// lighter weight will be returned.
func (c *Cache) closestFont(lookup Font) (Font, bool) {
if c.faces[lookup] != nil {
return lookup, true
}
found := false
var match Font
for cf := range c.faces {
if cf.Typeface != lookup.Typeface || cf.Variant != lookup.Variant || 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
}
func NewCache(collection []FontFace) *Cache {
c := &Cache{
faces: make(map[Font]*faceCache),
}
for i, ff := range collection {
if i == 0 {
c.def = ff.Font.Typeface
}
c.faces[ff.Font] = &faceCache{face: ff.Face}
}
return c
}
// Layout implements the Shaper interface.
func (c *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]Line, error) {
cache := c.lookup(font)
return cache.face.Layout(size, maxWidth, lc, txt)
}
// LayoutString is a caching implementation of the Shaper interface.
func (c *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, lc system.Locale, str string) []Line {
cache := c.lookup(font)
return cache.layout(size, maxWidth, lc, str)
}
// Shape is a caching implementation of the Shaper interface. Shape assumes that the layout
// argument is unchanged from a call to Layout or LayoutString.
func (c *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) clip.PathSpec {
cache := c.lookup(font)
return cache.shape(size, layout)
}
func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, str string) []Line {
if f == nil {
return nil
}
lk := layoutKey{
ppem: ppem,
maxWidth: maxWidth,
str: str,
locale: lc,
}
if l, ok := f.layoutCache.Get(lk); ok {
return l
}
l, _ := f.face.Layout(ppem, maxWidth, lc, strings.NewReader(str))
f.layoutCache.Put(lk, l)
return l
}
// hashGIDs returns a 64-bit hash value of the font GIDs contained
// within the provided layout.
func (f *faceCache) hashGIDs(layout Layout) uint64 {
if f.seed == (maphash.Seed{}) {
f.seed = maphash.MakeSeed()
}
var h maphash.Hash
h.SetSeed(f.seed)
for _, g := range layout.Glyphs {
binary.Write(&h, binary.LittleEndian, g.ID)
}
return h.Sum64()
}
func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) clip.PathSpec {
if f == nil {
return clip.PathSpec{}
}
pk := pathKey{
ppem: ppem,
str: layout.Text,
gidHash: f.hashGIDs(layout),
}
if clip, ok := f.pathCache.Get(pk, layout); ok {
return clip
}
clip := f.face.Shape(ppem, layout)
f.pathCache.Put(pk, layout, clip)
return clip
}