github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/text/shaper.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package text 4 5 import ( 6 "io" 7 "strings" 8 9 "golang.org/x/image/math/fixed" 10 11 "github.com/cybriq/giocore/op" 12 ) 13 14 // Shaper implements layout and shaping of text. 15 type Shaper interface { 16 // Layout a text according to a set of options. 17 Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) 18 // LayoutString is Layout for strings. 19 LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line 20 // Shape a line of text and return a clipping operation for its outline. 21 Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp 22 } 23 24 // A FontFace is a Font and a matching Face. 25 type FontFace struct { 26 Font Font 27 Face Face 28 } 29 30 // Cache implements cached layout and shaping of text from a set of 31 // registered fonts. 32 // 33 // If a font matches no registered shape, Cache falls back to the 34 // first registered face. 35 // 36 // The LayoutString and ShapeString results are cached and re-used if 37 // possible. 38 type Cache struct { 39 def Typeface 40 faces map[Font]*faceCache 41 } 42 43 type faceCache struct { 44 face Face 45 layoutCache layoutCache 46 pathCache pathCache 47 } 48 49 func (c *Cache) lookup(font Font) *faceCache { 50 f := c.faceForStyle(font) 51 if f == nil { 52 font.Typeface = c.def 53 f = c.faceForStyle(font) 54 } 55 return f 56 } 57 58 func (c *Cache) faceForStyle(font Font) *faceCache { 59 tf := c.faces[font] 60 if tf == nil { 61 font := font 62 font.Weight = Normal 63 tf = c.faces[font] 64 } 65 if tf == nil { 66 font := font 67 font.Style = Regular 68 tf = c.faces[font] 69 } 70 if tf == nil { 71 font := font 72 font.Style = Regular 73 font.Weight = Normal 74 tf = c.faces[font] 75 } 76 return tf 77 } 78 79 func NewCache(collection []FontFace) *Cache { 80 c := &Cache{ 81 faces: make(map[Font]*faceCache), 82 } 83 for i, ff := range collection { 84 if i == 0 { 85 c.def = ff.Font.Typeface 86 } 87 c.faces[ff.Font] = &faceCache{face: ff.Face} 88 } 89 return c 90 } 91 92 // Layout implements the Shaper interface. 93 func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) { 94 cache := s.lookup(font) 95 return cache.face.Layout(size, maxWidth, txt) 96 } 97 98 // LayoutString is a caching implementation of the Shaper interface. 99 func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line { 100 cache := s.lookup(font) 101 return cache.layout(size, maxWidth, str) 102 } 103 104 // Shape is a caching implementation of the Shaper interface. Shape assumes that the layout 105 // argument is unchanged from a call to Layout or LayoutString. 106 func (s *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp { 107 cache := s.lookup(font) 108 return cache.shape(size, layout) 109 } 110 111 func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line { 112 if f == nil { 113 return nil 114 } 115 lk := layoutKey{ 116 ppem: ppem, 117 maxWidth: maxWidth, 118 str: str, 119 } 120 if l, ok := f.layoutCache.Get(lk); ok { 121 return l 122 } 123 l, _ := f.face.Layout(ppem, maxWidth, strings.NewReader(str)) 124 f.layoutCache.Put(lk, l) 125 return l 126 } 127 128 func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) op.CallOp { 129 if f == nil { 130 return op.CallOp{} 131 } 132 pk := pathKey{ 133 ppem: ppem, 134 str: layout.Text, 135 } 136 if clip, ok := f.pathCache.Get(pk); ok { 137 return clip 138 } 139 clip := f.face.Shape(ppem, layout) 140 f.pathCache.Put(pk, clip) 141 return clip 142 }