go.*,text,widget{,/material}: enable configurable line wrapping within words

This commit enables consumers of the text shaper to select a policy for how
line breaking candidates will be chosen. The new default policy can break lines
within "words" (UAX#14 segments) when words do not fit by themselves on a line.
This ensures that text does not horizontally overflow its bounding box unless
the available width is insufficient to display a single UAX#29 grapheme cluster.

Fixes: https://todo.sr.ht/~eliasnaur/gio/467
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-06-07 16:56:32 -04:00
committed by Elias Naur
parent a252394356
commit c6e4eecf21
12 changed files with 92 additions and 16 deletions
+13 -1
View File
@@ -401,11 +401,23 @@ func (s *shaperImpl) shapeText(faces []font.Face, ppem fixed.Int26_6, lc system.
return s.outScratchBuf
}
func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy {
switch p {
case WrapGraphemes:
return shaping.Always
case WrapWords:
return shaping.Never
default:
return shaping.WhenNecessary
}
}
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
wc := shaping.WrapConfig{
TruncateAfterLines: params.MaxLines,
TextContinues: params.forceTruncate,
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
}
if wc.TruncateAfterLines > 0 {
if len(params.Truncator) == 0 {
@@ -416,7 +428,7 @@ func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt
wc.Truncator = s.shapeText(faces, params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
}
// Wrap outputs into lines.
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, s.shapeText(faces, params.PxPerEm, params.Locale, txt)...)
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(faces, params.PxPerEm, params.Locale, txt)))
}
// replaceControlCharacters replaces problematic unicode
+18
View File
@@ -10,6 +10,7 @@ import (
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/shaping"
"golang.org/x/exp/slices"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/math/fixed"
@@ -235,6 +236,21 @@ func complexGlyph(cluster, runes, glyphs int) shaping.Glyph {
}
}
// copyLines performs a deep copy of the provided lines. This is necessary if you
// want to use the line wrapper again while also using the lines.
func copyLines(lines []shaping.Line) []shaping.Line {
out := make([]shaping.Line, len(lines))
for lineIdx, line := range lines {
lineCopy := make([]shaping.Output, len(line))
for runIdx, run := range line {
lineCopy[runIdx] = run
lineCopy[runIdx].Glyphs = slices.Clone(run.Glyphs)
}
out[lineIdx] = lineCopy
}
return out
}
// makeTestText creates a simple and complex(bidi) sample 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.
@@ -277,11 +293,13 @@ func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize,
MaxWidth: lineWidth,
Locale: locale,
}, []rune(simpleSource))
simpleText = copyLines(simpleText)
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
PxPerEm: fixed.I(fontSize),
MaxWidth: lineWidth,
Locale: locale,
}, []rune(complexSource))
complexText = copyLines(complexText)
testShaper(rtlFace, ltrFace)
return simpleText, complexText
}
+1
View File
@@ -159,6 +159,7 @@ type layoutKey struct {
locale system.Locale
font giofont.Font
forceTruncate bool
wrapPolicy WrapPolicy
}
type pathKey struct {
+25
View File
@@ -16,6 +16,27 @@ import (
"golang.org/x/image/math/fixed"
)
// WrapPolicy configures strategies for choosing where to break lines of text for line
// wrapping.
type WrapPolicy uint8
const (
// WrapHeuristically tries to minimize breaking within words (UAX#14 text segments)
// while also ensuring that text fits within the given MaxWidth. It will only break
// a line within a word (on a UAX#29 grapheme cluster boundary) when that word cannot
// fit on a line by itself. Additionally, when the final word of a line is being
// truncated, this policy will preserve as many symbols of that word as
// possible before the truncator.
WrapHeuristically WrapPolicy = iota
// WrapWords does not permit words (UAX#14 text segments) to be broken across lines.
// This means that sometimes long words will exceed the MaxWidth they are wrapped with.
WrapWords
// WrapGraphemes will maximize the amount of text on each line at the expense of readability,
// breaking any word across lines on UAX#29 grapheme cluster boundaries to maximize the number of
// grapheme clusters on each line.
WrapGraphemes
)
// Parameters are static text shaping attributes applied to the entire shaped text.
type Parameters struct {
// Font describes the preferred typeface.
@@ -32,6 +53,9 @@ type Parameters struct {
// truncated.
Truncator string
// WrapPolicy configures how line breaks will be chosen when wrapping text across lines.
WrapPolicy WrapPolicy
// MinWidth and MaxWidth provide the minimum and maximum horizontal space constraints
// for the shaped text.
MinWidth, MaxWidth int
@@ -318,6 +342,7 @@ func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte
locale: params.Locale,
font: params.Font,
forceTruncate: params.forceTruncate,
wrapPolicy: params.WrapPolicy,
str: asStr,
}
if l, ok := l.layoutCache.Get(lk); ok {