text: fix EOF detection at newline boundaries

This commit tests and fixes some edge cases that threw off rune accounting
when a newline character was the final rune in the input *and* the text was
being truncated. I imagine they were never previously reported because it's
rare to try to truncate such text.

Thanks to Dominik Honnef for the report and reproducer.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-08-02 09:44:36 -04:00
parent edbf872b44
commit 80da4d6b02
2 changed files with 26 additions and 3 deletions
+4 -1
View File
@@ -316,7 +316,9 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
if !done {
_, re := l.reader.ReadByte()
done = re != nil
_ = l.reader.UnreadByte()
if !done {
_ = l.reader.UnreadByte()
}
}
} else {
idx := strings.IndexByte(str, '\n')
@@ -325,6 +327,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
endByte = len(str)
} else {
endByte = idx + 1
done = endByte == len(str)
}
}
if len(str[:endByte]) > 0 || (len(l.paragraph) > 0 || len(l.txt.lines) == 0) {
+22 -2
View File
@@ -6,6 +6,7 @@ import (
"testing"
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
"gioui.org/font"
"gioui.org/font/gofont"
"gioui.org/font/opentype"
"gioui.org/io/system"
@@ -161,6 +162,7 @@ func TestShapingNewlineHandling(t *testing.T) {
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
{textInput: "a\nb", expectedLines: 2, expectedGlyphs: 3},
{textInput: "", expectedLines: 1, expectedGlyphs: 1},
{textInput: "\n", expectedLines: 1, expectedGlyphs: 2},
} {
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) {
ltrFace, _ := opentype.Parse(goregular.TTF)
@@ -168,8 +170,13 @@ func TestShapingNewlineHandling(t *testing.T) {
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
checkGlyphs := func() {
glyphs := []Glyph{}
runes := 0
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g)
runes += g.Runes
}
if expected := len([]rune(tc.textInput)); expected != runes {
t.Errorf("expected %d runes, got %d", expected, runes)
}
if len(glyphs) != tc.expectedGlyphs {
t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs))
@@ -191,8 +198,8 @@ func TestShapingNewlineHandling(t *testing.T) {
}
breakX, breakY := breakGlyph.X, breakGlyph.Y
startX, startY := startGlyph.X, startGlyph.Y
if breakX == startX {
t.Errorf("expected paragraph start glyph to have cursor x")
if breakX == startX && idx != 0 {
t.Errorf("expected paragraph start glyph to have cursor x, got %v", startX)
}
if breakY == startY {
t.Errorf("expected paragraph start glyph to have cursor y")
@@ -464,6 +471,19 @@ func TestShapeStringRuneAccounting(t *testing.T) {
MaxWidth: 100,
},
},
{
name: "newline regression",
input: "\n",
params: Parameters{
Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal},
Alignment: Start,
PxPerEm: 768,
MaxLines: 1,
Truncator: "\u200b",
WrapPolicy: WrapHeuristically,
MaxWidth: 999929,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
for _, setup := range []setup{