1// Package hintlabel rasterizes the keyboard hint labels on the CPU: a small
2// box with the label text in Go Mono, as premultiplied BGRA pixels for the
3// renderer to upload and draw.
4package hintlabel
5
6import (
7 "image"
8 "image/color"
9 "image/draw"
10 "math"
11 "sync"
12
13 "golang.org/x/image/font"
14 "golang.org/x/image/font/gofont/gomono"
15 "golang.org/x/image/font/opentype"
16 "golang.org/x/image/math/fixed"
17)
18
19const (
20 // fontSize is the text size in logical pixels.
21 fontSize = 13
22 // padX and padY are the padding around the text in logical pixels.
23 padX, padY = 3, 1
24 // maxEntries bounds the cache: it is cleared when it grows past it.
25 maxEntries = 1024
26)
27
28var (
29 parseOnce sync.Once
30 parsed *opentype.Font
31 parseErr error
32)
33
34type key struct {
35 text string
36 typed int
37 scale float64
38 style Style
39}
40
41type entry struct {
42 pix []byte
43 w, h int
44}
45
46// Raster draws and caches labels. It is not safe for concurrent use: the
47// renderer owns it.
48type Raster struct {
49 faces map[float64]font.Face
50 cache map[key]entry
51}
52
53// Label returns the label box for text, whose first typed characters are
54// drawn dimmed, as premultiplied BGRA at scale physical pixels per logical
55// pixel. The slice is owned by the cache and stays valid until the cache is
56// cleared by a later call: do not modify it. Empty text, or a font that does
57// not load, yields a nil slice.
58func (r *Raster) Label(text string, typed int, scale float64, styles ...Style) (pix []byte, w, h int) {
59 if text == "" {
60 return nil, 0, 0
61 }
62 if scale <= 0 || math.IsNaN(scale) || math.IsInf(scale, 0) {
63 scale = 1
64 }
65 typed = max(0, min(typed, len(text)))
66 bg, fg := Colors("", "", "")
67 style := Style{fontSize * scale, bg, fg}
68 if len(styles) > 0 {
69 style = styles[0]
70 }
71 k := key{text, typed, scale, style}
72 if e, ok := r.cache[k]; ok {
73 return e.pix, e.w, e.h
74 }
75 face := r.face(style.FontPx)
76 if face == nil {
77 return nil, 0, 0
78 }
79 if r.cache == nil || len(r.cache) >= maxEntries {
80 r.cache = map[key]entry{}
81 }
82 e := render(face, text, typed, style)
83 r.cache[k] = e
84 return e.pix, e.w, e.h
85}
86
87func (r *Raster) face(scale float64) font.Face {
88 if f, ok := r.faces[scale]; ok {
89 return f
90 }
91 parseOnce.Do(func() { parsed, parseErr = opentype.Parse(gomono.TTF) })
92 if parseErr != nil {
93 return nil
94 }
95 f, err := opentype.NewFace(parsed, &opentype.FaceOptions{Size: scale, DPI: 72, Hinting: font.HintingFull})
96 if err != nil {
97 return nil
98 }
99 if r.faces == nil || len(r.faces) >= 16 {
100 for _, old := range r.faces {
101 _ = old.Close()
102 }
103 r.faces = map[float64]font.Face{}
104 }
105 r.faces[scale] = f
106 return f
107}
108
109// render draws an opaque box inside a one-physical-pixel translucent outline.
110// All colours are premultiplied.
111func render(face font.Face, text string, typed int, style Style) entry {
112 scale := style.FontPx / fontSize
113 background, ink := style.Background, style.Foreground
114 typedInk := mix(ink, background)
115 outline := color.RGBA{uint8(math.Round(float64(ink.R) * .35)), uint8(math.Round(float64(ink.G) * .35)), uint8(math.Round(float64(ink.B) * .35)), 89}
116 m := face.Metrics()
117 px := int(math.Round(padX * scale))
118 py := int(math.Round(padY * scale))
119 border := 1
120 adv := font.MeasureString(face, text).Ceil()
121 w := adv + 2*px + 2*border
122 h := m.Height.Ceil() + 2*py + 2*border
123 img := image.NewRGBA(image.Rect(0, 0, w, h))
124 draw.Draw(img, img.Bounds(), image.NewUniform(outline), image.Point{}, draw.Src)
125 draw.Draw(img, img.Bounds().Inset(border), image.NewUniform(background), image.Point{}, draw.Src)
126 d := font.Drawer{Dst: img, Face: face, Dot: fixed.Point26_6{X: fixed.I(border + px), Y: fixed.I(border+py) + m.Ascent}}
127 d.Src = image.NewUniform(typedInk)
128 d.DrawString(text[:typed])
129 d.Src = image.NewUniform(ink)
130 d.DrawString(text[typed:])
131 // Premultiplied RGBA to BGRA in place.
132 pix := img.Pix
133 for i := 0; i+3 < len(pix); i += 4 {
134 pix[i], pix[i+2] = pix[i+2], pix[i]
135 }
136 return entry{pix: pix, w: w, h: h}
137}