M go.mod => go.mod +1 -1
@@ 6,7 6,7 @@ require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.6
- github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae
+ github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.5.0
M go.sum => go.sum +2 -2
@@ 5,8 5,8 @@ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJG
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
-github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae h1:LCcaQgYrnS+sx9Tc3oGUvbRBRt+5oFnKWakaxeAvNVI=
-github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA=
+github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433 h1:Pdyvqsfi1QYgFfZa4R8otBOtgO+CGyBDMEG8cM3jwvE=
+github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA=
github.com/go-text/typesetting-utils v0.0.0-20230412163830-89e4bcfa3ecc h1:9Kf84pnrmmjdRzZIkomfjowmGUhHs20jkrWYw/I6CYc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
M text/gotext.go => text/gotext.go +13 -1
@@ 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
M text/gotext_test.go => text/gotext_test.go +18 -0
@@ 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
}
M text/lru.go => text/lru.go +1 -0
@@ 159,6 159,7 @@ type layoutKey struct {
locale system.Locale
font giofont.Font
forceTruncate bool
+ wrapPolicy WrapPolicy
}
type pathKey struct {
M text/shaper.go => text/shaper.go +25 -0
@@ 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 {
M widget/editor.go => widget/editor.go +3 -0
@@ 57,6 57,8 @@ type Editor struct {
// Filter is the list of characters allowed in the Editor. If Filter is empty,
// all characters are allowed.
Filter string
+ // WrapPolicy configures how displayed text will be broken into lines.
+ WrapPolicy text.WrapPolicy
buffer *editBuffer
// scratch is a byte buffer that is reused to efficiently read portions of text
@@ 504,6 506,7 @@ func (e *Editor) initBuffer() {
e.text.Alignment = e.Alignment
e.text.SingleLine = e.SingleLine
e.text.Mask = e.Mask
+ e.text.WrapPolicy = e.WrapPolicy
}
// Layout lays out the editor using the provided textMaterial as the paint material
M widget/editor_test.go => widget/editor_test.go +1 -0
@@ 409,6 409,7 @@ func TestEditorRTL(t *testing.T) {
func TestEditorLigature(t *testing.T) {
e := new(Editor)
+ e.WrapPolicy = text.WrapWords
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)),
M widget/label.go => widget/label.go +11 -8
@@ 28,6 28,8 @@ type Label struct {
// Truncator is the text that will be shown at the end of the final
// line if MaxLines is exceeded. Defaults to "…" if empty.
Truncator string
+ // WrapPolicy configures how displayed text will be broken into lines.
+ WrapPolicy text.WrapPolicy
}
// Layout the label with the given shaper, font, size, text, and material.
@@ 35,14 37,15 @@ func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size
cs := gtx.Constraints
textSize := fixed.I(gtx.Sp(size))
lt.LayoutString(text.Parameters{
- Font: font,
- PxPerEm: textSize,
- MaxLines: l.MaxLines,
- Truncator: l.Truncator,
- Alignment: l.Alignment,
- MaxWidth: cs.Max.X,
- MinWidth: cs.Min.X,
- Locale: gtx.Locale,
+ Font: font,
+ PxPerEm: textSize,
+ MaxLines: l.MaxLines,
+ Truncator: l.Truncator,
+ Alignment: l.Alignment,
+ WrapPolicy: l.WrapPolicy,
+ MaxWidth: cs.Max.X,
+ MinWidth: cs.Min.X,
+ Locale: gtx.Locale,
}, txt)
m := op.Record(gtx.Ops)
viewport := image.Rectangle{Max: cs.Max}
M widget/material/label.go => widget/material/label.go +7 -3
@@ 29,6 29,8 @@ type LabelStyle struct {
Alignment text.Alignment
// MaxLines limits the number of lines. Zero means no limit.
MaxLines int
+ // WrapPolicy configures how displayed text will be broken into lines.
+ WrapPolicy text.WrapPolicy
// Truncator is the text that will be shown at the end of the final
// line if MaxLines is exceeded. Defaults to "…" if empty.
Truncator string
@@ 127,12 129,14 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
l.State.Alignment = l.Alignment
l.State.MaxLines = l.MaxLines
l.State.Truncator = l.Truncator
+ l.State.WrapPolicy = l.WrapPolicy
return l.State.Layout(gtx, l.Shaper, l.Font, l.TextSize, textColor, selectColor)
}
tl := widget.Label{
- Alignment: l.Alignment,
- MaxLines: l.MaxLines,
- Truncator: l.Truncator,
+ Alignment: l.Alignment,
+ MaxLines: l.MaxLines,
+ Truncator: l.Truncator,
+ WrapPolicy: l.WrapPolicy,
}
return tl.Layout(gtx, l.Shaper, l.Font, l.TextSize, l.Text, textColor)
}
M widget/selectable.go => widget/selectable.go +4 -1
@@ 57,7 57,9 @@ type Selectable struct {
MaxLines int
// Truncator is the symbol to use at the end of the final line of text
// if text was cut off. Defaults to "…" if left empty.
- Truncator string
+ Truncator string
+ // WrapPolicy configures how displayed text will be broken into lines.
+ WrapPolicy text.WrapPolicy
initialized bool
source stringSource
// scratch is a buffer reused to efficiently read text out of the
@@ 182,6 184,7 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font,
l.text.Alignment = l.Alignment
l.text.MaxLines = l.MaxLines
l.text.Truncator = l.Truncator
+ l.text.WrapPolicy = l.WrapPolicy
l.text.Update(gtx, lt, font, size, l.handleEvents)
dims := l.text.Dimensions()
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
M widget/text.go => widget/text.go +6 -0
@@ 53,6 53,8 @@ type textView struct {
// Truncator is the text that will be shown at the end of the final
// line if MaxLines is exceeded. Defaults to "…" if empty.
Truncator string
+ // WrapPolicy configures how displayed text will be broken into lines.
+ WrapPolicy text.WrapPolicy
// Mask replaces the visual display of each rune in the contents with the given rune.
// Newline characters are not masked. When non-zero, the unmasked contents
// are accessed by Len, Text, and SetText.
@@ 267,6 269,10 @@ func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font font.Font, s
e.params.MaxLines = e.MaxLines
e.invalidate()
}
+ if e.WrapPolicy != e.params.WrapPolicy {
+ e.params.WrapPolicy = e.WrapPolicy
+ e.invalidate()
+ }
e.makeValid()
if eventHandling != nil {