mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
959f5889a1
This commit adds support for the idea of a text "Truncator", a string that is shown at the end of truncated text to indicate that it has been shortened because it would not fit within the requested number of lines. When specifying a maximum number of lines, a truncator symbol is always used. If the user does not provide one, the rune `…` is used. This requirement results in a better user experience and significantly simpler code, as we can rely upon the presence of one or more truncator glyphs in the output glyph stream when truncation has occurred. When interacting with truncated text, the truncator glyphs all act as a single, indivisible unit. They can be selected or not, and if selected they act as the entire contents of the truncated portion of the text. This means that copying all of a truncated label will copy the entire label text content, with the truncator symbol not appearing at all. Concretely, the exposed text API now accepts a Truncator string in text.Parameters, and there is a new glyph flag FlagTruncator which indicates that the glyph is part of the truncator run. The truncator run will only have a single FlagClusterBreak (even if the run would usually have many), and the glyph with both FlagClusterBreak and FlagTruncator will have the quantity of truncated runes in its Runes field. This necessitated increasing the size of the Runes field from a byte to an int, as it's theoretically possible for quite a lot of text to be truncated. This commit necessarily bumps our go-text/typesetting dependency to the version exposing truncation in the exported API. Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
184 lines
3.6 KiB
Go
184 lines
3.6 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package text
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"hash/maphash"
|
|
"image"
|
|
|
|
"gioui.org/io/system"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
// entry holds a single key-value pair for an LRU cache.
|
|
type entry[K comparable, V any] struct {
|
|
next, prev *entry[K, V]
|
|
key K
|
|
v V
|
|
}
|
|
|
|
// lru is a generic least-recently-used cache.
|
|
type lru[K comparable, V any] struct {
|
|
m map[K]*entry[K, V]
|
|
head, tail *entry[K, V]
|
|
}
|
|
|
|
// Get fetches the value associated with the given key, if any.
|
|
func (l *lru[K, V]) Get(k K) (V, bool) {
|
|
if lt, ok := l.m[k]; ok {
|
|
l.remove(lt)
|
|
l.insert(lt)
|
|
return lt.v, true
|
|
}
|
|
var v V
|
|
return v, false
|
|
}
|
|
|
|
// Put inserts the given value with the given key, evicting old
|
|
// cache entries if necessary.
|
|
func (l *lru[K, V]) Put(k K, v V) {
|
|
if l.m == nil {
|
|
l.m = make(map[K]*entry[K, V])
|
|
l.head = new(entry[K, V])
|
|
l.tail = new(entry[K, V])
|
|
l.head.prev = l.tail
|
|
l.tail.next = l.head
|
|
}
|
|
val := &entry[K, V]{key: k, v: v}
|
|
l.m[k] = val
|
|
l.insert(val)
|
|
if len(l.m) > maxSize {
|
|
oldest := l.tail.next
|
|
l.remove(oldest)
|
|
delete(l.m, oldest.key)
|
|
}
|
|
}
|
|
|
|
// remove cuts e out of the lru linked list.
|
|
func (l *lru[K, V]) remove(e *entry[K, V]) {
|
|
e.next.prev = e.prev
|
|
e.prev.next = e.next
|
|
}
|
|
|
|
// insert adds e to the lru linked list.
|
|
func (l *lru[K, V]) insert(e *entry[K, V]) {
|
|
e.next = l.head
|
|
e.prev = l.head.prev
|
|
e.prev.next = e
|
|
e.next.prev = e
|
|
}
|
|
|
|
type bitmapCache = lru[GlyphID, bitmap]
|
|
|
|
type bitmap struct {
|
|
img paint.ImageOp
|
|
size image.Point
|
|
}
|
|
|
|
type layoutCache = lru[layoutKey, document]
|
|
|
|
type glyphValue[V any] struct {
|
|
v V
|
|
glyphs []glyphInfo
|
|
}
|
|
|
|
type glyphLRU[V any] struct {
|
|
seed maphash.Seed
|
|
cache lru[uint64, glyphValue[V]]
|
|
}
|
|
|
|
// hashGlyphs computes a hash key based on the ID and X offset of
|
|
// every glyph in the slice.
|
|
func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 {
|
|
if c.seed == (maphash.Seed{}) {
|
|
c.seed = maphash.MakeSeed()
|
|
}
|
|
var h maphash.Hash
|
|
h.SetSeed(c.seed)
|
|
var b [8]byte
|
|
firstX := fixed.Int26_6(0)
|
|
for i, g := range gs {
|
|
if i == 0 {
|
|
firstX = g.X
|
|
}
|
|
// Cache glyph X offsets relative to the first glyph.
|
|
binary.LittleEndian.PutUint32(b[:4], uint32(g.X-firstX))
|
|
h.Write(b[:4])
|
|
binary.LittleEndian.PutUint64(b[:], uint64(g.ID))
|
|
h.Write(b[:])
|
|
}
|
|
sum := h.Sum64()
|
|
return sum
|
|
}
|
|
|
|
func (c *glyphLRU[V]) Get(key uint64, gs []Glyph) (V, bool) {
|
|
if v, ok := c.cache.Get(key); ok && gidsEqual(v.glyphs, gs) {
|
|
return v.v, true
|
|
}
|
|
var v V
|
|
return v, false
|
|
}
|
|
|
|
func (c *glyphLRU[V]) Put(key uint64, glyphs []Glyph, v V) {
|
|
gids := make([]glyphInfo, len(glyphs))
|
|
firstX := fixed.I(0)
|
|
for i, glyph := range glyphs {
|
|
if i == 0 {
|
|
firstX = glyph.X
|
|
}
|
|
// Cache glyph X offsets relative to the first glyph.
|
|
gids[i] = glyphInfo{ID: glyph.ID, X: glyph.X - firstX}
|
|
}
|
|
val := glyphValue[V]{
|
|
glyphs: gids,
|
|
v: v,
|
|
}
|
|
c.cache.Put(key, val)
|
|
}
|
|
|
|
type pathCache = glyphLRU[clip.PathSpec]
|
|
|
|
type bitmapShapeCache = glyphLRU[op.CallOp]
|
|
|
|
type glyphInfo struct {
|
|
ID GlyphID
|
|
X fixed.Int26_6
|
|
}
|
|
|
|
type layoutKey struct {
|
|
ppem fixed.Int26_6
|
|
maxWidth, minWidth int
|
|
maxLines int
|
|
str string
|
|
truncator string
|
|
locale system.Locale
|
|
font Font
|
|
}
|
|
|
|
type pathKey struct {
|
|
gidHash uint64
|
|
}
|
|
|
|
const maxSize = 1000
|
|
|
|
func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
|
|
if len(a) != len(glyphs) {
|
|
return false
|
|
}
|
|
firstX := fixed.Int26_6(0)
|
|
for i := range a {
|
|
if i == 0 {
|
|
firstX = glyphs[i].X
|
|
}
|
|
// Cache glyph X offsets relative to the first glyph.
|
|
if a[i].ID != glyphs[i].ID || a[i].X != (glyphs[i].X-firstX) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|