~eliasnaur/gio

aee87baefec54496a63e5bb67c6341b97f196733 — Elias Naur 8 months ago 6759463
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>
6 files changed, 95 insertions(+), 75 deletions(-)

M font/opentype/opentype.go
M text/shaper.go
M text/text.go
M widget/editor.go
M widget/editor_test.go
M widget/label.go
M font/opentype/opentype.go => font/opentype/opentype.go +34 -14
@@ 5,6 5,7 @@
package opentype

import (
	"bytes"
	"io"
	"unicode"
	"unicode/utf8"


@@ 36,6 37,13 @@ type opentype struct {
	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
// data source.
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)
}

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
	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)
}

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
	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
}

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 nextLine text.Line
	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]
		}
		updateBounds(prev.f)
		nextLine.Layout = glyphs[:prev.idx:prev.idx]
		nextLine.Len = prev.len
		nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx])
		nextLine.Width = prev.x + prev.adv
		nextLine.Bounds.Max.X += prev.x
		lines = append(lines, nextLine)


@@ 242,20 249,32 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, fonts []*op
	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 builder clip.Path
	ops := new(op.Ops)
	m := op.Record(ops)
	var x fixed.Int26_6
	builder.Begin(ops)
	for _, g := range str {
		if !unicode.IsSpace(g.Rune) {
			f := fontForGlyph(buf, fonts, g.Rune)
	rune := 0
	for _, r := range str.Text {
		if !unicode.IsSpace(r) {
			f := fontForGlyph(buf, fonts, r)
			if f == nil {
				continue
			}
			segs, ok := f.LoadGlyph(buf, ppem, g.Rune)
			segs, ok := f.LoadGlyph(buf, ppem, r)
			if !ok {
				continue
			}


@@ 301,14 320,15 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str []tex
			}
			lastPos = lastPos.Add(lastArg)
		}
		x += g.Advance
		x += str.Advances[rune]
		rune++
	}
	builder.Outline().Add(ops)
	return m.Stop()
}

