mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 16:35:36 +00:00
text: represent laid out text as strings to facilitate caching of layouts
Commit https://gioui.org/commit/b331407e81456 added text layout and shaping based on io.Reader and changed Editor to use it. Unfortunately, as ~inkeliz discovered, caching of shapes were also lost. ~inkeliz suggested fix, https://lists.sr.ht/~eliasnaur/gio-patches/patches/15059 adds caching of shapes to Editor to regain lost performance. This change repairs the cache to work on io.Reader API, in hope that the already complicated Editor won't need additional caching. Before this change, text layouts were represented as a slice of (rune, advance) pairs. Unfortunately, this representation doesn't lend itself to caching of shaping results, so change the representation of a line of text to be a pair of text and advances: package text type Layout { Text string Advances []fixed.Int26_6 } The Text field can then be used in a cache key, assuming Advances is consistent with it. The end result is that the two shaper variants of text.Shaper is reduced to just one, and the Len field field of text.Line is no longer needed. The changed representation adds a bit of extra work to package opentype. Cleaning that up is left as a future TODO. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+23
-20
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-16
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user