diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go index 04ca2f45..3e89cefc 100644 --- a/font/opentype/opentype.go +++ b/font/opentype/opentype.go @@ -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] diff --git a/text/shaper.go b/text/shaper.go index 8e95befa..71d7301d 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -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 diff --git a/text/text.go b/text/text.go index 927ab118..ed8695fc 100644 --- a/text/text.go +++ b/text/text.go @@ -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 diff --git a/widget/editor.go b/widget/editor.go index 608e9b66..7d925b99 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -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 } diff --git a/widget/editor_test.go b/widget/editor_test.go index 3b003ad7..bc9c2921 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -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) } } } diff --git a/widget/label.go b/widget/label.go index e59c7886..2cf4890d 100644 --- a/widget/label.go +++ b/widget/label.go @@ -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() }