From 36e768e716d11193b34f24c1c5063f76afcd084f Mon Sep 17 00:00:00 2001 From: Chris Waldon Date: Thu, 16 Mar 2023 12:13:41 -0400 Subject: [PATCH] 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 --- widget/index.go | 16 +++++++++++++++- widget/index_test.go | 34 +++++++++++++++++++++++----------- widget/text.go | 2 +- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/widget/index.go b/widget/index.go index 58308c48..f8a00cb8 100644 --- a/widget/index.go +++ b/widget/index.go @@ -20,6 +20,7 @@ type lineInfo struct { } type glyphIndex struct { + // glyphs holds the glyphs processed. glyphs []text.Glyph // positions contain all possible caret positions, sorted by rune index. positions []combinedPos @@ -46,6 +47,20 @@ type glyphIndex struct { 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, // not pixels. type screenPos struct { @@ -90,7 +105,6 @@ func (g *glyphIndex) incrementPosition(pos combinedPos) (next combinedPos, eof b return g.positions[index+1], false } return candidate, true - } // Glyph indexes the provided glyph, generating text cursor positions for it. diff --git a/widget/index_test.go b/widget/index_test.go index 4cb99339..bfa9ab97 100644 --- a/widget/index_test.go +++ b/widget/index_test.go @@ -13,7 +13,7 @@ import ( // makePosTestText returns two bidi samples of shaped text at the given // font size and wrapped to the given line width. The runeLimit, if nonzero, // 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) 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() { bidiRTL = append(bidiRTL, g) } - return bidiLTR, bidiRTL + return bidiSource, bidiLTR, bidiRTL } // makeAccountingTestText shapes text designed to stress rune accounting // 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) rtlFace, _ := opentype.Parse(nsareg.TTF) @@ -62,11 +62,8 @@ func makeAccountingTestText(fontSize, lineWidth int) (txt []text.Glyph) { 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)} - shaper.LayoutString(params, 0, lineWidth, english, bidiSource) + shaper.LayoutString(params, 0, lineWidth, english, str) for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { txt = append(txt, g) } @@ -167,6 +164,7 @@ func TestIndexPositionWhitespace(t *testing.T) { t.Run(tc.name, func(t *testing.T) { glyphs := getGlyphs(16, 0, 200, tc.align, tc.str) var gi glyphIndex + gi.reset() for _, g := range glyphs { gi.Glyph(g) } @@ -194,7 +192,7 @@ func TestIndexPositionWhitespace(t *testing.T) { func TestIndexPositionBidi(t *testing.T) { fontSize := 16 lineWidth := fontSize * 10 - bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false) + _, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false) type testcase struct { name string glyphs []text.Glyph @@ -223,6 +221,7 @@ func TestIndexPositionBidi(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { var gi glyphIndex + gi.reset() for _, g := range tc.glyphs { gi.Glyph(g) } @@ -267,19 +266,22 @@ func TestIndexPositionBidi(t *testing.T) { }) } } + func TestIndexPositionLines(t *testing.T) { fontSize := 16 lineWidth := fontSize * 10 - bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false) - bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true) + source1, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false) + source2, bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true) type testcase struct { name string + source string glyphs []text.Glyph expectedLines []lineInfo } for _, tc := range []testcase{ { name: "bidi ltr", + source: source1, glyphs: bidiLTRText, expectedLines: []lineInfo{ { @@ -318,6 +320,7 @@ func TestIndexPositionLines(t *testing.T) { }, { name: "bidi rtl", + source: source1, glyphs: bidiRTLText, expectedLines: []lineInfo{ { @@ -348,6 +351,7 @@ func TestIndexPositionLines(t *testing.T) { }, { name: "bidi ltr opposite alignment", + source: source2, glyphs: bidiLTRTextOpp, expectedLines: []lineInfo{ { @@ -386,6 +390,7 @@ func TestIndexPositionLines(t *testing.T) { }, { name: "bidi rtl opposite alignment", + source: source2, glyphs: bidiRTLTextOpp, expectedLines: []lineInfo{ { @@ -417,6 +422,7 @@ func TestIndexPositionLines(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { var gi glyphIndex + gi.reset() for _, g := range tc.glyphs { gi.Glyph(g) } @@ -439,15 +445,20 @@ func TestIndexPositionLines(t *testing.T) { func TestIndexPositionRunes(t *testing.T) { fontSize := 16 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 { name string + source string glyphs []text.Glyph expected []combinedPos } for _, tc := range []testcase{ { name: "many newlines", + source: source, glyphs: testText, expected: []combinedPos{ {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) { var gi glyphIndex + gi.reset() for _, g := range tc.glyphs { gi.Glyph(g) } diff --git a/widget/text.go b/widget/text.go index 687c5ccd..59526b37 100644 --- a/widget/text.go +++ b/widget/text.go @@ -413,7 +413,7 @@ func (e *textView) layoutText(lt *text.Shaper) { e.maskReader.Reset(e, e.Mask) r = &e.maskReader } - e.index = glyphIndex{} + e.index.reset() it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}} if lt != nil { lt.Layout(text.Parameters{