diff --git a/text/shaper.go b/text/shaper.go index 6f02701a..114b174e 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -247,9 +247,11 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) { break } } - _, re := l.reader.ReadByte() - done = re != nil - _ = l.reader.UnreadByte() + if !done { + _, re := l.reader.ReadByte() + done = re != nil + _ = l.reader.UnreadByte() + } } else { idx := strings.IndexByte(str, '\n') if idx == -1 { diff --git a/text/shaper_test.go b/text/shaper_test.go index b2926070..05cb694f 100644 --- a/text/shaper_test.go +++ b/text/shaper_test.go @@ -6,6 +6,7 @@ import ( "testing" nsareg "eliasnaur.com/font/noto/sans/arabic/regular" + "gioui.org/font/gofont" "gioui.org/font/opentype" "gioui.org/io/system" "golang.org/x/exp/slices" @@ -431,3 +432,71 @@ func printLinePositioning(t *testing.T, lines []line, glyphs []Glyph) { } } } + +// TestShapeStringRuneAccounting tries shaping the same string/parameter combinations with both +// shaping methods and ensures that the resulting glyph stream always has the right number of +// runes accounted for. +func TestShapeStringRuneAccounting(t *testing.T) { + type testcase struct { + name string + input string + params Parameters + } + type setup struct { + kind string + do func(*Shaper, Parameters, string) + } + for _, tc := range []testcase{ + { + name: "simple truncated", + input: "abc", + params: Parameters{ + PxPerEm: fixed.Int26_6(16), + MaxWidth: 100, + MaxLines: 1, + }, + }, + { + name: "simple", + input: "abc", + params: Parameters{ + PxPerEm: fixed.Int26_6(16), + MaxWidth: 100, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + for _, setup := range []setup{ + { + kind: "LayoutString", + do: func(shaper *Shaper, params Parameters, input string) { + shaper.LayoutString(params, input) + }, + }, + { + kind: "Layout", + do: func(shaper *Shaper, params Parameters, input string) { + shaper.Layout(params, strings.NewReader(input)) + }, + }, + } { + t.Run(setup.kind, func(t *testing.T) { + shaper := NewShaper(gofont.Collection()) + setup.do(shaper, tc.params, tc.input) + + glyphs := []Glyph{} + for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { + glyphs = append(glyphs, g) + } + totalRunes := 0 + for _, g := range glyphs { + totalRunes += g.Runes + } + if inputRunes := len([]rune(tc.input)); totalRunes != inputRunes { + t.Errorf("input contained %d runes, but glyphs contained %d", inputRunes, totalRunes) + } + }) + } + }) + } +}