From b331407e8145648c6804788a41e89fe77c9a2a2d Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Mon, 13 Jan 2020 20:11:55 +0100 Subject: [PATCH] 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 --- font/opentype/opentype.go | 95 ++++++++++++++++++++++++++------------- text/shaper.go | 41 +++++++++++++---- text/text.go | 4 +- widget/buffer.go | 22 ++++++--- widget/editor.go | 10 ++--- widget/label.go | 4 +- 6 files changed, 123 insertions(+), 53 deletions(-) diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go index 7dcee2b0..e6e0a2d8 100644 --- a/font/opentype/opentype.go +++ b/font/opentype/opentype.go @@ -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 { diff --git a/text/shaper.go b/text/shaper.go index d2cacb1d..3514f94c 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -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 } diff --git a/text/text.go b/text/text.go index 703a994d..2b454670 100644 --- a/text/text.go +++ b/text/text.go @@ -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 } diff --git a/widget/buffer.go b/widget/buffer.go index 74cacdf8..c62ff34c 100644 --- a/widget/buffer.go +++ b/widget/buffer.go @@ -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 } - n += copy(p, e.text[e.gapend:]) - e.pos += n - return n, nil + 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!") + } + return total, nil } func (e *editBuffer) ReadRune() (rune, int, error) { diff --git a/widget/editor.go b/widget/editor.go index aed6dd22..341b4cf4 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -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 diff --git a/widget/label.go b/widget/label.go index 7e9cd158..836d408f 100644 --- a/widget/label.go +++ b/widget/label.go @@ -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() }