text,widget{,/material}: [API] move all shaping parameters into text.Parameters

This commit moves the min/max width of shaped text and the text's Locale into
text.Parameters. They were previously passed as separate function parameters to
the shaper, but this made little sense and added visual noise. This is a breaking
change, but only if you previously invoked the shaping API directly.

Callers of text.(*Shaper).LayoutString should change:

    shaper.LayoutString(params, minWidth, maxWidth, locale, "string")

to

    params.MinWidth=minWidth
    params.MaxWidth=maxWidth
    params.Locale=locale
    shaper.LayoutString(params, "string")

Callers of text.(*Shaper).Layout should do likewise.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-03-27 16:23:44 -04:00
committed by Elias Naur
parent 9d0a53fc9f
commit 7e8c10927b
10 changed files with 191 additions and 90 deletions
+11 -11
View File
@@ -401,7 +401,7 @@ func (s *shaperImpl) shapeText(faces []font.Face, ppem fixed.Int26_6, lc system.
}
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, maxWidth int, lc system.Locale, txt []rune) (_ []shaping.Line, truncated int) {
func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
wc := shaping.WrapConfig{
TruncateAfterLines: params.MaxLines,
}
@@ -411,10 +411,10 @@ func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, maxW
}
// We only permit a single run as the truncator, regardless of whether more were generated.
// Just use the first one.
wc.Truncator = s.shapeText(faces, params.PxPerEm, lc, []rune(params.Truncator))[0]
wc.Truncator = s.shapeText(faces, params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
}
// Wrap outputs into lines.
return s.wrapper.WrapParagraph(wc, maxWidth, txt, s.shapeText(faces, params.PxPerEm, lc, txt)...)
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, s.shapeText(faces, params.PxPerEm, params.Locale, txt)...)
}
// replaceControlCharacters replaces problematic unicode
@@ -443,17 +443,17 @@ func replaceControlCharacters(in []rune) []rune {
}
// Layout shapes and wraps the text, and returns the result in Gio's shaped text format.
func (s *shaperImpl) LayoutString(params Parameters, minWidth, maxWidth int, lc system.Locale, txt string) document {
return s.LayoutRunes(params, minWidth, maxWidth, lc, []rune(txt))
func (s *shaperImpl) LayoutString(params Parameters, txt string) document {
return s.LayoutRunes(params, []rune(txt))
}
// Layout shapes and wraps the text, and returns the result in Gio's shaped text format.
func (s *shaperImpl) Layout(params Parameters, minWidth, maxWidth int, lc system.Locale, txt io.RuneReader) document {
func (s *shaperImpl) Layout(params Parameters, txt io.RuneReader) document {
s.scratchRunes = s.scratchRunes[:0]
for r, _, err := txt.ReadRune(); err != nil; r, _, err = txt.ReadRune() {
s.scratchRunes = append(s.scratchRunes, r)
}
return s.LayoutRunes(params, minWidth, maxWidth, lc, s.scratchRunes)
return s.LayoutRunes(params, s.scratchRunes)
}
func calculateYOffsets(lines []line) {
@@ -468,12 +468,12 @@ func calculateYOffsets(lines []line) {
}
// LayoutRunes shapes and wraps the text, and returns the result in Gio's shaped text format.
func (s *shaperImpl) LayoutRunes(params Parameters, minWidth, maxWidth int, lc system.Locale, txt []rune) document {
func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
hasNewline := len(txt) > 0 && txt[len(txt)-1] == '\n'
if hasNewline {
txt = txt[:len(txt)-1]
}
ls, truncated := s.shapeAndWrapText(s.orderer.sortedFacesForStyle(params.Font), params, maxWidth, lc, replaceControlCharacters(txt))
ls, truncated := s.shapeAndWrapText(s.orderer.sortedFacesForStyle(params.Font), params, replaceControlCharacters(txt))
if truncated > 0 && hasNewline {
// We've truncated the newline, since it was at the end and we've truncated some amount of runes
@@ -484,7 +484,7 @@ func (s *shaperImpl) LayoutRunes(params Parameters, minWidth, maxWidth int, lc s
// Convert to Lines.
textLines := make([]line, len(ls))
for i := range ls {
otLine := toLine(&s.orderer, ls[i], lc.Direction)
otLine := toLine(&s.orderer, ls[i], params.Locale.Direction)
isFinalLine := i == len(ls)-1
if isFinalLine && hasNewline {
// If there was a trailing newline update the rune counts to include
@@ -536,7 +536,7 @@ func (s *shaperImpl) LayoutRunes(params Parameters, minWidth, maxWidth int, lc s
return document{
lines: textLines,
alignment: params.Alignment,
alignWidth: alignWidth(minWidth, textLines),
alignWidth: alignWidth(params.MinWidth, textLines),
}
}
+38 -10
View File
@@ -37,7 +37,11 @@ func TestEmptyString(t *testing.T) {
ltrFace, _ := opentype.Parse(goregular.TTF)
shaper := testShaper(ltrFace)
lines := shaper.LayoutRunes(Parameters{PxPerEm: ppem}, 0, 2000, english, []rune{})
lines := shaper.LayoutRunes(Parameters{
PxPerEm: ppem,
MaxWidth: 2000,
Locale: english,
}, []rune{})
if len(lines.lines) == 0 {
t.Fatalf("Layout returned no lines for empty string; expected 1")
}
@@ -110,7 +114,11 @@ func TestShapingAlignWidth(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
lines := shaper.LayoutString(Parameters{PxPerEm: ppem}, tc.minWidth, tc.maxWidth, english, tc.str)
lines := shaper.LayoutString(Parameters{PxPerEm: ppem,
MinWidth: tc.minWidth,
MaxWidth: tc.maxWidth,
Locale: english,
}, tc.str)
if lines.alignWidth != tc.expected {
t.Errorf("expected line alignWidth to be %d, got %d", tc.expected, lines.alignWidth)
}
@@ -155,7 +163,11 @@ func TestNewlineSynthesis(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
doc := shaper.LayoutRunes(Parameters{PxPerEm: ppem}, 0, 200, tc.locale, []rune(tc.txt))
doc := shaper.LayoutRunes(Parameters{
PxPerEm: ppem,
MaxWidth: 200,
Locale: tc.locale,
}, []rune(tc.txt))
for lineIdx, line := range doc.lines {
lastRunIdx := len(line.runs) - 1
lastRun := line.runs[lastRunIdx]
@@ -256,8 +268,16 @@ func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize,
rtlSource = string(complexRunes[:runeLimit])
}
}
simpleText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(Font{}), Parameters{PxPerEm: fixed.I(fontSize)}, lineWidth, locale, []rune(simpleSource))
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(Font{}), Parameters{PxPerEm: fixed.I(fontSize)}, lineWidth, locale, []rune(complexSource))
simpleText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(Font{}), Parameters{
PxPerEm: fixed.I(fontSize),
MaxWidth: lineWidth,
Locale: locale,
}, []rune(simpleSource))
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(Font{}), Parameters{
PxPerEm: fixed.I(fontSize),
MaxWidth: lineWidth,
Locale: locale,
}, []rune(complexSource))
testShaper(rtlFace, ltrFace)
return simpleText, complexText
}
@@ -536,7 +556,11 @@ func FuzzLayout(f *testing.F) {
if fontSize < 1 {
fontSize = 1
}
lines := shaper.LayoutRunes(Parameters{PxPerEm: fixed.I(int(fontSize))}, 0, int(width), locale, []rune(txt))
lines := shaper.LayoutRunes(Parameters{
PxPerEm: fixed.I(int(fontSize)),
MaxWidth: int(width),
Locale: locale,
}, []rune(txt))
validateLines(t, lines.lines, len([]rune(txt)))
})
}
@@ -595,11 +619,15 @@ func TestTextAppend(t *testing.T) {
shaper := testShaper(ltrFace, rtlFace)
text1 := shaper.LayoutString(Parameters{
PxPerEm: fixed.I(14),
}, 0, 200, english, "د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.")
PxPerEm: fixed.I(14),
MaxWidth: 200,
Locale: english,
}, "د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.")
text2 := shaper.LayoutString(Parameters{
PxPerEm: fixed.I(14),
}, 0, 200, english, "د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.")
PxPerEm: fixed.I(14),
MaxWidth: 200,
Locale: english,
}, "د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.")
text1.append(text2)
curY := math.MinInt
+19 -14
View File
@@ -31,6 +31,11 @@ type Parameters struct {
// can currently ohly happen if MaxLines is nonzero and the text on the final line is
// truncated.
Truncator string
// MinWidth and MaxWidth provide the minimum and maximum horizontal space constraints
// for the shaped text.
MinWidth, MaxWidth int
// Locale provides primary direction and language information for the shaped text.
Locale system.Locale
}
// A FontFace is a Font and a matching Face.
@@ -194,13 +199,13 @@ func NewShaper(collection []FontFace) *Shaper {
// Layout text from an io.Reader according to a set of options. Results can be retrieved by
// iteratively calling NextGlyph.
func (l *Shaper) Layout(params Parameters, minWidth, maxWidth int, lc system.Locale, txt io.Reader) {
l.layoutText(params, minWidth, maxWidth, lc, bufio.NewReader(txt), "")
func (l *Shaper) Layout(params Parameters, txt io.Reader) {
l.layoutText(params, bufio.NewReader(txt), "")
}
// LayoutString is Layout for strings.
func (l *Shaper) LayoutString(params Parameters, minWidth, maxWidth int, lc system.Locale, str string) {
l.layoutText(params, minWidth, maxWidth, lc, nil, str)
func (l *Shaper) LayoutString(params Parameters, str string) {
l.layoutText(params, nil, str)
}
func (l *Shaper) reset(align Alignment) {
@@ -213,10 +218,10 @@ func (l *Shaper) reset(align Alignment) {
// layoutText lays out a large text document by breaking it into paragraphs and laying
// out each of them separately. This allows the shaping results to be cached independently
// by paragraph. Only one of txt and str should be provided.
func (l *Shaper) layoutText(params Parameters, minWidth, maxWidth int, lc system.Locale, txt io.RuneReader, str string) {
func (l *Shaper) layoutText(params Parameters, txt io.RuneReader, str string) {
l.reset(params.Alignment)
if txt == nil && len(str) == 0 {
l.txt.append(l.layoutParagraph(params, minWidth, maxWidth, lc, "", nil))
l.txt.append(l.layoutParagraph(params, "", nil))
return
}
truncating := params.MaxLines > 0
@@ -250,7 +255,7 @@ func (l *Shaper) layoutText(params Parameters, minWidth, maxWidth int, lc system
done = endByte == len(str)
}
if startByte != endByte || (len(l.paragraph) > 0 || len(l.txt.lines) == 0) {
lines := l.layoutParagraph(params, minWidth, maxWidth, lc, str[startByte:endByte], l.paragraph)
lines := l.layoutParagraph(params, str[startByte:endByte], l.paragraph)
if truncating {
params.MaxLines -= len(lines.lines)
if params.MaxLines == 0 {
@@ -285,7 +290,7 @@ func (l *Shaper) layoutText(params Parameters, minWidth, maxWidth int, lc system
}
}
func (l *Shaper) layoutParagraph(params Parameters, minWidth, maxWidth int, lc system.Locale, asStr string, asRunes []rune) document {
func (l *Shaper) layoutParagraph(params Parameters, asStr string, asRunes []rune) document {
if l == nil {
return document{}
}
@@ -294,14 +299,14 @@ func (l *Shaper) layoutParagraph(params Parameters, minWidth, maxWidth int, lc s
}
// Alignment is not part of the cache key because changing it does not impact shaping.
lk := layoutKey{
truncator: params.Truncator,
ppem: params.PxPerEm,
maxWidth: maxWidth,
minWidth: minWidth,
maxWidth: params.MaxWidth,
minWidth: params.MinWidth,
maxLines: params.MaxLines,
str: asStr,
locale: lc,
truncator: params.Truncator,
locale: params.Locale,
font: params.Font,
str: asStr,
}
if l, ok := l.layoutCache.Get(lk); ok {
return l
@@ -309,7 +314,7 @@ func (l *Shaper) layoutParagraph(params Parameters, minWidth, maxWidth int, lc s
if len(asRunes) == 0 && len(asStr) > 0 {
asRunes = []rune(asStr)
}
lines := l.shaper.LayoutRunes(params, minWidth, maxWidth, lc, asRunes)
lines := l.shaper.LayoutRunes(params, asRunes)
l.layoutCache.Put(lk, lines)
return lines
}
+38 -14
View File
@@ -25,7 +25,10 @@ func TestWrappingTruncation(t *testing.T) {
cache.LayoutString(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
}, 200, 200, english, textInput)
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, textInput)
untruncatedCount := len(cache.txt.lines)
for i := untruncatedCount + 1; i > 0; i-- {
@@ -34,7 +37,10 @@ func TestWrappingTruncation(t *testing.T) {
Alignment: Middle,
PxPerEm: fixed.I(10),
MaxLines: i,
}, 200, 200, english, textInput)
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, textInput)
lineCount := 0
lastGlyphWasLineBreak := false
glyphs := []Glyph{}
@@ -130,7 +136,10 @@ func TestShapingNewlineHandling(t *testing.T) {
cache.LayoutString(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
}, 200, 200, english, tc.textInput)
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, tc.textInput)
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
}
@@ -139,7 +148,10 @@ func TestShapingNewlineHandling(t *testing.T) {
cache.Layout(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
}, 200, 200, english, strings.NewReader(tc.textInput))
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, strings.NewReader(tc.textInput))
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
}
@@ -157,7 +169,10 @@ func TestCacheEmptyString(t *testing.T) {
cache.LayoutString(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
}, 200, 200, english, "")
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, "")
glyphs := make([]Glyph, 0, 1)
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g)
@@ -190,31 +205,38 @@ 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")
params := Parameters{
Alignment: Start,
PxPerEm: fixed.I(10),
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}
cache.LayoutString(params, "A")
glyph, _ := cache.NextGlyph()
startX := glyph.X
params.Alignment = Middle
cache.LayoutString(params, 200, 200, english, "A")
cache.LayoutString(params, "A")
glyph, _ = cache.NextGlyph()
middleX := glyph.X
params.Alignment = End
cache.LayoutString(params, 200, 200, english, "A")
cache.LayoutString(params, "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.Locale = arabic
params.Alignment = Start
cache.LayoutString(params, 200, 200, arabic, "A")
cache.LayoutString(params, "A")
glyph, _ = cache.NextGlyph()
rtlStartX := glyph.X
params.Alignment = Middle
cache.LayoutString(params, 200, 200, arabic, "A")
cache.LayoutString(params, "A")
glyph, _ = cache.NextGlyph()
rtlMiddleX := glyph.X
params.Alignment = End
cache.LayoutString(params, 200, 200, arabic, "A")
cache.LayoutString(params, "A")
glyph, _ = cache.NextGlyph()
rtlEndX := glyph.X
if rtlStartX == rtlMiddleX || rtlStartX == rtlEndX || rtlEndX == rtlMiddleX {
@@ -250,8 +272,10 @@ func TestCacheGlyphConverstion(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
cache := NewShaper(collection)
cache.LayoutString(Parameters{
PxPerEm: fixed.I(10),
}, 0, 200, tc.locale, tc.text)
PxPerEm: fixed.I(10),
MaxWidth: 200,
Locale: tc.locale,
}, tc.text)
doc := cache.txt
glyphs := make([]Glyph, 0, len(tc.expected))
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {