gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/measure/measure.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 /* 4 Package measure implements text layout and shaping. 5 */ 6 package measure 7 8 import ( 9 "math" 10 "unicode" 11 "unicode/utf8" 12 13 "gioui.org/ui" 14 "gioui.org/ui/f32" 15 "gioui.org/ui/paint" 16 "gioui.org/ui/text" 17 "golang.org/x/image/font" 18 "golang.org/x/image/font/sfnt" 19 "golang.org/x/image/math/fixed" 20 ) 21 22 // Faces is a cache of text layouts and paths. 23 type Faces struct { 24 config ui.Config 25 faceCache map[faceKey]*Face 26 layoutCache map[layoutKey]cachedLayout 27 pathCache map[pathKey]cachedPath 28 } 29 30 type cachedLayout struct { 31 active bool 32 layout *text.Layout 33 } 34 35 type cachedPath struct { 36 active bool 37 path ui.MacroOp 38 } 39 40 type layoutKey struct { 41 f *sfnt.Font 42 ppem fixed.Int26_6 43 str string 44 opts text.LayoutOptions 45 } 46 47 type pathKey struct { 48 f *sfnt.Font 49 ppem fixed.Int26_6 50 str string 51 } 52 53 type faceKey struct { 54 font *sfnt.Font 55 size ui.Value 56 } 57 58 // Face is a cached implementation of text.Face. 59 type Face struct { 60 faces *Faces 61 size ui.Value 62 font *opentype 63 } 64 65 // Reset the cache, discarding any measures or paths that 66 // haven't been used since the last call to Reset. 67 func (f *Faces) Reset(c ui.Config) { 68 f.config = c 69 f.init() 70 for pk, p := range f.pathCache { 71 if !p.active { 72 delete(f.pathCache, pk) 73 continue 74 } 75 p.active = false 76 f.pathCache[pk] = p 77 } 78 for lk, l := range f.layoutCache { 79 if !l.active { 80 delete(f.layoutCache, lk) 81 continue 82 } 83 l.active = false 84 f.layoutCache[lk] = l 85 } 86 } 87 88 // For returns a Face for the given font and size. 89 func (f *Faces) For(fnt *sfnt.Font, size ui.Value) *Face { 90 f.init() 91 fk := faceKey{fnt, size} 92 if f, exist := f.faceCache[fk]; exist { 93 return f 94 } 95 face := &Face{ 96 faces: f, 97 size: size, 98 font: &opentype{Font: fnt, Hinting: font.HintingFull}, 99 } 100 f.faceCache[fk] = face 101 return face 102 } 103 104 func (f *Faces) init() { 105 if f.faceCache != nil { 106 return 107 } 108 f.faceCache = make(map[faceKey]*Face) 109 f.pathCache = make(map[pathKey]cachedPath) 110 f.layoutCache = make(map[layoutKey]cachedLayout) 111 } 112 113 func (f *Face) Layout(str string, opts text.LayoutOptions) *text.Layout { 114 ppem := fixed.Int26_6(f.faces.config.Px(f.size) * 64) 115 lk := layoutKey{ 116 f: f.font.Font, 117 ppem: ppem, 118 str: str, 119 opts: opts, 120 } 121 if l, ok := f.faces.layoutCache[lk]; ok { 122 l.active = true 123 f.faces.layoutCache[lk] = l 124 return l.layout 125 } 126 l := layoutText(ppem, str, f.font, opts) 127 f.faces.layoutCache[lk] = cachedLayout{active: true, layout: l} 128 return l 129 } 130 131 func (f *Face) Path(str text.String) ui.MacroOp { 132 ppem := fixed.Int26_6(f.faces.config.Px(f.size) * 64) 133 pk := pathKey{ 134 f: f.font.Font, 135 ppem: ppem, 136 str: str.String, 137 } 138 if p, ok := f.faces.pathCache[pk]; ok { 139 p.active = true 140 f.faces.pathCache[pk] = p 141 return p.path 142 } 143 p := textPath(ppem, f.font, str) 144 f.faces.pathCache[pk] = cachedPath{active: true, path: p} 145 return p 146 } 147 148 func layoutText(ppem fixed.Int26_6, str string, f *opentype, opts text.LayoutOptions) *text.Layout { 149 m := f.Metrics(ppem) 150 lineTmpl := text.Line{ 151 Ascent: m.Ascent, 152 // m.Height is equal to m.Ascent + m.Descent + linegap. 153 // Compute the descent including the linegap. 154 Descent: m.Height - m.Ascent, 155 Bounds: f.Bounds(ppem), 156 } 157 var lines []text.Line 158 maxDotX := fixed.Int26_6(math.MaxInt32) 159 maxDotX = fixed.I(opts.MaxWidth) 160 type state struct { 161 r rune 162 advs []fixed.Int26_6 163 adv fixed.Int26_6 164 x fixed.Int26_6 165 idx int 166 valid bool 167 } 168 var prev, word state 169 endLine := func() { 170 line := lineTmpl 171 line.Text.Advances = prev.advs 172 line.Text.String = str[:prev.idx] 173 line.Width = prev.x + prev.adv 174 line.Bounds.Max.X += prev.x 175 lines = append(lines, line) 176 str = str[prev.idx:] 177 prev = state{} 178 word = state{} 179 } 180 for prev.idx < len(str) { 181 c, s := utf8.DecodeRuneInString(str[prev.idx:]) 182 nl := c == '\n' 183 if opts.SingleLine && nl { 184 nl = false 185 c = ' ' 186 s = 1 187 } 188 a, ok := f.GlyphAdvance(ppem, c) 189 if !ok { 190 prev.idx += s 191 continue 192 } 193 next := state{ 194 r: c, 195 advs: prev.advs, 196 idx: prev.idx + s, 197 x: prev.x + prev.adv, 198 valid: true, 199 } 200 if nl { 201 // The newline is zero width; use the previous 202 // character for line measurements. 203 prev.advs = append(prev.advs, 0) 204 prev.idx = next.idx 205 endLine() 206 continue 207 } 208 next.adv = a 209 var k fixed.Int26_6 210 if prev.valid { 211 k = f.Kern(ppem, prev.r, next.r) 212 } 213 // Break the line if we're out of space. 214 if prev.idx > 0 && next.x+next.adv+k >= maxDotX { 215 // If the line contains no word breaks, break off the last rune. 216 if word.idx == 0 { 217 word = prev 218 } 219 next.x -= word.x + word.adv 220 next.idx -= word.idx 221 next.advs = next.advs[len(word.advs):] 222 prev = word 223 endLine() 224 } else { 225 next.adv += k 226 } 227 next.advs = append(next.advs, next.adv) 228 if unicode.IsSpace(next.r) { 229 word = next 230 } 231 prev = next 232 } 233 endLine() 234 return &text.Layout{Lines: lines} 235 } 236 237 func textPath(ppem fixed.Int26_6, f *opentype, str text.String) ui.MacroOp { 238 var lastPos f32.Point 239 var builder paint.PathBuilder 240 ops := new(ui.Ops) 241 builder.Init(ops) 242 var x fixed.Int26_6 243 var advIdx int 244 var m ui.MacroOp 245 m.Record(ops) 246 for _, r := range str.String { 247 if !unicode.IsSpace(r) { 248 segs, ok := f.LoadGlyph(ppem, r) 249 if !ok { 250 continue 251 } 252 // Move to glyph position. 253 pos := f32.Point{ 254 X: float32(x) / 64, 255 } 256 builder.Move(pos.Sub(lastPos)) 257 lastPos = pos 258 var lastArg f32.Point 259 // Convert sfnt.Segments to relative segments. 260 for _, fseg := range segs { 261 nargs := 1 262 switch fseg.Op { 263 case sfnt.SegmentOpQuadTo: 264 nargs = 2 265 case sfnt.SegmentOpCubeTo: 266 nargs = 3 267 } 268 var args [3]f32.Point 269 for i := 0; i < nargs; i++ { 270 a := f32.Point{ 271 X: float32(fseg.Args[i].X) / 64, 272 Y: float32(fseg.Args[i].Y) / 64, 273 } 274 args[i] = a.Sub(lastArg) 275 if i == nargs-1 { 276 lastArg = a 277 } 278 } 279 switch fseg.Op { 280 case sfnt.SegmentOpMoveTo: 281 builder.Move(args[0]) 282 case sfnt.SegmentOpLineTo: 283 builder.Line(args[0]) 284 case sfnt.SegmentOpQuadTo: 285 builder.Quad(args[0], args[1]) 286 case sfnt.SegmentOpCubeTo: 287 builder.Cube(args[0], args[1], args[2]) 288 default: 289 panic("unsupported segment op") 290 } 291 } 292 lastPos = lastPos.Add(lastArg) 293 } 294 x += str.Advances[advIdx] 295 advIdx++ 296 } 297 builder.End() 298 m.Stop() 299 return m 300 }