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
+3
View File
@@ -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
+1
View File
@@ -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)),
+11 -8
View File
@@ -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}
+7 -3
View File
@@ -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)
}
+4 -1
View File
@@ -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()
+6
View File
@@ -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 {