From 7e8c10927bcc2c93242322c19285f8df4f9396a3 Mon Sep 17 00:00:00 2001 From: Chris Waldon Date: Mon, 27 Mar 2023 16:23:44 -0400 Subject: [PATCH] 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 --- text/gotext.go | 22 +++++++-------- text/gotext_test.go | 48 ++++++++++++++++++++++++------- text/shaper.go | 33 ++++++++++++---------- text/shaper_test.go | 52 ++++++++++++++++++++++++---------- widget/index_test.go | 39 ++++++++++++++++++++------ widget/label.go | 5 +++- widget/material/label.go | 9 +++++- widget/selectable.go | 12 +++++++- widget/selectable_test.go | 2 +- widget/text.go | 59 ++++++++++++++++++++------------------- 10 files changed, 191 insertions(+), 90 deletions(-) diff --git a/text/gotext.go b/text/gotext.go index 369e0467..14f21ab4 100644 --- a/text/gotext.go +++ b/text/gotext.go @@ -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), } } diff --git a/text/gotext_test.go b/text/gotext_test.go index 47815a31..98fd8d80 100644 --- a/text/gotext_test.go +++ b/text/gotext_test.go @@ -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 diff --git a/text/shaper.go b/text/shaper.go index 1f16d3cd..38ba8ef8 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -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 } diff --git a/text/shaper_test.go b/text/shaper_test.go index b086b360..a6984662 100644 --- a/text/shaper_test.go +++ b/text/shaper_test.go @@ -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() { diff --git a/widget/index_test.go b/widget/index_test.go index 5b8d2f9e..f3d5af88 100644 --- a/widget/index_test.go +++ b/widget/index_test.go @@ -32,17 +32,30 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string // bidiSource is crafted to contain multiple consecutive RTL runs (by // changing scripts within the RTL). bidiSource := "The quick سماء שלום لا fox تمط שלום غير the lazy dog." - ltrParams := text.Parameters{Font: text.Font{Typeface: "LTR"}, PxPerEm: fixed.I(fontSize)} - rtlParams := text.Parameters{Alignment: text.End, Font: text.Font{Typeface: "RTL"}, PxPerEm: fixed.I(fontSize)} + ltrParams := text.Parameters{ + Font: text.Font{Typeface: "LTR"}, + PxPerEm: fixed.I(fontSize), + MaxWidth: lineWidth, + MinWidth: lineWidth, + Locale: english, + } + rtlParams := text.Parameters{ + Alignment: text.End, + Font: text.Font{Typeface: "RTL"}, + PxPerEm: fixed.I(fontSize), + MaxWidth: lineWidth, + MinWidth: lineWidth, + Locale: arabic, + } if alignOpposite { ltrParams.Alignment = text.End rtlParams.Alignment = text.Start } - shaper.LayoutString(ltrParams, lineWidth, lineWidth, english, bidiSource) + shaper.LayoutString(ltrParams, bidiSource) for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { bidiLTR = append(bidiLTR, g) } - shaper.LayoutString(rtlParams, lineWidth, lineWidth, arabic, bidiSource) + shaper.LayoutString(rtlParams, bidiSource) for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { bidiRTL = append(bidiRTL, g) } @@ -64,8 +77,12 @@ func makeAccountingTestText(str string, fontSize, lineWidth int) (txt []text.Gly Face: rtlFace, }, }) - params := text.Parameters{PxPerEm: fixed.I(fontSize)} - shaper.LayoutString(params, 0, lineWidth, english, str) + params := text.Parameters{ + PxPerEm: fixed.I(fontSize), + MaxWidth: lineWidth, + Locale: english, + } + shaper.LayoutString(params, str) for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { txt = append(txt, g) } @@ -86,8 +103,14 @@ func getGlyphs(fontSize, minWidth, lineWidth int, align text.Alignment, str stri Face: rtlFace, }, }) - params := text.Parameters{PxPerEm: fixed.I(fontSize), Alignment: align} - shaper.LayoutString(params, minWidth, lineWidth, english, str) + params := text.Parameters{ + PxPerEm: fixed.I(fontSize), + Alignment: align, + MinWidth: minWidth, + MaxWidth: lineWidth, + Locale: english, + } + shaper.LayoutString(params, str) for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { txt = append(txt, g) } diff --git a/widget/label.go b/widget/label.go index 90a6d7d4..93ac4739 100644 --- a/widget/label.go +++ b/widget/label.go @@ -38,7 +38,10 @@ func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size MaxLines: l.MaxLines, Truncator: l.Truncator, Alignment: l.Alignment, - }, cs.Min.X, cs.Max.X, gtx.Locale, txt) + MaxWidth: cs.Max.X, + MinWidth: cs.Min.X, + Locale: gtx.Locale, + }, txt) m := op.Record(gtx.Ops) viewport := image.Rectangle{Max: cs.Max} it := textIterator{ diff --git a/widget/material/label.go b/widget/material/label.go index cd835bc9..554e87c2 100644 --- a/widget/material/label.go +++ b/widget/material/label.go @@ -116,8 +116,15 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { if l.State.Text() != l.Text { l.State.SetText(l.Text) } + l.State.Alignment = l.Alignment + l.State.MaxLines = l.MaxLines + l.State.Truncator = l.Truncator return l.State.Layout(gtx, l.shaper, l.Font, l.TextSize, textColor, selectColor) } - tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines} + tl := widget.Label{ + Alignment: l.Alignment, + MaxLines: l.MaxLines, + Truncator: l.Truncator, + } return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text, textColor) } diff --git a/widget/selectable.go b/widget/selectable.go index ff4da67d..438cc097 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -48,8 +48,15 @@ func (s stringSource) ReadAt(b []byte, offset int64) (int, error) { func (s stringSource) ReplaceRunes(byteOffset, runeCount int64, str string) { } -// Selectable holds text selection state. +// Selectable displays selectable text. type Selectable struct { + // Alignment controls the alignment of the text. + Alignment text.Alignment + // MaxLines is the maximum number of lines of text to be displayed. + 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 initialized bool source stringSource // scratch is a buffer reused to efficiently read text out of the @@ -171,6 +178,9 @@ func (l *Selectable) Truncated() bool { // paint material for the text and selection rectangles, respectively. func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, textMaterial, selectionMaterial op.CallOp) layout.Dimensions { l.initialize() + l.text.Alignment = l.Alignment + l.text.MaxLines = l.MaxLines + l.text.Truncator = l.Truncator 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() diff --git a/widget/selectable_test.go b/widget/selectable_test.go index c514bfe9..0307ff33 100644 --- a/widget/selectable_test.go +++ b/widget/selectable_test.go @@ -100,7 +100,7 @@ func TestSelectableConfigurations(t *testing.T) { gtx.Constraints.Min = gtx.Constraints.Max } s := new(Selectable) - s.text.Alignment = alignment + s.Alignment = alignment s.SetText(sentence) interactiveDims := s.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) staticDims := Label{Alignment: alignment}.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{}) diff --git a/widget/text.go b/widget/text.go index 2b4d33a2..e6054203 100644 --- a/widget/text.go +++ b/widget/text.go @@ -10,7 +10,6 @@ import ( "unicode/utf8" "gioui.org/f32" - "gioui.org/io/system" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -58,22 +57,20 @@ type textView struct { // are accessed by Len, Text, and SetText. Mask rune - font text.Font + params text.Parameters shaper *text.Shaper - textSize fixed.Int26_6 seekCursor int64 rr textSource maskReader maskReader // graphemes tracks the indices of grapheme cluster boundaries within rr. graphemes []int // paragraphReader is used to populate graphemes. - paragraphReader graphemeReader - lastMask rune - maxWidth, minWidth int - viewSize image.Point - valid bool - regions []Region - dims layout.Dimensions + paragraphReader graphemeReader + lastMask rune + viewSize image.Point + valid bool + regions []Region + dims layout.Dimensions // offIndex is an index of rune index to byte offsets. offIndex []offEntry @@ -93,8 +90,6 @@ type textView struct { } scrollOff image.Point - - locale system.Locale } func (e *textView) Changed() bool { @@ -228,27 +223,27 @@ func (e *textView) calculateViewSize(gtx layout.Context) image.Point { // allow parent widgets to adapt to any changes in text content or positioning. If eventHandling modifies the contents // of the textView, it is guaranteed to be reshaped (and ready for painting) before Update returns. func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, eventHandling func(gtx layout.Context)) { - if e.locale != gtx.Locale { - e.locale = gtx.Locale + if e.params.Locale != gtx.Locale { + e.params.Locale = gtx.Locale e.invalidate() } textSize := fixed.I(gtx.Sp(size)) - if e.font != font || e.textSize != textSize { + if e.params.Font != font || e.params.PxPerEm != textSize { e.invalidate() - e.font = font - e.textSize = textSize + e.params.Font = font + e.params.PxPerEm = textSize } maxWidth := gtx.Constraints.Max.X if e.SingleLine { maxWidth = math.MaxInt } minWidth := gtx.Constraints.Min.X - if maxWidth != e.maxWidth { - e.maxWidth = maxWidth + if maxWidth != e.params.MaxWidth { + e.params.MaxWidth = maxWidth e.invalidate() } - if minWidth != e.minWidth { - e.minWidth = minWidth + if minWidth != e.params.MinWidth { + e.params.MinWidth = minWidth e.invalidate() } if lt != e.shaper { @@ -259,6 +254,18 @@ func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font text.Font, s e.lastMask = e.Mask e.invalidate() } + if e.Alignment != e.params.Alignment { + e.params.Alignment = e.Alignment + e.invalidate() + } + if e.Truncator != e.params.Truncator { + e.params.Truncator = e.Truncator + e.invalidate() + } + if e.MaxLines != e.params.MaxLines { + e.params.MaxLines = e.MaxLines + e.invalidate() + } e.makeValid() if eventHandling != nil { @@ -463,13 +470,7 @@ func (e *textView) layoutText(lt *text.Shaper) { e.index.reset() it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}} if lt != nil { - lt.Layout(text.Parameters{ - Font: e.font, - PxPerEm: e.textSize, - Alignment: e.Alignment, - MaxLines: e.MaxLines, - Truncator: e.Truncator, - }, e.minWidth, e.maxWidth, e.locale, r) + lt.Layout(e.params, r) for glyph, ok := it.processGlyph(lt.NextGlyph()); ok; glyph, ok = it.processGlyph(lt.NextGlyph()) { e.index.Glyph(glyph) } @@ -635,7 +636,7 @@ func (e *textView) MoveEnd(selAct selectionAction) { caret := e.closestToRune(e.caret.start) caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt) e.caret.start = caret.runes - e.caret.xoff = fixed.I(e.maxWidth) - caret.x + e.caret.xoff = fixed.I(e.params.MaxWidth) - caret.x e.updateSelection(selAct) e.clampCursorToGraphemes() }