b331407e8145648c6804788a41e89fe77c9a2a2d — Elias Naur 5 days ago 16d2a3a
text: add io.Reader Layout method to Shaper

use them for Editor, which is no longer required to construct a string
for laying out its content.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
6 files changed, 123 insertions(+), 53 deletions(-)

M font/opentype/opentype.go
M text/shaper.go
M text/text.go
M widget/buffer.go
M widget/editor.go
M widget/label.go
M font/opentype/opentype.go => font/opentype/opentype.go +64 -31
@@ 85,8 85,12 @@ func (c *Collection) Font(i int) (*Font, error) {
 	return &Font{font: fnt}, nil
 }
 
-func (f *Font) Layout(ppem fixed.Int26_6, str string, opts text.LayoutOptions) []text.Line {
-	return layoutText(&f.buf, ppem, str, &opentype{Font: f.font, Hinting: font.HintingFull}, opts)
+func (f *Font) Layout(ppem fixed.Int26_6, txt io.Reader, opts text.LayoutOptions) ([]text.Line, error) {
+	glyphs, err := readGlyphs(txt)
+	if err != nil {
+		return nil, err
+	}
+	return layoutText(&f.buf, ppem, &opentype{Font: f.font, Hinting: font.HintingFull}, glyphs, opts)
 }
 
 func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp {


@@ 98,59 102,59 @@ func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
 	return o.Metrics(&f.buf, ppem)
 }
 
-func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, opts text.LayoutOptions) []text.Line {
-	m := f.Metrics(buf, ppem)
+func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, glyphs []text.Glyph, opts text.LayoutOptions) ([]text.Line, error) {
+	m := f.Metrics(sbuf, ppem)
 	lineTmpl := text.Line{
 		Ascent: m.Ascent,
 		// m.Height is equal to m.Ascent + m.Descent + linegap.
 		// Compute the descent including the linegap.
 		Descent: m.Height - m.Ascent,
-		Bounds:  f.Bounds(buf, ppem),
+		Bounds:  f.Bounds(sbuf, ppem),
 	}
 	var lines []text.Line
 	maxDotX := fixed.I(opts.MaxWidth)
 	type state struct {
-		r      rune
-		layout []text.Glyph
-		adv    fixed.Int26_6
-		x      fixed.Int26_6
-		idx    int
-		valid  bool
+		r     rune
+		adv   fixed.Int26_6
+		x     fixed.Int26_6
+		idx   int
+		len   int
+		valid bool
 	}
 	var prev, word state
 	endLine := func() {
 		line := lineTmpl
-		line.Layout = prev.layout
-		line.Len = prev.idx
+		line.Layout = glyphs[:prev.idx:prev.idx]
+		line.Len = prev.len
 		line.Width = prev.x + prev.adv
 		line.Bounds.Max.X += prev.x
 		lines = append(lines, line)
-		str = str[prev.idx:]
+		glyphs = glyphs[prev.idx:]
 		prev = state{}
 		word = state{}
 	}
-	for prev.idx < len(str) {
-		c, s := utf8.DecodeRuneInString(str[prev.idx:])
-		a, valid := f.GlyphAdvance(buf, ppem, c)
+	for prev.idx < len(glyphs) {
+		g := &glyphs[prev.idx]
+		a, valid := f.GlyphAdvance(sbuf, ppem, g.Rune)
 		next := state{
-			r:      c,
-			layout: prev.layout,
-			idx:    prev.idx + s,
-			x:      prev.x + prev.adv,
-			adv:    a,
-			valid:  valid,
+			r:     g.Rune,
+			idx:   prev.idx + 1,
+			len:   prev.len + utf8.RuneLen(g.Rune),
+			x:     prev.x + prev.adv,
+			adv:   a,
+			valid: valid,
 		}
-		if c == '\n' {
+		if g.Rune == '\n' {
 			// The newline is zero width; use the previous
 			// character for line measurements.
-			prev.layout = append(prev.layout, text.Glyph{Rune: c, Advance: 0})
 			prev.idx = next.idx
+			prev.len = next.len
 			endLine()
 			continue
 		}
 		var k fixed.Int26_6
 		if prev.valid {
-			k = f.Kern(buf, ppem, prev.r, next.r)
+			k = f.Kern(sbuf, ppem, prev.r, next.r)
 		}
 		// Break the line if we're out of space.
 		if prev.idx > 0 && next.x+next.adv+k > maxDotX {


@@ 160,21 164,21 @@ func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, o
 			}
 			next.x -= word.x + word.adv
 			next.idx -= word.idx
-			next.layout = next.layout[len(word.layout):]
+			next.len -= word.len
 			prev = word
 			endLine()
 		} else if k != 0 {
-			next.layout[len(next.layout)-1].Advance += k
+			glyphs[prev.idx-1].Advance += k
 			next.x += k
 		}
-		next.layout = append(next.layout, text.Glyph{Rune: c, Advance: next.adv})
-		if unicode.IsSpace(c) {
+		g.Advance = next.adv
+		if unicode.IsSpace(g.Rune) {
 			word = next
 		}
 		prev = next
 	}
 	endLine()
-	return lines
+	return lines, nil
 }
 
 func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyph) op.CallOp {


@@ 237,6 241,35 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyp
 	return op.CallOp{Ops: ops}
 }
 
+func readGlyphs(r io.Reader) ([]text.Glyph, error) {
+	var glyphs []text.Glyph
+	buf := make([]byte, 0, 1024)
+	for {
+		n, err := r.Read(buf[len(buf):cap(buf)])
+		buf = buf[:len(buf)+n]
+		lim := len(buf)
+		// Read full runes if possible.
+		if err != io.EOF {
+			lim -= utf8.UTFMax - 1
+		}
+		i := 0
+		for i < lim {
+			c, s := utf8.DecodeRune(buf[i:])
+			i += s
+			glyphs = append(glyphs, text.Glyph{Rune: c})
+		}
+		n = copy(buf, buf[i:])
+		buf = buf[:n]
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			return nil, err
+		}
+	}
+	return glyphs, nil
+}
+
 func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) {
 	g, err := f.Font.GlyphIndex(buf, r)
 	if err != nil {

M text/shaper.go => text/shaper.go +33 -8
@@ 3,6 3,9 @@
 package text
 
 import (
+	"io"
+	"strings"
+
 	"golang.org/x/image/font"
 
 	"gioui.org/op"


@@ 13,17 16,27 @@ import (
 // Shaper implements layout and shaping of text.
 type Shaper interface {
 	// Layout a text according to a set of options.
-	Layout(c unit.Converter, font Font, str string, opts LayoutOptions) []Line
-	// Shape a line of text previously laid out by Layout.
-	Shape(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp
+	Layout(c unit.Converter, font Font, txt io.Reader, opts LayoutOptions) ([]Line, error)
+	// Shape a line of text and return a clipping operation for its outline.
+	Shape(c unit.Converter, font Font, layout []Glyph) op.CallOp
+
+	// LayoutString is like Layout, but for strings..
+	LayoutString(c unit.Converter, font Font, str string, opts LayoutOptions) []Line
+	// ShapeString is like Shape for lines previously laid out by LayoutString.
+	ShapeString(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp
+
+	// Metrics returns the font metrics for font.
 	Metrics(c unit.Converter, font Font) font.Metrics
 }
 
-// FontRegistry implements layout and shaping of text and a cache of
-// computed results.
+// FontRegistry implements layout and shaping of text from a set of
+// registered fonts.
 //
 // If a font matches no registered shape, FontRegistry falls back to the
 // first registered face.
+//
+// The LayoutString and ShapeString results are cached and re-used if
+// possible.
 type FontRegistry struct {
 	def   Typeface
 	faces map[Font]*face


@@ 50,12 63,24 @@ func (s *FontRegistry) Register(font Font, tf Face) {
 	}
 }
 
-func (s *FontRegistry) Layout(c unit.Converter, font Font, str string, opts LayoutOptions) []Line {
+func (s *FontRegistry) Layout(c unit.Converter, font Font, txt io.Reader, opts LayoutOptions) ([]Line, error) {
+	tf := s.faceForFont(font)
+	ppem := fixed.I(c.Px(font.Size))
+	return tf.face.Layout(ppem, txt, opts)
+}
+
+func (s *FontRegistry) Shape(c unit.Converter, font Font, layout []Glyph) op.CallOp {
+	tf := s.faceForFont(font)
+	ppem := fixed.I(c.Px(font.Size))
+	return tf.face.Shape(ppem, layout)
+}
+
+func (s *FontRegistry) LayoutString(c unit.Converter, font Font, str string, opts LayoutOptions) []Line {
 	tf := s.faceForFont(font)
 	return tf.layout(fixed.I(c.Px(font.Size)), str, opts)
 }
 
-func (s *FontRegistry) Shape(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp {
+func (s *FontRegistry) ShapeString(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp {
 	tf := s.faceForFont(font)
 	return tf.shape(fixed.I(c.Px(font.Size)), str, layout)
 }


@@ 108,7 133,7 @@ func (t *face) layout(ppem fixed.Int26_6, str string, opts LayoutOptions) []Line
 	if l, ok := t.layoutCache.Get(lk); ok {
 		return l
 	}
-	l := t.face.Layout(ppem, str, opts)
+	l, _ := t.face.Layout(ppem, strings.NewReader(str), opts)
 	t.layoutCache.Put(lk, l)
 	return l
 }

M text/text.go => text/text.go +3 -1
@@ 3,6 3,8 @@
 package text
 
 import (
+	"io"
+
 	"gioui.org/op"
 	"gioui.org/unit"
 	"golang.org/x/image/font"


@@ 54,7 56,7 @@ type Font struct {
 
 // Face implements text layout and shaping for a particular font.
 type Face interface {
-	Layout(ppem fixed.Int26_6, str string, opts LayoutOptions) []Line
+	Layout(ppem fixed.Int26_6, txt io.Reader, opts LayoutOptions) ([]Line, error)
 	Shape(ppem fixed.Int26_6, str []Glyph) op.CallOp
 	Metrics(ppem fixed.Int26_6) font.Metrics
 }

M widget/buffer.go => widget/buffer.go +17 -5
@@ 95,18 95,30 @@ func (e *editBuffer) gapLen() int {
 	return e.gapend - e.gapstart
 }
 
+func (e *editBuffer) Reset() {
+	e.pos = 0
+}
+
 func (e *editBuffer) Read(p []byte) (int, error) {
 	if e.pos == e.len() {
 		return 0, io.EOF
 	}
-	var n int
+	var total int
 	if e.pos < e.gapstart {
-		n += copy(p, e.text[e.pos:e.gapstart])
+		n := copy(p, e.text[e.pos:e.gapstart])
 		p = p[n:]
+		total += n
+		e.pos += n
+	}
+	if e.pos >= e.gapstart {
+		n := copy(p, e.text[e.pos+e.gapLen():])
+		total += n
+		e.pos += n
+	}
+	if e.pos > e.len() {
+		panic("hey!")
 	}
-	n += copy(p, e.text[e.gapend:])
-	e.pos += n
-	return n, nil
+	return total, nil
 }
 
 func (e *editBuffer) ReadRune() (rune, int, error) {

M widget/editor.go => widget/editor.go +4 -6
@@ 273,13 273,11 @@ func (e *Editor) layout(gtx *layout.Context, sh text.Shaper) {
 	}
 	e.shapes = e.shapes[:0]
 	for {
-		start, end, layout, off, ok := it.Next()
+		_, _, layout, off, ok := it.Next()
 		if !ok {
 			break
 		}
-		// TODO: remove
-		str := e.rr.String()[start:end]
-		path := sh.Shape(gtx, e.font, str, layout)
+		path := sh.Shape(gtx, e.font, layout)
 		e.shapes = append(e.shapes, line{off, path})
 	}
 


@@ 433,9 431,9 @@ func (e *Editor) moveCoord(c unit.Converter, pos image.Point) {
 }
 
 func (e *Editor) layoutText(c unit.Converter, s text.Shaper, font text.Font) ([]text.Line, layout.Dimensions) {
-	txt := e.rr.String()
+	e.rr.Reset()
 	opts := text.LayoutOptions{MaxWidth: e.maxWidth}
-	lines := s.Layout(c, font, txt, opts)
+	lines, _ := s.Layout(c, font, &e.rr, opts)
 	dims := linesDimens(lines)
 	for i := 0; i < len(lines)-1; i++ {
 		// To avoid layout flickering while editing, assume a soft newline takes

M widget/label.go => widget/label.go +2 -2
@@ 85,7 85,7 @@ func (l *lineIterator) Next() (int, int, []text.Glyph, f32.Point, bool) {
 
 func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt string) {
 	cs := gtx.Constraints
-	lines := s.Layout(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max})
+	lines := s.LayoutString(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max})
 	if max := l.MaxLines; max > 0 && len(lines) > max {
 		lines = lines[:max]
 	}


@@ 109,7 109,7 @@ func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt st
 		stack.Push(gtx.Ops)
 		op.TransformOp{}.Offset(off).Add(gtx.Ops)
 		str := txt[start:end]
-		s.Shape(gtx, font, str, layout).Add(gtx.Ops)
+		s.ShapeString(gtx, font, str, layout).Add(gtx.Ops)
 		paint.PaintOp{Rect: lclip}.Add(gtx.Ops)
 		stack.Pop()
 	}