diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go index 58dd4366..7dcee2b0 100644 --- a/font/opentype/opentype.go +++ b/font/opentype/opentype.go @@ -85,11 +85,11 @@ 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.Layout { +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) Shape(ppem fixed.Int26_6, str text.String) op.CallOp { +func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp { return textPath(&f.buf, ppem, &opentype{Font: f.font, Hinting: font.HintingFull}, str) } @@ -98,7 +98,7 @@ 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.Layout { +func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, opts text.LayoutOptions) []text.Line { m := f.Metrics(buf, ppem) lineTmpl := text.Line{ Ascent: m.Ascent, @@ -110,18 +110,18 @@ func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, o var lines []text.Line maxDotX := fixed.I(opts.MaxWidth) type state struct { - r rune - advs []fixed.Int26_6 - adv fixed.Int26_6 - x fixed.Int26_6 - idx int - valid bool + r rune + layout []text.Glyph + adv fixed.Int26_6 + x fixed.Int26_6 + idx int + valid bool } var prev, word state endLine := func() { line := lineTmpl - line.Text.Advances = prev.advs - line.Text.String = str[:prev.idx] + line.Layout = prev.layout + line.Len = prev.idx line.Width = prev.x + prev.adv line.Bounds.Max.X += prev.x lines = append(lines, line) @@ -133,17 +133,17 @@ func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, o c, s := utf8.DecodeRuneInString(str[prev.idx:]) a, valid := f.GlyphAdvance(buf, ppem, c) next := state{ - r: c, - advs: prev.advs, - idx: prev.idx + s, - x: prev.x + prev.adv, - adv: a, - valid: valid, + r: c, + layout: prev.layout, + idx: prev.idx + s, + x: prev.x + prev.adv, + adv: a, + valid: valid, } if c == '\n' { // The newline is zero width; use the previous // character for line measurements. - prev.advs = append(prev.advs, 0) + prev.layout = append(prev.layout, text.Glyph{Rune: c, Advance: 0}) prev.idx = next.idx endLine() continue @@ -160,33 +160,32 @@ 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.advs = next.advs[len(word.advs):] + next.layout = next.layout[len(word.layout):] prev = word endLine() } else if k != 0 { - next.advs[len(next.advs)-1] += k + next.layout[len(next.layout)-1].Advance += k next.x += k } - next.advs = append(next.advs, next.adv) - if unicode.IsSpace(next.r) { + next.layout = append(next.layout, text.Glyph{Rune: c, Advance: next.adv}) + if unicode.IsSpace(c) { word = next } prev = next } endLine() - return &text.Layout{Lines: lines} + return lines } -func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str text.String) op.CallOp { +func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyph) op.CallOp { var lastPos f32.Point var builder clip.Path ops := new(op.Ops) var x fixed.Int26_6 - var advIdx int builder.Begin(ops) - for _, r := range str.String { - if !unicode.IsSpace(r) { - segs, ok := f.LoadGlyph(buf, ppem, r) + for _, g := range str { + if !unicode.IsSpace(g.Rune) { + segs, ok := f.LoadGlyph(buf, ppem, g.Rune) if !ok { continue } @@ -232,8 +231,7 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str text.String } lastPos = lastPos.Add(lastArg) } - x += str.Advances[advIdx] - advIdx++ + x += g.Advance } builder.End().Add(ops) return op.CallOp{Ops: ops} diff --git a/text/lru.go b/text/lru.go index 343e7aee..3ff9b6d1 100644 --- a/text/lru.go +++ b/text/lru.go @@ -20,7 +20,7 @@ type pathCache struct { type layoutElem struct { next, prev *layoutElem key layoutKey - layout *Layout + layout []Line } type path struct { @@ -42,7 +42,7 @@ type pathKey struct { const maxSize = 1000 -func (l *layoutCache) Get(k layoutKey) (*Layout, bool) { +func (l *layoutCache) Get(k layoutKey) ([]Line, bool) { if lt, ok := l.m[k]; ok { l.remove(lt) l.insert(lt) @@ -51,7 +51,7 @@ func (l *layoutCache) Get(k layoutKey) (*Layout, bool) { return nil, false } -func (l *layoutCache) Put(k layoutKey, lt *Layout) { +func (l *layoutCache) Put(k layoutKey, lt []Line) { if l.m == nil { l.m = make(map[layoutKey]*layoutElem) l.head = new(layoutElem) diff --git a/text/shaper.go b/text/shaper.go index 39bc459b..d2cacb1d 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -3,8 +3,6 @@ package text import ( - "unicode/utf8" - "golang.org/x/image/font" "gioui.org/op" @@ -14,8 +12,10 @@ import ( // Shaper implements layout and shaping of text. type Shaper interface { - Layout(c unit.Converter, font Font, str string, opts LayoutOptions) *Layout - Shape(c unit.Converter, font Font, str String) op.CallOp + // 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 Metrics(c unit.Converter, font Font) font.Metrics } @@ -50,14 +50,14 @@ func (s *FontRegistry) Register(font Font, tf Face) { } } -func (s *FontRegistry) Layout(c unit.Converter, font Font, str string, opts LayoutOptions) *Layout { +func (s *FontRegistry) Layout(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) op.CallOp { +func (s *FontRegistry) Shape(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) + return tf.shape(fixed.I(c.Px(font.Size)), str, layout) } func (s *FontRegistry) Metrics(c unit.Converter, font Font) font.Metrics { @@ -96,9 +96,9 @@ func (s *FontRegistry) faceForFont(font Font) *face { return tf } -func (t *face) layout(ppem fixed.Int26_6, str string, opts LayoutOptions) *Layout { +func (t *face) layout(ppem fixed.Int26_6, str string, opts LayoutOptions) []Line { if t == nil { - return fallbackLayout(str) + return nil } lk := layoutKey{ ppem: ppem, @@ -113,18 +113,18 @@ func (t *face) layout(ppem fixed.Int26_6, str string, opts LayoutOptions) *Layou return l } -func (t *face) shape(ppem fixed.Int26_6, str String) op.CallOp { +func (t *face) shape(ppem fixed.Int26_6, str string, layout []Glyph) op.CallOp { if t == nil { return op.CallOp{} } pk := pathKey{ ppem: ppem, - str: str.String, + str: str, } if clip, ok := t.pathCache.Get(pk); ok { return clip } - clip := t.face.Shape(ppem, str) + clip := t.face.Shape(ppem, layout) t.pathCache.Put(pk, clip) return clip } @@ -132,16 +132,3 @@ func (t *face) shape(ppem fixed.Int26_6, str String) op.CallOp { func (t *face) metrics(ppem fixed.Int26_6) font.Metrics { return t.face.Metrics(ppem) } - -func fallbackLayout(str string) *Layout { - l := &Layout{ - Lines: []Line{ - {Text: String{ - String: str, - }}, - }, - } - strlen := utf8.RuneCountInString(str) - l.Lines[0].Text.Advances = make([]fixed.Int26_6, strlen) - return l -} diff --git a/text/text.go b/text/text.go index 9f3c59e4..703a994d 100644 --- a/text/text.go +++ b/text/text.go @@ -11,7 +11,9 @@ import ( // A Line contains the measurements of a line of text. type Line struct { - Text String + Layout []Glyph + // Len is the length in UTF8 bytes of the line. + Len int // Width is the width of the line. Width fixed.Int26_6 // Ascent is the height above the baseline. @@ -23,16 +25,9 @@ type Line struct { Bounds fixed.Rectangle26_6 } -type String struct { - String string - // Advances contain the advance of each rune in String. - Advances []fixed.Int26_6 -} - -// A Layout contains the measurements of a body of text as -// a list of Lines. -type Layout struct { - Lines []Line +type Glyph struct { + Rune rune + Advance fixed.Int26_6 } // LayoutOptions specify the constraints of a text layout. @@ -59,8 +54,8 @@ 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) *Layout - Shape(ppem fixed.Int26_6, str String) op.CallOp + Layout(ppem fixed.Int26_6, str string, opts LayoutOptions) []Line + Shape(ppem fixed.Int26_6, str []Glyph) op.CallOp Metrics(ppem fixed.Int26_6) font.Metrics } diff --git a/widget/editor.go b/widget/editor.go index 0ae7d359..aed6dd22 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -273,11 +273,13 @@ func (e *Editor) layout(gtx *layout.Context, sh text.Shaper) { } e.shapes = e.shapes[:0] for { - str, off, ok := it.Next() + start, end, layout, off, ok := it.Next() if !ok { break } - path := sh.Shape(gtx, e.font, str) + // TODO: remove + str := e.rr.String()[start:end] + path := sh.Shape(gtx, e.font, str, layout) e.shapes = append(e.shapes, line{off, path}) } @@ -433,15 +435,13 @@ 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() opts := text.LayoutOptions{MaxWidth: e.maxWidth} - textLayout := s.Layout(c, font, txt, opts) - lines := textLayout.Lines + lines := s.Layout(c, font, txt, opts) dims := linesDimens(lines) for i := 0; i < len(lines)-1; i++ { - s := lines[i].Text.String // To avoid layout flickering while editing, assume a soft newline takes // up all available space. - if len(s) > 0 { - r, _ := utf8.DecodeLastRuneInString(s) + if layout := lines[i].Layout; len(layout) > 0 { + r := layout[len(layout)-1].Rune if r != '\n' { dims.Size.X = e.maxWidth break @@ -459,21 +459,18 @@ loop: l := e.lines[carLine] y += (prevDesc + l.Ascent).Ceil() prevDesc = l.Descent - if carLine == len(e.lines)-1 || idx+len(l.Text.String) > e.rr.caret { - str := l.Text.String - for _, adv := range l.Text.Advances { + if carLine == len(e.lines)-1 || idx+len(l.Layout) > e.rr.caret { + for _, g := range l.Layout { if idx == e.rr.caret { break loop } - x += adv - _, s := utf8.DecodeRuneInString(str) - idx += s - str = str[s:] + x += g.Advance + idx += utf8.RuneLen(g.Rune) carCol++ } break } - idx += len(l.Text.String) + idx += l.Len } x += align(e.Alignment, e.lines[carLine].Width, e.viewSize.X) return @@ -553,11 +550,11 @@ func (e *Editor) moveToLine(carX fixed.Int26_6, carLine2 int) fixed.Int26_6 { // Move to start of line2. if carLine2 > carLine { for i := carLine; i < carLine2; i++ { - e.rr.caret += len(e.lines[i].Text.String) + e.rr.caret += e.lines[i].Len } } else { for i := carLine - 1; i >= carLine2; i-- { - e.rr.caret -= len(e.lines[i].Text.String) + e.rr.caret -= e.lines[i].Len } } } @@ -569,15 +566,15 @@ func (e *Editor) moveToLine(carX fixed.Int26_6, carLine2 int) fixed.Int26_6 { end = 1 } // Move to rune closest to previous horizontal position. - for i := 0; i < len(l2.Text.Advances)-end; i++ { - adv := l2.Text.Advances[i] + for i := 0; i < len(l2.Layout)-end; i++ { + g := l2.Layout[i] if carX2 >= carX { break } - if carX2+adv-carX >= carX-carX2 { + if carX2+g.Advance-carX >= carX-carX2 { break } - carX2 += adv + carX2 += g.Advance _, s := e.rr.runeAt(e.rr.caret) e.rr.caret += s } @@ -593,11 +590,11 @@ func (e *Editor) Move(distance int) { func (e *Editor) moveStart() { carLine, carCol, x, _ := e.layoutCaret() - advances := e.lines[carLine].Text.Advances + layout := e.lines[carLine].Layout for i := carCol - 1; i >= 0; i-- { _, s := e.rr.runeBefore(e.rr.caret) e.rr.caret -= s - x -= advances[i] + x -= layout[i].Advance } e.carXOff = -x } @@ -610,8 +607,9 @@ func (e *Editor) moveEnd() { if carLine < len(e.lines)-1 { end = 1 } - for i := carCol; i < len(l.Text.Advances)-end; i++ { - adv := l.Text.Advances[i] + layout := l.Layout + for i := carCol; i < len(layout)-end; i++ { + adv := layout[i].Advance _, s := e.rr.runeAt(e.rr.caret) e.rr.caret += s x += adv diff --git a/widget/label.go b/widget/label.go index 768ae797..7e9cd158 100644 --- a/widget/label.go +++ b/widget/label.go @@ -32,11 +32,12 @@ type lineIterator struct { Offset image.Point y, prevDesc fixed.Int26_6 + txtOff int } const inf = 1e6 -func (l *lineIterator) Next() (text.String, f32.Point, bool) { +func (l *lineIterator) Next() (int, int, []text.Glyph, f32.Point, bool) { for len(l.Lines) > 0 { line := l.Lines[0] l.Lines = l.Lines[1:] @@ -50,42 +51,41 @@ func (l *lineIterator) Next() (text.String, f32.Point, bool) { if (off.Y + line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { break } + layout := line.Layout + start := l.txtOff + l.txtOff += line.Len if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { continue } - str := line.Text - for len(str.Advances) > 0 { - adv := str.Advances[0] + for len(layout) > 0 { + g := layout[0] + adv := g.Advance if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X { break } off.X += adv - _, s := utf8.DecodeRuneInString(str.String) - str.String = str.String[s:] - str.Advances = str.Advances[1:] + layout = layout[1:] + start += utf8.RuneLen(g.Rune) } - n := 0 + end := start endx := off.X - for i, adv := range str.Advances { + for i, g := range layout { if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X { - str.String = str.String[:n] - str.Advances = str.Advances[:i] + layout = layout[:i] break } - _, s := utf8.DecodeRuneInString(str.String[n:]) - n += s - endx += adv + end += utf8.RuneLen(g.Rune) + endx += g.Advance } offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64} - return str, offf, true + return start, end, layout, offf, true } - return text.String{}, f32.Point{}, false + return 0, 0, nil, f32.Point{}, false } func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt string) { cs := gtx.Constraints - textLayout := s.Layout(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max}) - lines := textLayout.Lines + lines := s.Layout(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max}) if max := l.MaxLines; max > 0 && len(lines) > max { lines = lines[:max] } @@ -100,7 +100,7 @@ func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt st Width: dims.Size.X, } for { - str, off, ok := it.Next() + start, end, layout, off, ok := it.Next() if !ok { break } @@ -108,7 +108,8 @@ func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt st var stack op.StackOp stack.Push(gtx.Ops) op.TransformOp{}.Offset(off).Add(gtx.Ops) - s.Shape(gtx, font, str).Add(gtx.Ops) + str := txt[start:end] + s.Shape(gtx, font, str, layout).Add(gtx.Ops) paint.PaintOp{Rect: lclip}.Add(gtx.Ops) stack.Pop() }