widget: make glyphIndex reusable

This commit allows the glyph index type to be reset and reused, preventing the
reallocation of numerous buffers when indexing glyphs.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-03-16 12:13:41 -04:00
committed by Elias Naur
parent 25171df66a
commit 36e768e716
3 changed files with 39 additions and 13 deletions
+15 -1
View File
@@ -20,6 +20,7 @@ type lineInfo struct {
} }
type glyphIndex struct { type glyphIndex struct {
// glyphs holds the glyphs processed.
glyphs []text.Glyph glyphs []text.Glyph
// positions contain all possible caret positions, sorted by rune index. // positions contain all possible caret positions, sorted by rune index.
positions []combinedPos positions []combinedPos
@@ -46,6 +47,20 @@ type glyphIndex struct {
skipPrior bool skipPrior bool
} }
// reset prepares the index for reuse.
func (g *glyphIndex) reset() {
g.glyphs = g.glyphs[:0]
g.positions = g.positions[:0]
g.lines = g.lines[:0]
g.currentLineMin = 0
g.currentLineMax = 0
g.currentLineGlyphs = 0
g.pos = combinedPos{}
g.prog = 0
g.clusterAdvance = 0
g.skipPrior = false
}
// screenPos represents a character position in text line and column numbers, // screenPos represents a character position in text line and column numbers,
// not pixels. // not pixels.
type screenPos struct { type screenPos struct {
@@ -90,7 +105,6 @@ func (g *glyphIndex) incrementPosition(pos combinedPos) (next combinedPos, eof b
return g.positions[index+1], false return g.positions[index+1], false
} }
return candidate, true return candidate, true
} }
// Glyph indexes the provided glyph, generating text cursor positions for it. // Glyph indexes the provided glyph, generating text cursor positions for it.
+23 -11
View File
@@ -13,7 +13,7 @@ import (
// makePosTestText returns two bidi samples of shaped text at the given // makePosTestText returns two bidi samples of shaped text at the given
// font size and wrapped to the given line width. The runeLimit, if nonzero, // font size and wrapped to the given line width. The runeLimit, if nonzero,
// truncates the sample text to ensure shorter output for expensive tests. // truncates the sample text to ensure shorter output for expensive tests.
func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (bidiLTR, bidiRTL []text.Glyph) { func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string, bidiLTR, bidiRTL []text.Glyph) {
ltrFace, _ := opentype.Parse(goregular.TTF) ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF) rtlFace, _ := opentype.Parse(nsareg.TTF)
@@ -44,12 +44,12 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (bidiLTR, bidi
for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() {
bidiRTL = append(bidiRTL, g) bidiRTL = append(bidiRTL, g)
} }
return bidiLTR, bidiRTL return bidiSource, bidiLTR, bidiRTL
} }
// makeAccountingTestText shapes text designed to stress rune accounting // makeAccountingTestText shapes text designed to stress rune accounting
// logic within the index. // logic within the index.
func makeAccountingTestText(fontSize, lineWidth int) (txt []text.Glyph) { func makeAccountingTestText(str string, fontSize, lineWidth int) (txt []text.Glyph) {
ltrFace, _ := opentype.Parse(goregular.TTF) ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF) rtlFace, _ := opentype.Parse(nsareg.TTF)
@@ -62,11 +62,8 @@ func makeAccountingTestText(fontSize, lineWidth int) (txt []text.Glyph) {
Face: rtlFace, Face: rtlFace,
}, },
}) })
// bidiSource is crafted to contain multiple consecutive RTL runs (by
// changing scripts within the RTL).
bidiSource := "The\nquick سماء של\nום لا fox\nتمط של\nום."
params := text.Parameters{PxPerEm: fixed.I(fontSize)} params := text.Parameters{PxPerEm: fixed.I(fontSize)}
shaper.LayoutString(params, 0, lineWidth, english, bidiSource) shaper.LayoutString(params, 0, lineWidth, english, str)
for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() {
txt = append(txt, g) txt = append(txt, g)
} }
@@ -167,6 +164,7 @@ func TestIndexPositionWhitespace(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
glyphs := getGlyphs(16, 0, 200, tc.align, tc.str) glyphs := getGlyphs(16, 0, 200, tc.align, tc.str)
var gi glyphIndex var gi glyphIndex
gi.reset()
for _, g := range glyphs { for _, g := range glyphs {
gi.Glyph(g) gi.Glyph(g)
} }
@@ -194,7 +192,7 @@ func TestIndexPositionWhitespace(t *testing.T) {
func TestIndexPositionBidi(t *testing.T) { func TestIndexPositionBidi(t *testing.T) {
fontSize := 16 fontSize := 16
lineWidth := fontSize * 10 lineWidth := fontSize * 10
bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false) _, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
type testcase struct { type testcase struct {
name string name string
glyphs []text.Glyph glyphs []text.Glyph
@@ -223,6 +221,7 @@ func TestIndexPositionBidi(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var gi glyphIndex var gi glyphIndex
gi.reset()
for _, g := range tc.glyphs { for _, g := range tc.glyphs {
gi.Glyph(g) gi.Glyph(g)
} }
@@ -267,19 +266,22 @@ func TestIndexPositionBidi(t *testing.T) {
}) })
} }
} }
func TestIndexPositionLines(t *testing.T) { func TestIndexPositionLines(t *testing.T) {
fontSize := 16 fontSize := 16
lineWidth := fontSize * 10 lineWidth := fontSize * 10
bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false) source1, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true) source2, bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true)
type testcase struct { type testcase struct {
name string name string
source string
glyphs []text.Glyph glyphs []text.Glyph
expectedLines []lineInfo expectedLines []lineInfo
} }
for _, tc := range []testcase{ for _, tc := range []testcase{
{ {
name: "bidi ltr", name: "bidi ltr",
source: source1,
glyphs: bidiLTRText, glyphs: bidiLTRText,
expectedLines: []lineInfo{ expectedLines: []lineInfo{
{ {
@@ -318,6 +320,7 @@ func TestIndexPositionLines(t *testing.T) {
}, },
{ {
name: "bidi rtl", name: "bidi rtl",
source: source1,
glyphs: bidiRTLText, glyphs: bidiRTLText,
expectedLines: []lineInfo{ expectedLines: []lineInfo{
{ {
@@ -348,6 +351,7 @@ func TestIndexPositionLines(t *testing.T) {
}, },
{ {
name: "bidi ltr opposite alignment", name: "bidi ltr opposite alignment",
source: source2,
glyphs: bidiLTRTextOpp, glyphs: bidiLTRTextOpp,
expectedLines: []lineInfo{ expectedLines: []lineInfo{
{ {
@@ -386,6 +390,7 @@ func TestIndexPositionLines(t *testing.T) {
}, },
{ {
name: "bidi rtl opposite alignment", name: "bidi rtl opposite alignment",
source: source2,
glyphs: bidiRTLTextOpp, glyphs: bidiRTLTextOpp,
expectedLines: []lineInfo{ expectedLines: []lineInfo{
{ {
@@ -417,6 +422,7 @@ func TestIndexPositionLines(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var gi glyphIndex var gi glyphIndex
gi.reset()
for _, g := range tc.glyphs { for _, g := range tc.glyphs {
gi.Glyph(g) gi.Glyph(g)
} }
@@ -439,15 +445,20 @@ func TestIndexPositionLines(t *testing.T) {
func TestIndexPositionRunes(t *testing.T) { func TestIndexPositionRunes(t *testing.T) {
fontSize := 16 fontSize := 16
lineWidth := fontSize * 10 lineWidth := fontSize * 10
testText := makeAccountingTestText(fontSize, lineWidth) // source is crafted to contain multiple consecutive RTL runs (by
// changing scripts within the RTL).
source := "The\nquick سماء של\nום لا fox\nتمط של\nום."
testText := makeAccountingTestText(source, fontSize, lineWidth)
type testcase struct { type testcase struct {
name string name string
source string
glyphs []text.Glyph glyphs []text.Glyph
expected []combinedPos expected []combinedPos
} }
for _, tc := range []testcase{ for _, tc := range []testcase{
{ {
name: "many newlines", name: "many newlines",
source: source,
glyphs: testText, glyphs: testText,
expected: []combinedPos{ expected: []combinedPos{
{runes: 0, lineCol: screenPos{line: 0, col: 0}, runIndex: 0, towardOrigin: false}, {runes: 0, lineCol: screenPos{line: 0, col: 0}, runIndex: 0, towardOrigin: false},
@@ -496,6 +507,7 @@ func TestIndexPositionRunes(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var gi glyphIndex var gi glyphIndex
gi.reset()
for _, g := range tc.glyphs { for _, g := range tc.glyphs {
gi.Glyph(g) gi.Glyph(g)
} }
+1 -1
View File
@@ -413,7 +413,7 @@ func (e *textView) layoutText(lt *text.Shaper) {
e.maskReader.Reset(e, e.Mask) e.maskReader.Reset(e, e.Mask)
r = &e.maskReader r = &e.maskReader
} }
e.index = glyphIndex{} e.index.reset()
it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}} it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}}
if lt != nil { if lt != nil {
lt.Layout(text.Parameters{ lt.Layout(text.Parameters{