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>
This commit is contained in:
Elias Naur
2020-01-13 20:11:55 +01:00
parent 16d2a3ac0a
commit b331407e81
6 changed files with 123 additions and 53 deletions
+64 -31
View File
@@ -85,8 +85,12 @@ func (c *Collection) Font(i int) (*Font, error) {
return &Font{font: fnt}, nil return &Font{font: fnt}, nil
} }
func (f *Font) Layout(ppem fixed.Int26_6, str string, opts text.LayoutOptions) []text.Line { func (f *Font) Layout(ppem fixed.Int26_6, txt io.Reader, opts text.LayoutOptions) ([]text.Line, error) {
return layoutText(&f.buf, ppem, str, &opentype{Font: f.font, Hinting: font.HintingFull}, opts) 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 { 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) return o.Metrics(&f.buf, ppem)
} }
func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, opts text.LayoutOptions) []text.Line { func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, glyphs []text.Glyph, opts text.LayoutOptions) ([]text.Line, error) {
m := f.Metrics(buf, ppem) m := f.Metrics(sbuf, ppem)
lineTmpl := text.Line{ lineTmpl := text.Line{
Ascent: m.Ascent, Ascent: m.Ascent,
// m.Height is equal to m.Ascent + m.Descent + linegap. // m.Height is equal to m.Ascent + m.Descent + linegap.
// Compute the descent including the linegap. // Compute the descent including the linegap.
Descent: m.Height - m.Ascent, Descent: m.Height - m.Ascent,
Bounds: f.Bounds(buf, ppem), Bounds: f.Bounds(sbuf, ppem),
} }
var lines []text.Line var lines []text.Line
maxDotX := fixed.I(opts.MaxWidth) maxDotX := fixed.I(opts.MaxWidth)
type state struct { type state struct {
r rune r rune
layout []text.Glyph adv fixed.Int26_6
adv fixed.Int26_6 x fixed.Int26_6
x fixed.Int26_6 idx int
idx int len int
valid bool valid bool
} }
var prev, word state var prev, word state
endLine := func() { endLine := func() {
line := lineTmpl line := lineTmpl
line.Layout = prev.layout line.Layout = glyphs[:prev.idx:prev.idx]
line.Len = prev.idx line.Len = prev.len
line.Width = prev.x + prev.adv line.Width = prev.x + prev.adv
line.Bounds.Max.X += prev.x line.Bounds.Max.X += prev.x
lines = append(lines, line) lines = append(lines, line)
str = str[prev.idx:] glyphs = glyphs[prev.idx:]
prev = state{} prev = state{}
word = state{} word = state{}
} }
for prev.idx < len(str) { for prev.idx < len(glyphs) {
c, s := utf8.DecodeRuneInString(str[prev.idx:]) g := &glyphs[prev.idx]
a, valid := f.GlyphAdvance(buf, ppem, c) a, valid := f.GlyphAdvance(sbuf, ppem, g.Rune)
next := state{ next := state{
r: c, r: g.Rune,
layout: prev.layout, idx: prev.idx + 1,
idx: prev.idx + s, len: prev.len + utf8.RuneLen(g.Rune),
x: prev.x + prev.adv, x: prev.x + prev.adv,
adv: a, adv: a,
valid: valid, valid: valid,
} }
if c == '\n' { if g.Rune == '\n' {
// The newline is zero width; use the previous // The newline is zero width; use the previous
// character for line measurements. // character for line measurements.
prev.layout = append(prev.layout, text.Glyph{Rune: c, Advance: 0})
prev.idx = next.idx prev.idx = next.idx
prev.len = next.len
endLine() endLine()
continue continue
} }
var k fixed.Int26_6 var k fixed.Int26_6
if prev.valid { 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. // Break the line if we're out of space.
if prev.idx > 0 && next.x+next.adv+k > maxDotX { 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.x -= word.x + word.adv
next.idx -= word.idx next.idx -= word.idx
next.layout = next.layout[len(word.layout):] next.len -= word.len
prev = word prev = word
endLine() endLine()
} else if k != 0 { } else if k != 0 {
next.layout[len(next.layout)-1].Advance += k glyphs[prev.idx-1].Advance += k
next.x += k next.x += k
} }
next.layout = append(next.layout, text.Glyph{Rune: c, Advance: next.adv}) g.Advance = next.adv
if unicode.IsSpace(c) { if unicode.IsSpace(g.Rune) {
word = next word = next
} }
prev = next prev = next
} }
endLine() endLine()
return lines return lines, nil
} }
func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyph) op.CallOp { 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} 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) { 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) g, err := f.Font.GlyphIndex(buf, r)
if err != nil { if err != nil {
+33 -8
View File
@@ -3,6 +3,9 @@
package text package text
import ( import (
"io"
"strings"
"golang.org/x/image/font" "golang.org/x/image/font"
"gioui.org/op" "gioui.org/op"
@@ -13,17 +16,27 @@ import (
// Shaper implements layout and shaping of text. // Shaper implements layout and shaping of text.
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(c unit.Converter, font Font, str string, opts LayoutOptions) []Line Layout(c unit.Converter, font Font, txt io.Reader, opts LayoutOptions) ([]Line, error)
// Shape a line of text previously laid out by Layout. // Shape a line of text and return a clipping operation for its outline.
Shape(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp 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 Metrics(c unit.Converter, font Font) font.Metrics
} }
// FontRegistry implements layout and shaping of text and a cache of // FontRegistry implements layout and shaping of text from a set of
// computed results. // registered fonts.
// //
// If a font matches no registered shape, FontRegistry falls back to the // If a font matches no registered shape, FontRegistry falls back to the
// first registered face. // first registered face.
//
// The LayoutString and ShapeString results are cached and re-used if
// possible.
type FontRegistry struct { type FontRegistry struct {
def Typeface def Typeface
faces map[Font]*face 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) tf := s.faceForFont(font)
return tf.layout(fixed.I(c.Px(font.Size)), str, opts) 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) tf := s.faceForFont(font)
return tf.shape(fixed.I(c.Px(font.Size)), str, layout) 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 { if l, ok := t.layoutCache.Get(lk); ok {
return l return l
} }
l := t.face.Layout(ppem, str, opts) l, _ := t.face.Layout(ppem, strings.NewReader(str), opts)
t.layoutCache.Put(lk, l) t.layoutCache.Put(lk, l)
return l return l
} }
+3 -1
View File
@@ -3,6 +3,8 @@
package text package text
import ( import (
"io"
"gioui.org/op" "gioui.org/op"
"gioui.org/unit" "gioui.org/unit"
"golang.org/x/image/font" "golang.org/x/image/font"
@@ -54,7 +56,7 @@ type Font struct {
// Face implements text layout and shaping for a particular font. // Face implements text layout and shaping for a particular font.
type Face interface { 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 Shape(ppem fixed.Int26_6, str []Glyph) op.CallOp
Metrics(ppem fixed.Int26_6) font.Metrics Metrics(ppem fixed.Int26_6) font.Metrics
} }
+17 -5
View File
@@ -95,18 +95,30 @@ func (e *editBuffer) gapLen() int {
return e.gapend - e.gapstart return e.gapend - e.gapstart
} }
func (e *editBuffer) Reset() {
e.pos = 0
}
func (e *editBuffer) Read(p []byte) (int, error) { func (e *editBuffer) Read(p []byte) (int, error) {
if e.pos == e.len() { if e.pos == e.len() {
return 0, io.EOF return 0, io.EOF
} }
var n int var total int
if e.pos < e.gapstart { 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:] p = p[n:]
total += n
e.pos += n
} }
n += copy(p, e.text[e.gapend:]) if e.pos >= e.gapstart {
e.pos += n n := copy(p, e.text[e.pos+e.gapLen():])
return n, nil total += n
e.pos += n
}
if e.pos > e.len() {
panic("hey!")
}
return total, nil
} }
func (e *editBuffer) ReadRune() (rune, int, error) { func (e *editBuffer) ReadRune() (rune, int, error) {
+4 -6
View File
@@ -273,13 +273,11 @@ func (e *Editor) layout(gtx *layout.Context, sh text.Shaper) {
} }
e.shapes = e.shapes[:0] e.shapes = e.shapes[:0]
for { for {
start, end, layout, off, ok := it.Next() _, _, layout, off, ok := it.Next()
if !ok { if !ok {
break break
} }
// TODO: remove path := sh.Shape(gtx, e.font, layout)
str := e.rr.String()[start:end]
path := sh.Shape(gtx, e.font, str, layout)
e.shapes = append(e.shapes, line{off, path}) 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) { 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} opts := text.LayoutOptions{MaxWidth: e.maxWidth}
lines := s.Layout(c, font, txt, opts) lines, _ := s.Layout(c, font, &e.rr, opts)
dims := linesDimens(lines) dims := linesDimens(lines)
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
+2 -2
View File
@@ -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) { func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt string) {
cs := gtx.Constraints 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 { if max := l.MaxLines; max > 0 && len(lines) > max {
lines = 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) stack.Push(gtx.Ops)
op.TransformOp{}.Offset(off).Add(gtx.Ops) op.TransformOp{}.Offset(off).Add(gtx.Ops)
str := txt[start:end] 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) paint.PaintOp{Rect: lclip}.Add(gtx.Ops)
stack.Pop() stack.Pop()
} }