func readGlyphs(r io.Reader) ([]text.Glyph, error) {
	var glyphs []text.Glyph
func readGlyphs(r io.Reader) ([]glyph, error) {
	var glyphs []glyph
	buf := make([]byte, 0, 1024)
	for {
		n, err := r.Read(buf[len(buf):cap(buf)])


@@ 322,7 342,7 @@ func readGlyphs(r io.Reader) ([]text.Glyph, error) {
		for i < lim {
			c, s := utf8.DecodeRune(buf[i:])
			i += s
			glyphs = append(glyphs, text.Glyph{Rune: c})
			glyphs = append(glyphs, glyph{Rune: c})
		}
		n = copy(buf, buf[i:])
		buf = buf[:n]

M text/shaper.go => text/shaper.go +11 -15
@@ 14,13 14,10 @@ import (
type Shaper interface {
	// Layout a text according to a set of options.
	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.
	Shape(font Font, size fixed.Int26_6, layout []Glyph) op.CallOp

	// LayoutString is like Layout, but for strings.
	// LayoutString is Layout for strings.
	LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line
	// ShapeString is like Shape for lines previously laid out by LayoutString.
	ShapeString(font Font, size fixed.Int26_6, str string, layout []Glyph) op.CallOp
	// Shape a line of text and return a clipping operation for its outline.
	Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp
}

// A FontFace is a Font and a matching Face.


@@ 91,24 88,23 @@ func NewCache(collection []FontFace) *Cache {
	return c
}

// Layout implements the Shaper interface.
func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) {
	cache := s.lookup(font)
	return cache.face.Layout(size, maxWidth, txt)
}

func (s *Cache) Shape(font Font, size fixed.Int26_6, layout []Glyph) op.CallOp {
	cache := s.lookup(font)
	return cache.face.Shape(size, layout)
}

// LayoutString is a caching implementation of the Shaper interface.
func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line {
	cache := s.lookup(font)
	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)
	return cache.shape(size, str, layout)
	return cache.shape(size, layout)
}

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
}

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 {
		return op.CallOp{}
	}
	pk := pathKey{
		ppem: ppem,
		str:  str,
		str:  layout.Text,
	}
	if clip, ok := f.pathCache.Get(pk); ok {
		return clip

M text/text.go => text/text.go +5 -7
@@ 11,9 11,7 @@ import (

// A Line contains the measurements of a line of text.
type Line struct {
	Layout []Glyph
	// Len is the length in UTF8 bytes of the line.
	Len int
	Layout Layout
	// Width is the width of the line.
	Width fixed.Int26_6
	// Ascent is the height above the baseline.


@@ 25,9 23,9 @@ type Line struct {
	Bounds fixed.Rectangle26_6
}

type Glyph struct {
	Rune    rune
	Advance fixed.Int26_6
type Layout struct {
	Text     string
	Advances []fixed.Int26_6
}

// Style is the font style.


@@ 50,7 48,7 @@ type Font struct {
// methods must be safe for concurrent use.
type Face interface {
	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

M widget/editor.go => widget/editor.go +23 -20
@@ 4,6 4,7 @@ package widget

import (
	"bufio"
	"bytes"
	"image"
	"io"
	"math"


@@ 390,7 391,7 @@ func (e *Editor) layout(gtx layout.Context) layout.Dimensions {
	}
	e.shapes = e.shapes[:0]
	for {
		_, _, layout, off, ok := it.Next()
		layout, off, ok := it.Next()
		if !ok {
			break
		}


@@ 568,8 569,8 @@ func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
	for i := 0; i < len(lines)-1; i++ {
		// To avoid layout flickering while editing, assume a soft newline takes
		// up all available space.
		if layout := lines[i].Layout; len(layout) > 0 {
			r := layout[len(layout)-1].Rune
		if layout := lines[i].Layout; len(layout.Text) > 0 {
			r := layout.Text[len(layout.Text)-1]
			if r != '\n' {
				dims.Size.X = e.maxWidth
				break


@@ 602,11 603,11 @@ loop:
		l := e.lines[line]
		y += (prevDesc + l.Ascent).Ceil()
		prevDesc = l.Descent
		for _, g := range l.Layout {
		for _, adv := range l.Layout.Advances {
			if idx == e.rr.caret {
				break loop
			}
			x += g.Advance
			x += adv
			_, s := e.rr.runeAt(idx)
			idx += s
			col++


@@ 706,7 707,7 @@ func (e *Editor) moveToLine(x fixed.Int26_6, line int) {
		prevDesc = l.Descent
		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()


@@ 718,15 719,15 @@ func (e *Editor) moveToLine(x fixed.Int26_6, line int) {
		end = 1
	}
	// Move to rune closest to x.
	for i := 0; i < len(l.Layout)-end; i++ {
		g := l.Layout[i]
	for i := 0; i < len(l.Layout.Advances)-end; i++ {
		adv := l.Layout.Advances[i]
		if e.caret.x >= x {
			break
		}
		if e.caret.x+g.Advance-x >= x-e.caret.x {
		if e.caret.x+adv-x >= x-e.caret.x {
			break
		}
		e.caret.x += g.Advance
		e.caret.x += adv
		_, s := e.rr.runeAt(e.rr.caret)
		e.rr.caret += s
		e.caret.col++


@@ 748,7 749,7 @@ func (e *Editor) Move(distance int) {
		_, s := e.rr.runeBefore(e.rr.caret)
		e.rr.caret -= s
		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-- {
		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 {
			end = 1
		}
		if e.caret.col >= len(l)-end {
		if e.caret.col >= len(l.Advances)-end {
			// Move to start of next line.
			e.moveToLine(0, e.caret.line+1)
			continue
		}
		e.caret.x += l[e.caret.col].Advance
		e.caret.x += l.Advances[e.caret.col]
		_, s := e.rr.runeAt(e.rr.caret)
		e.rr.caret += s
		e.caret.col++


@@ 776,7 777,7 @@ func (e *Editor) moveStart() {
	for i := e.caret.col - 1; i >= 0; i-- {
		_, s := e.rr.runeBefore(e.rr.caret)
		e.rr.caret -= s
		e.caret.x -= layout[i].Advance
		e.caret.x -= layout.Advances[i]
	}
	e.caret.col = 0
	e.caret.xoff = -e.caret.x


@@ 791,8 792,8 @@ func (e *Editor) moveEnd() {
		end = 1
	}
	layout := l.Layout
	for i := e.caret.col; i < len(layout)-end; i++ {
		adv := layout[i].Advance
	for i := e.caret.col; i < len(layout.Advances)-end; i++ {
		adv := layout.Advances[i]
		_, s := e.rr.runeAt(e.rr.caret)
		e.rr.caret += s
		e.caret.x += adv


@@ 915,14 916,14 @@ func (e *Editor) NumLines() int {
}

func nullLayout(r io.Reader) ([]text.Line, error) {
	var layout []text.Glyph
	rr := bufio.NewReader(r)
	var rerr error
	var n int
	var buf bytes.Buffer
	for {
		r, s, err := rr.ReadRune()
		n += s
		layout = append(layout, text.Glyph{Rune: r})
		buf.WriteRune(r)
		if err != nil {
			rerr = err
			break


@@ 930,8 931,10 @@ func nullLayout(r io.Reader) ([]text.Line, error) {
	}
	return []text.Line{
		{
			Layout: layout,
			Len:    n,
			Layout: text.Layout{
				Text:     buf.String(),
				Advances: make([]fixed.Int26_6, n),
			},
		},
	}, rerr
}

M widget/editor_test.go => widget/editor_test.go +3 -3
@@ 62,9 62,9 @@ func TestEditor(t *testing.T) {

	// When a password mask is applied, it should replace all visible glyphs
	for i, line := range e.lines {
		for j, glyph := range line.Layout {
			if glyph.Rune != e.Mask && !unicode.IsSpace(glyph.Rune) {
				t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, glyph.Rune)
		for j, r := range line.Layout.Text {
			if r != e.Mask && !unicode.IsSpace(r) {
				t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, r)
			}
		}
	}

M widget/label.go => widget/label.go +19 -16
@@ 38,7 38,7 @@ type lineIterator struct {

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 {
		line := l.Lines[0]
		l.Lines = l.Lines[1:]


@@ 54,34 54,38 @@ func (l *lineIterator) Next() (int, int, []text.Glyph, f32.Point, bool) {
		}
		layout := line.Layout
		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 {
			continue
		}
		for len(layout) > 0 {
			g := layout[0]
			adv := g.Advance
		for len(layout.Advances) > 0 {
			_, n := utf8.DecodeRuneInString(layout.Text)
			adv := layout.Advances[0]
			if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X {
				break
			}
			off.X += adv
			layout = layout[1:]
			start += utf8.RuneLen(g.Rune)
			layout.Text = layout.Text[n:]
			layout.Advances = layout.Advances[1:]
			start += n
		}
		end := start
		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 {
				layout = layout[:i]
				layout.Advances = layout.Advances[:rune]
				layout.Text = layout.Text[:n]
				break
			}
			end += utf8.RuneLen(g.Rune)
			endx += g.Advance
			end += utf8.RuneLen(r)
			endx += layout.Advances[rune]
			rune++
		}
		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 {


@@ 102,14 106,13 @@ func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size un
		Width:     dims.Size.X,
	}
	for {
		start, end, l, off, ok := it.Next()
		l, off, ok := it.Next()
		if !ok {
			break
		}
		stack := op.Push(gtx.Ops)
		op.Offset(off).Add(gtx.Ops)
		str := txt[start:end]
		s.ShapeString(font, textSize, str, l).Add(gtx.Ops)
		s.Shape(font, textSize, l).Add(gtx.Ops)
		paint.PaintOp{}.Add(gtx.Ops)
		stack.Pop()
	}