shaper.go raw
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/p9c/p9/pkg/gel/gio/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 }
143