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() }