1package text
2
3import (
4 "encoding/binary"
5 "hash/maphash"
6 "math"
7 "slices"
8
9 "github.com/go-text/typesetting/font"
10)
11
12// maxMeasureEntries bounds the cache so a single frame measuring many unique
13// strings cannot grow it without limit; beyond it results are not cached.
14const maxMeasureEntries = 4096
15
16var measureSeed = maphash.MakeSeed()
17
18type measureKey struct {
19 text string
20 families uint64 // hash of Request.Families and Variations
21 weight float32
22 stretch float32
23 italic bool
24 dir uint8
25 size float64
26 spacing float64
27 width float64
28}
29
30type measureEntry struct {
31 families []string // verified on hit, so a hash collision cannot alias
32 variations []font.Variation
33 layout Layout
34 used uint64
35}
36
37type measureCache struct {
38 entries map[measureKey]*measureEntry
39 frame uint64
40}
41
42func requestKey(s string, r Request, width float64) measureKey {
43 var h maphash.Hash
44 h.SetSeed(measureSeed)
45 for _, f := range r.Families {
46 h.WriteString(f)
47 h.WriteByte(0)
48 }
49 h.WriteByte(1)
50 for _, v := range r.Variations {
51 var b [8]byte
52 binary.LittleEndian.PutUint32(b[:4], uint32(v.Tag))
53 binary.LittleEndian.PutUint32(b[4:], math.Float32bits(v.Value))
54 h.Write(b[:])
55 }
56 return measureKey{text: s, families: h.Sum64(), weight: r.Weight, stretch: r.Stretch, italic: r.Italic, dir: uint8(r.Direction), size: r.Size, spacing: r.LetterSpacing, width: width}
57}
58
59func (c *measureCache) get(s string, r Request, width float64) (Layout, bool) {
60 e := c.entries[requestKey(s, r, width)]
61 if e == nil || !slices.Equal(e.families, r.Families) || !slices.Equal(e.variations, r.Variations) {
62 return Layout{}, false
63 }
64 e.used = c.frame
65 return e.layout, true
66}
67
68func (c *measureCache) put(s string, r Request, width float64, l Layout) {
69 if c.entries == nil {
70 c.entries = make(map[measureKey]*measureEntry)
71 }
72 if len(c.entries) >= maxMeasureEntries {
73 return
74 }
75 c.entries[requestKey(s, r, width)] = &measureEntry{families: slices.Clone(r.Families), variations: slices.Clone(r.Variations), layout: l, used: c.frame}
76}
77
78func (c *measureCache) endFrame() {
79 for k, e := range c.entries {
80 if c.frame-e.used >= 2 {
81 delete(c.entries, k)
82 }
83 }
84 c.frame++
85}
86
87// Clone deep-copies lines, runs and glyphs so positions can be adjusted
88// without changing a cached measurement.
89func (l Layout) Clone() Layout {
90 lines := make([]Line, len(l.Lines))
91 for i, line := range l.Lines {
92 runs := make([]Run, len(line.Runs))
93 for j, run := range line.Runs {
94 run.Glyphs = slices.Clone(run.Glyphs)
95 runs[j] = run
96 }
97 line.Runs = runs
98 lines[i] = line
99 }
100 l.Lines = lines
101 return l
102}