mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
2db1a7bfb9
This commit pushes limiting the maximum number of lines of text into the shaper implementation. This is more efficient than doing it in widgets, and also opens the door for future use of the shaper to insert ellipsis and other truncating characters as appropriate. I realized that we lost the implementation of limiting the number of lines of text in my text stack overhaul, so this fixes a regression from that work. Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
234 lines
7.8 KiB
Go
234 lines
7.8 KiB
Go
package text
|
|
|
|
import (
|
|
"testing"
|
|
|
|
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
|
|
"gioui.org/font/opentype"
|
|
"gioui.org/io/system"
|
|
"golang.org/x/image/font/gofont/goregular"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
// TestWrappingTruncation checks that the line wrapper's truncation features
|
|
// behave as expected.
|
|
func TestWrappingTruncation(t *testing.T) {
|
|
// Use a test string containing multiple newlines to ensure that they are shaped
|
|
// as separate paragraphs.
|
|
textInput := "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua."
|
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
|
collection := []FontFace{{Face: ltrFace}}
|
|
cache := NewShaper(collection)
|
|
cache.LayoutString(Parameters{
|
|
Alignment: Middle,
|
|
PxPerEm: fixed.I(10),
|
|
}, 200, 200, english, textInput)
|
|
untruncatedCount := len(cache.txt.lines)
|
|
|
|
for i := untruncatedCount + 1; i > 0; i-- {
|
|
cache.LayoutString(Parameters{
|
|
Alignment: Middle,
|
|
PxPerEm: fixed.I(10),
|
|
MaxLines: i,
|
|
}, 200, 200, english, textInput)
|
|
lineCount := len(cache.txt.lines)
|
|
if i <= untruncatedCount && lineCount != i {
|
|
t.Errorf("expected %d lines, got %d", i, lineCount)
|
|
} else if i > untruncatedCount && lineCount != untruncatedCount {
|
|
t.Errorf("expected %d lines, got %d", untruncatedCount, lineCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCacheEmptyString ensures that shaping the empty string returns a
|
|
// single synthetic glyph with ascent/descent info.
|
|
func TestCacheEmptyString(t *testing.T) {
|
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
|
collection := []FontFace{{Face: ltrFace}}
|
|
cache := NewShaper(collection)
|
|
cache.LayoutString(Parameters{
|
|
Alignment: Middle,
|
|
PxPerEm: fixed.I(10),
|
|
}, 200, 200, english, "")
|
|
glyphs := make([]Glyph, 0, 1)
|
|
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
|
|
glyphs = append(glyphs, g)
|
|
}
|
|
if len(glyphs) != 1 {
|
|
t.Errorf("expected %d glyphs, got %d", 1, len(glyphs))
|
|
}
|
|
glyph := glyphs[0]
|
|
checkFlag(t, true, FlagClusterBreak, glyph, 0)
|
|
checkFlag(t, true, FlagRunBreak, glyph, 0)
|
|
checkFlag(t, true, FlagLineBreak, glyph, 0)
|
|
checkFlag(t, true, FlagSynthetic, glyph, 0)
|
|
if glyph.Ascent == 0 {
|
|
t.Errorf("expected non-zero ascent")
|
|
}
|
|
if glyph.Descent == 0 {
|
|
t.Errorf("expected non-zero descent")
|
|
}
|
|
if glyph.Y == 0 {
|
|
t.Errorf("expected non-zero y offset")
|
|
}
|
|
if glyph.X == 0 {
|
|
t.Errorf("expected non-zero x offset")
|
|
}
|
|
}
|
|
|
|
// TestCacheAlignment ensures that shaping with different alignments or dominant
|
|
// text directions results in different X offsets.
|
|
func TestCacheAlignment(t *testing.T) {
|
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
|
collection := []FontFace{{Face: ltrFace}}
|
|
cache := NewShaper(collection)
|
|
params := Parameters{Alignment: Start, PxPerEm: fixed.I(10)}
|
|
cache.LayoutString(params, 200, 200, english, "A")
|
|
glyph, _ := cache.NextGlyph()
|
|
startX := glyph.X
|
|
params.Alignment = Middle
|
|
cache.LayoutString(params, 200, 200, english, "A")
|
|
glyph, _ = cache.NextGlyph()
|
|
middleX := glyph.X
|
|
params.Alignment = End
|
|
cache.LayoutString(params, 200, 200, english, "A")
|
|
glyph, _ = cache.NextGlyph()
|
|
endX := glyph.X
|
|
if startX == middleX || startX == endX || endX == middleX {
|
|
t.Errorf("[LTR] shaping with with different alignments should not produce the same X, start %d, middle %d, end %d", startX, middleX, endX)
|
|
}
|
|
params.Alignment = Start
|
|
cache.LayoutString(params, 200, 200, arabic, "A")
|
|
glyph, _ = cache.NextGlyph()
|
|
rtlStartX := glyph.X
|
|
params.Alignment = Middle
|
|
cache.LayoutString(params, 200, 200, arabic, "A")
|
|
glyph, _ = cache.NextGlyph()
|
|
rtlMiddleX := glyph.X
|
|
params.Alignment = End
|
|
cache.LayoutString(params, 200, 200, arabic, "A")
|
|
glyph, _ = cache.NextGlyph()
|
|
rtlEndX := glyph.X
|
|
if rtlStartX == rtlMiddleX || rtlStartX == rtlEndX || rtlEndX == rtlMiddleX {
|
|
t.Errorf("[RTL] shaping with with different alignments should not produce the same X, start %d, middle %d, end %d", rtlStartX, rtlMiddleX, rtlEndX)
|
|
}
|
|
if startX == rtlStartX || endX == rtlEndX {
|
|
t.Errorf("shaping with with different dominant text directions and the same alignment should not produce the same X unless it's middle-aligned")
|
|
}
|
|
}
|
|
|
|
func TestCacheGlyphConverstion(t *testing.T) {
|
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
|
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
|
collection := []FontFace{{Face: ltrFace}, {Face: rtlFace}}
|
|
type testcase struct {
|
|
name string
|
|
text string
|
|
locale system.Locale
|
|
expected []Glyph
|
|
}
|
|
for _, tc := range []testcase{
|
|
{
|
|
name: "bidi ltr",
|
|
text: "The quick سماء שלום لا fox تمط שלום\nغير the\nlazy dog.",
|
|
locale: english,
|
|
},
|
|
{
|
|
name: "bidi rtl",
|
|
text: "الحب سماء brown привет fox تمط jumps\nпривет over\nغير الأحلام.",
|
|
locale: arabic,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cache := NewShaper(collection)
|
|
cache.LayoutString(Parameters{
|
|
PxPerEm: fixed.I(10),
|
|
}, 0, 200, tc.locale, tc.text)
|
|
doc := cache.txt
|
|
glyphs := make([]Glyph, 0, len(tc.expected))
|
|
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
|
|
glyphs = append(glyphs, g)
|
|
}
|
|
glyphCursor := 0
|
|
for _, line := range doc.lines {
|
|
for runIdx, run := range line.runs {
|
|
lastRun := runIdx == len(line.runs)-1
|
|
start := 0
|
|
end := len(run.Glyphs) - 1
|
|
inc := 1
|
|
towardOrigin := false
|
|
if run.Direction.Progression() == system.TowardOrigin {
|
|
start = len(run.Glyphs) - 1
|
|
end = 0
|
|
inc = -1
|
|
towardOrigin = true
|
|
}
|
|
for glyphIdx := start; ; glyphIdx += inc {
|
|
endOfRun := glyphIdx == end
|
|
glyph := run.Glyphs[glyphIdx]
|
|
endOfCluster := glyphIdx == end || run.Glyphs[glyphIdx+inc].clusterIndex != glyph.clusterIndex
|
|
|
|
actual := glyphs[glyphCursor]
|
|
if actual.ID != glyph.id {
|
|
t.Errorf("glyphs[%d] expected id %d, got id %d", glyphCursor, glyph.id, actual.ID)
|
|
}
|
|
// Synthetic glyphs should only ever show up at the end of lines.
|
|
endOfLine := lastRun && endOfRun
|
|
synthetic := glyph.glyphCount == 0 && endOfLine
|
|
checkFlag(t, endOfLine, FlagLineBreak, actual, glyphCursor)
|
|
checkFlag(t, endOfRun, FlagRunBreak, actual, glyphCursor)
|
|
checkFlag(t, towardOrigin, FlagTowardOrigin, actual, glyphCursor)
|
|
checkFlag(t, synthetic, FlagSynthetic, actual, glyphCursor)
|
|
checkFlag(t, endOfCluster, FlagClusterBreak, actual, glyphCursor)
|
|
glyphCursor++
|
|
if glyphIdx == end {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
printLinePositioning(t, doc.lines, glyphs)
|
|
})
|
|
}
|
|
}
|
|
|
|
func checkFlag(t *testing.T, shouldHave bool, flag Flags, actual Glyph, glyphCursor int) {
|
|
t.Helper()
|
|
if shouldHave && actual.Flags&flag == 0 {
|
|
t.Errorf("glyphs[%d] should have %s set", glyphCursor, flag)
|
|
} else if !shouldHave && actual.Flags&flag != 0 {
|
|
t.Errorf("glyphs[%d] should not have %s set", glyphCursor, flag)
|
|
}
|
|
}
|
|
|
|
func printLinePositioning(t *testing.T, lines []line, glyphs []Glyph) {
|
|
t.Helper()
|
|
glyphCursor := 0
|
|
for i, line := range lines {
|
|
t.Logf("line %d, dir %s, width %d, visual %v, runeCount: %d", i, line.direction, line.width, line.visualOrder, line.runeCount)
|
|
for k, run := range line.runs {
|
|
t.Logf("run: %d, dir %s, width %d, runes {count: %d, offset: %d}", k, run.Direction, run.Advance, run.Runes.Count, run.Runes.Offset)
|
|
start := 0
|
|
end := len(run.Glyphs) - 1
|
|
inc := 1
|
|
if run.Direction.Progression() == system.TowardOrigin {
|
|
start = len(run.Glyphs) - 1
|
|
end = 0
|
|
inc = -1
|
|
}
|
|
for g := start; ; g += inc {
|
|
glyph := run.Glyphs[g]
|
|
if glyphCursor < len(glyphs) {
|
|
t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - glyphs[%2d] flags %s", g, glyph.xAdvance, glyph.runeCount, glyph.glyphCount, glyphCursor, glyphs[glyphCursor].Flags)
|
|
t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - n/a", g, glyph.xAdvance, glyph.runeCount, glyph.glyphCount)
|
|
}
|
|
glyphCursor++
|
|
if g == end {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|