text: represent laid out text as strings to facilitate caching of layouts

Commit https://gioui.org/commit/b331407e81456 added text layout and shaping
based on io.Reader and changed Editor to use it. Unfortunately, as ~inkeliz
discovered, caching of shapes were also lost.

~inkeliz suggested fix,

https://lists.sr.ht/~eliasnaur/gio-patches/patches/15059

adds caching of shapes to Editor to regain lost performance.

This change repairs the cache to work on io.Reader API, in hope that the
already complicated Editor won't need additional caching.

Before this change, text layouts were represented as a slice of (rune, advance)
pairs. Unfortunately, this representation doesn't lend itself to caching of
shaping results, so change the representation of a line of text to be a pair
of text and advances:

	package text

	type Layout {
		Text string
		Advances []fixed.Int26_6
	}

The Text field can then be used in a cache key, assuming Advances is
consistent with it.

The end result is that the two shaper variants of text.Shaper is reduced to
just one, and the Len field field of text.Line is no longer needed.

The changed representation adds a bit of extra work to package opentype.
Cleaning that up is left as a future TODO.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2020-11-16 15:45:32 +01:00
parent 67594636e7
commit aee87baefe
6 changed files with 95 additions and 75 deletions
+34 -14
View File
@@ -5,6 +5,7 @@
package opentype package opentype
import ( import (
"bytes"
"io" "io"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@@ -36,6 +37,13 @@ type opentype struct {
Hinting font.Hinting Hinting font.Hinting
} }
// a glyph represents a rune and its advance according to a Font.
// TODO: remove this type and work on io.Readers directly.
type glyph struct {
Rune rune
Advance fixed.Int26_6
}
// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte // NewFont parses an SFNT font, such as TTF or OTF data, from a []byte
// data source. // data source.
func Parse(src []byte) (*Font, error) { func Parse(src []byte) (*Font, error) {
@@ -110,7 +118,7 @@ func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]text.L
return layoutText(&buf, ppem, maxWidth, fonts, glyphs) return layoutText(&buf, ppem, maxWidth, fonts, glyphs)
} }
func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp { func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp {
var buf sfnt.Buffer var buf sfnt.Buffer
return textPath(&buf, ppem, []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str) return textPath(&buf, ppem, []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str)
} }
@@ -130,7 +138,7 @@ func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]
return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs) return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs)
} }
func (c *Collection) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp { func (c *Collection) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp {
var buf sfnt.Buffer var buf sfnt.Buffer
return textPath(&buf, ppem, c.fonts, str) return textPath(&buf, ppem, c.fonts, str)
} }
@@ -147,7 +155,7 @@ func fontForGlyph(buf *sfnt.Buffer, fonts []*opentype, r rune) *opentype {
return fonts[0] // Use replacement character from the first font if necessary return fonts[0] // Use replacement character from the first font if necessary
} }
func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, fonts []*opentype, glyphs []text.Glyph) ([]text.Line, error) { func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, fonts []*opentype, glyphs []glyph) ([]text.Line, error) {
var lines []text.Line var lines []text.Line
var nextLine text.Line var nextLine text.Line
updateBounds := func(f *opentype) { updateBounds := func(f *opentype) {
@@ -180,8 +188,7 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, fonts []*op
prev.f = fonts[0] prev.f = fonts[0]
} }
updateBounds(prev.f) updateBounds(prev.f)
nextLine.Layout = glyphs[:prev.idx:prev.idx] nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx])
nextLine.Len = prev.len
nextLine.Width = prev.x + prev.adv nextLine.Width = prev.x + prev.adv
nextLine.Bounds.Max.X += prev.x nextLine.Bounds.Max.X += prev.x
lines = append(lines, nextLine) lines = append(lines, nextLine)
@@ -242,20 +249,32 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, fonts []*op
return lines, nil return lines, nil
} }
func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str []text.Glyph) op.CallOp { // toLayout converts a slice of glyphs to a text.Layout.
func toLayout(glyphs []glyph) text.Layout {
var buf bytes.Buffer
advs := make([]fixed.Int26_6, len(glyphs))
for i, g := range glyphs {
buf.WriteRune(g.Rune)
advs[i] = glyphs[i].Advance
}
return text.Layout{Text: buf.String(), Advances: advs}
}
func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str text.Layout) op.CallOp {
var lastPos f32.Point var lastPos f32.Point
var builder clip.Path var builder clip.Path
ops := new(op.Ops) ops := new(op.Ops)
m := op.Record(ops) m := op.Record(ops)
var x fixed.Int26_6 var x fixed.Int26_6
builder.Begin(ops) builder.Begin(ops)
for _, g := range str { rune := 0
if !unicode.IsSpace(g.Rune) { for _, r := range str.Text {
f := fontForGlyph(buf, fonts, g.Rune) if !unicode.IsSpace(r) {
f := fontForGlyph(buf, fonts, r)
if f == nil { if f == nil {
continue continue
} }
segs, ok := f.LoadGlyph(buf, ppem, g.Rune) segs, ok := f.LoadGlyph(buf, ppem, r)
if !ok { if !ok {
continue continue
} }
@@ -301,14 +320,15 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str []tex
} }
lastPos = lastPos.Add(lastArg) lastPos = lastPos.Add(lastArg)
} }
x += g.Advance x += str.Advances[rune]
rune++
} }
builder.Outline().Add(ops) builder.Outline().Add(ops)
return m.Stop() return m.Stop()
} }
func readGlyphs(r io.Reader) ([]text.Glyph, error) { func readGlyphs(r io.Reader) ([]glyph, error) {
var glyphs []text.Glyph var glyphs []glyph
buf := make([]byte, 0, 1024) buf := make([]byte, 0, 1024)
for { for {
n, err := r.Read(buf[len(buf):cap(buf)]) n, err := r.Read(buf[len(buf):cap(buf)])
@@ -322,7 +342,7 @@ func readGlyphs(r io.Reader) ([]text.Glyph, error) {
for i < lim { for i < lim {
c, s := utf8.DecodeRune(buf[i:]) c, s := utf8.DecodeRune(buf[i:])
i += s i += s
glyphs = append(glyphs, text.Glyph{Rune: c}) glyphs = append(glyphs, glyph{Rune: c})
} }
n = copy(buf, buf[i:]) n = copy(buf, buf[i:])
buf = buf[:n] buf = buf[:n]
+11 -15
View File
@@ -14,13 +14,10 @@ import (
type Shaper interface { type Shaper interface {
// Layout a text according to a set of options. // Layout a text according to a set of options.
Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
// Shape a line of text and return a clipping operation for its outline. // LayoutString is Layout for strings.
Shape(font Font, size fixed.Int26_6, layout []Glyph) op.CallOp
// LayoutString is like Layout, but for strings.
LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line
// ShapeString is like Shape for lines previously laid out by LayoutString. // Shape a line of text and return a clipping operation for its outline.
ShapeString(font Font, size fixed.Int26_6, str string, layout []Glyph) op.CallOp Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp
} }
// A FontFace is a Font and a matching Face. // A FontFace is a Font and a matching Face.
@@ -91,24 +88,23 @@ func NewCache(collection []FontFace) *Cache {
return c return c
} }
// Layout implements the Shaper interface.
func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) { func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) {
cache := s.lookup(font) cache := s.lookup(font)
return cache.face.Layout(size, maxWidth, txt) return cache.face.Layout(size, maxWidth, txt)
} }
func (s *Cache) Shape(font Font, size fixed.Int26_6, layout []Glyph) op.CallOp { // LayoutString is a caching implementation of the Shaper interface.
cache := s.lookup(font)
return cache.face.Shape(size, layout)
}
func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line { func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line {
cache := s.lookup(font) cache := s.lookup(font)
return cache.layout(size, maxWidth, str) return cache.layout(size, maxWidth, str)
} }
func (s *Cache) ShapeString(font Font, size fixed.Int26_6, str string, layout []Glyph) op.CallOp { // Shape is a caching implementation of the Shaper interface. Shape assumes that the layout
// argument is unchanged from a call to Layout or LayoutString.
func (s *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp {
cache := s.lookup(font) cache := s.lookup(font)
return cache.shape(size, str, layout) return cache.shape(size, layout)
} }
func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line { func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line {
@@ -128,13 +124,13 @@ func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line
return l return l
} }
func (f *faceCache) shape(ppem fixed.Int26_6, str string, layout []Glyph) op.CallOp { func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) op.CallOp {
if f == nil { if f == nil {
return op.CallOp{} return op.CallOp{}
} }
pk := pathKey{ pk := pathKey{
ppem: ppem, ppem: ppem,
str: str, str: layout.Text,
} }
if clip, ok := f.pathCache.Get(pk); ok { if clip, ok := f.pathCache.Get(pk); ok {
return clip return clip
+5 -7
View File
@@ -11,9 +11,7 @@ import (
// A Line contains the measurements of a line of text. // A Line contains the measurements of a line of text.
type Line struct { type Line struct {
Layout []Glyph Layout Layout
// Len is the length in UTF8 bytes of the line.
Len int
// Width is the width of the line. // Width is the width of the line.
Width fixed.Int26_6 Width fixed.Int26_6
// Ascent is the height above the baseline. // Ascent is the height above the baseline.
@@ -25,9 +23,9 @@ type Line struct {
Bounds fixed.Rectangle26_6 Bounds fixed.Rectangle26_6
} }
type Glyph struct { type Layout struct {
Rune rune Text string
Advance fixed.Int26_6 Advances []fixed.Int26_6
} }
// Style is the font style. // Style is the font style.
@@ -50,7 +48,7 @@ type Font struct {
// methods must be safe for concurrent use. // methods must be safe for concurrent use.
type Face interface { type Face interface {
Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
Shape(ppem fixed.Int26_6, str []Glyph) op.CallOp Shape(ppem fixed.Int26_6, str Layout) op.CallOp
} }
// Typeface identifies a particular typeface design. The empty // Typeface identifies a particular typeface design. The empty
+23 -20
View File
@@ -4,6 +4,7 @@ package widget
import ( import (
"bufio" "bufio"
"bytes"
"image" "image"
"io" "io"
"math" "math"
@@ -390,7 +391,7 @@ func (e *Editor) layout(gtx layout.Context) layout.Dimensions {
} }
e.shapes = e.shapes[:0] e.shapes = e.shapes[:0]
for { for {
_, _, layout, off, ok := it.Next() layout, off, ok := it.Next()
if !ok { if !ok {
break break
} }
@@ -568,8 +569,8 @@ func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
for i := 0; i < len(lines)-1; i++ { for i := 0; i < len(lines)-1; i++ {
// To avoid layout flickering while editing, assume a soft newline takes // To avoid layout flickering while editing, assume a soft newline takes
// up all available space. // up all available space.
if layout := lines[i].Layout; len(layout) > 0 { if layout := lines[i].Layout; len(layout.Text) > 0 {
r := layout[len(layout)-1].Rune r := layout.Text[len(layout.Text)-1]
if r != '\n' { if r != '\n' {
dims.Size.X = e.maxWidth dims.Size.X = e.maxWidth
break break
@@ -602,11 +603,11 @@ loop:
l := e.lines[line] l := e.lines[line]
y += (prevDesc + l.Ascent).Ceil() y += (prevDesc + l.Ascent).Ceil()
prevDesc = l.Descent prevDesc = l.Descent
for _, g := range l.Layout { for _, adv := range l.Layout.Advances {
if idx == e.rr.caret { if idx == e.rr.caret {
break loop break loop
} }
x += g.Advance x += adv
_, s := e.rr.runeAt(idx) _, s := e.rr.runeAt(idx)
idx += s idx += s
col++ col++
@@ -706,7 +707,7 @@ func (e *Editor) moveToLine(x fixed.Int26_6, line int) {
prevDesc = l.Descent prevDesc = l.Descent
e.caret.line-- e.caret.line--
l = e.lines[e.caret.line] l = e.lines[e.caret.line]
e.caret.col = len(l.Layout) - 1 e.caret.col = len(l.Layout.Advances) - 1
} }
e.moveStart() e.moveStart()
@@ -718,15 +719,15 @@ func (e *Editor) moveToLine(x fixed.Int26_6, line int) {
end = 1 end = 1
} }
// Move to rune closest to x. // Move to rune closest to x.
for i := 0; i < len(l.Layout)-end; i++ { for i := 0; i < len(l.Layout.Advances)-end; i++ {
g := l.Layout[i] adv := l.Layout.Advances[i]
if e.caret.x >= x { if e.caret.x >= x {
break break
} }
if e.caret.x+g.Advance-x >= x-e.caret.x { if e.caret.x+adv-x >= x-e.caret.x {
break break
} }
e.caret.x += g.Advance e.caret.x += adv
_, s := e.rr.runeAt(e.rr.caret) _, s := e.rr.runeAt(e.rr.caret)
e.rr.caret += s e.rr.caret += s
e.caret.col++ e.caret.col++
@@ -748,7 +749,7 @@ func (e *Editor) Move(distance int) {
_, s := e.rr.runeBefore(e.rr.caret) _, s := e.rr.runeBefore(e.rr.caret)
e.rr.caret -= s e.rr.caret -= s
e.caret.col-- e.caret.col--
e.caret.x -= l[e.caret.col].Advance e.caret.x -= l.Advances[e.caret.col]
} }
for ; distance > 0 && e.rr.caret < e.rr.len(); distance-- { for ; distance > 0 && e.rr.caret < e.rr.len(); distance-- {
l := e.lines[e.caret.line].Layout l := e.lines[e.caret.line].Layout
@@ -757,12 +758,12 @@ func (e *Editor) Move(distance int) {
if e.caret.line < len(e.lines)-1 { if e.caret.line < len(e.lines)-1 {
end = 1 end = 1
} }
if e.caret.col >= len(l)-end { if e.caret.col >= len(l.Advances)-end {
// Move to start of next line. // Move to start of next line.
e.moveToLine(0, e.caret.line+1) e.moveToLine(0, e.caret.line+1)
continue continue
} }
e.caret.x += l[e.caret.col].Advance e.caret.x += l.Advances[e.caret.col]
_, s := e.rr.runeAt(e.rr.caret) _, s := e.rr.runeAt(e.rr.caret)
e.rr.caret += s e.rr.caret += s
e.caret.col++ e.caret.col++
@@ -776,7 +777,7 @@ func (e *Editor) moveStart() {
for i := e.caret.col - 1; i >= 0; i-- { for i := e.caret.col - 1; i >= 0; i-- {
_, s := e.rr.runeBefore(e.rr.caret) _, s := e.rr.runeBefore(e.rr.caret)
e.rr.caret -= s e.rr.caret -= s
e.caret.x -= layout[i].Advance e.caret.x -= layout.Advances[i]
} }
e.caret.col = 0 e.caret.col = 0
e.caret.xoff = -e.caret.x e.caret.xoff = -e.caret.x
@@ -791,8 +792,8 @@ func (e *Editor) moveEnd() {
end = 1 end = 1
} }
layout := l.Layout layout := l.Layout
for i := e.caret.col; i < len(layout)-end; i++ { for i := e.caret.col; i < len(layout.Advances)-end; i++ {
adv := layout[i].Advance adv := layout.Advances[i]
_, s := e.rr.runeAt(e.rr.caret) _, s := e.rr.runeAt(e.rr.caret)
e.rr.caret += s e.rr.caret += s
e.caret.x += adv e.caret.x += adv
@@ -915,14 +916,14 @@ func (e *Editor) NumLines() int {
} }
func nullLayout(r io.Reader) ([]text.Line, error) { func nullLayout(r io.Reader) ([]text.Line, error) {
var layout []text.Glyph
rr := bufio.NewReader(r) rr := bufio.NewReader(r)
var rerr error var rerr error
var n int var n int
var buf bytes.Buffer
for { for {
r, s, err := rr.ReadRune() r, s, err := rr.ReadRune()
n += s n += s
layout = append(layout, text.Glyph{Rune: r}) buf.WriteRune(r)
if err != nil { if err != nil {
rerr = err rerr = err
break break
@@ -930,8 +931,10 @@ func nullLayout(r io.Reader) ([]text.Line, error) {
} }
return []text.Line{ return []text.Line{
{ {
Layout: layout, Layout: text.Layout{
Len: n, Text: buf.String(),
Advances: make([]fixed.Int26_6, n),
},
}, },
}, rerr }, rerr
} }
+3 -3
View File
@@ -62,9 +62,9 @@ func TestEditor(t *testing.T) {
// When a password mask is applied, it should replace all visible glyphs // When a password mask is applied, it should replace all visible glyphs
for i, line := range e.lines { for i, line := range e.lines {
for j, glyph := range line.Layout { for j, r := range line.Layout.Text {
if glyph.Rune != e.Mask && !unicode.IsSpace(glyph.Rune) { if r != e.Mask && !unicode.IsSpace(r) {
t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, glyph.Rune) t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, r)
} }
} }
} }
+19 -16
View File
@@ -38,7 +38,7 @@ type lineIterator struct {
const inf = 1e6 const inf = 1e6
func (l *lineIterator) Next() (int, int, []text.Glyph, f32.Point, bool) { func (l *lineIterator) Next() (text.Layout, f32.Point, bool) {
for len(l.Lines) > 0 { for len(l.Lines) > 0 {
line := l.Lines[0] line := l.Lines[0]
l.Lines = l.Lines[1:] l.Lines = l.Lines[1:]
@@ -54,34 +54,38 @@ func (l *lineIterator) Next() (int, int, []text.Glyph, f32.Point, bool) {
} }
layout := line.Layout layout := line.Layout
start := l.txtOff start := l.txtOff
l.txtOff += line.Len l.txtOff += len(line.Layout.Text)
if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
continue continue
} }
for len(layout) > 0 { for len(layout.Advances) > 0 {
g := layout[0] _, n := utf8.DecodeRuneInString(layout.Text)
adv := g.Advance adv := layout.Advances[0]
if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X { if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X {
break break
} }
off.X += adv off.X += adv
layout = layout[1:] layout.Text = layout.Text[n:]
start += utf8.RuneLen(g.Rune) layout.Advances = layout.Advances[1:]
start += n
} }
end := start end := start
endx := off.X endx := off.X
for i, g := range layout { rune := 0
for n, r := range layout.Text {
if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X { if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X {
layout = layout[:i] layout.Advances = layout.Advances[:rune]
layout.Text = layout.Text[:n]
break break
} }
end += utf8.RuneLen(g.Rune) end += utf8.RuneLen(r)
endx += g.Advance endx += layout.Advances[rune]
rune++
} }
offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64} offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64}
return start, end, layout, offf, true return layout, offf, true
} }
return 0, 0, nil, f32.Point{}, false return text.Layout{}, f32.Point{}, false
} }
func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size unit.Value, txt string) layout.Dimensions { func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size unit.Value, txt string) layout.Dimensions {
@@ -102,14 +106,13 @@ func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size un
Width: dims.Size.X, Width: dims.Size.X,
} }
for { for {
start, end, l, off, ok := it.Next() l, off, ok := it.Next()
if !ok { if !ok {
break break
} }
stack := op.Push(gtx.Ops) stack := op.Push(gtx.Ops)
op.Offset(off).Add(gtx.Ops) op.Offset(off).Add(gtx.Ops)
str := txt[start:end] s.Shape(font, textSize, l).Add(gtx.Ops)
s.ShapeString(font, textSize, str, l).Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops)
stack.Pop() stack.Pop()
} }