From 2db1a7bfb94adb72e4c911352ec3adaeeaef196b Mon Sep 17 00:00:00 2001 From: Chris Waldon Date: Wed, 14 Dec 2022 11:11:25 -0500 Subject: [PATCH] go.*,text: implement shaper-driven line truncation This commit pushes limiting the maximum number of lines of text into the shaper implementation. This is more efficient than doing it in widgets, and also opens the door for future use of the shaper to insert ellipsis and other truncating characters as appropriate. I realized that we lost the implementation of limiting the number of lines of text in my text stack overhaul, so this fixes a regression from that work. Signed-off-by: Chris Waldon --- go.mod | 4 ++-- go.sum | 8 ++++---- text/gotext.go | 8 +++++--- text/gotext_test.go | 4 ++-- text/shaper.go | 8 ++++++++ text/shaper_test.go | 30 ++++++++++++++++++++++++++++++ 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 12085117..b8b1244e 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 gioui.org/shader v1.0.6 - github.com/benoitkugler/textlayout v0.2.0 - github.com/go-text/typesetting v0.0.0-20221111143014-22bec069817d + github.com/benoitkugler/textlayout v0.3.0 + github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 diff --git a/go.sum b/go.sum index 778f92ad..4bcf33f7 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,12 @@ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3 gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y= gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE= -github.com/benoitkugler/textlayout v0.2.0 h1:I+s1LuIKckIJgDbsGF1iUNEZ24HZ8mMfb2LsMJU+Uko= -github.com/benoitkugler/textlayout v0.2.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w= +github.com/benoitkugler/textlayout v0.3.0 h1:2ehWXEkgb6RUokTjXh1LzdGwG4dRP6X3dqhYYDYhUVk= +github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w= github.com/benoitkugler/textlayout-testdata v0.1.1 h1:AvFxBxpfrQd8v55qH59mZOJOQjtD6K2SFe9/HvnIbJk= github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo= -github.com/go-text/typesetting v0.0.0-20221111143014-22bec069817d h1:aAOmUgKRf9Rg2OsWezNWgsEjEtCIcFReQgXH81LrldI= -github.com/go-text/typesetting v0.0.0-20221111143014-22bec069817d/go.mod h1:RO32JTaCKQV+XI9psTihlQhZsQJp0R4BIkJvHQGsuVo= +github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 h1:iOA0HmtpANn48hX2nlDNMu0VVaNza35HJG0WeetBVzQ= +github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5/go.mod h1:/cmOXaoTiO+lbCwkTZBgCvevJpbFsZ5reXIpEJVh5MI= golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg= golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0= diff --git a/text/gotext.go b/text/gotext.go index 80167a1e..d433fa52 100644 --- a/text/gotext.go +++ b/text/gotext.go @@ -384,9 +384,11 @@ 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, ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt []rune) []shaping.Line { +func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, maxWidth int, lc system.Locale, txt []rune) []shaping.Line { // Wrap outputs into lines. - return s.wrapper.WrapParagraph(maxWidth, txt, s.shapeText(faces, ppem, lc, txt)...) + return s.wrapper.WrapParagraph(shaping.WrapConfig{ + TruncateAfterLines: params.MaxLines, + }, maxWidth, txt, s.shapeText(faces, params.PxPerEm, lc, txt)...) } // replaceControlCharacters replaces problematic unicode @@ -445,7 +447,7 @@ func (s *shaperImpl) LayoutRunes(params Parameters, minWidth, maxWidth int, lc s if hasNewline { txt = txt[:len(txt)-1] } - ls := s.shapeAndWrapText(s.orderer.sortedFacesForStyle(params.Font), params.PxPerEm, maxWidth, lc, replaceControlCharacters(txt)) + ls := s.shapeAndWrapText(s.orderer.sortedFacesForStyle(params.Font), params, maxWidth, lc, replaceControlCharacters(txt)) // Convert to Lines. textLines := make([]line, len(ls)) for i := range ls { diff --git a/text/gotext_test.go b/text/gotext_test.go index 969743e7..a66be7e5 100644 --- a/text/gotext_test.go +++ b/text/gotext_test.go @@ -193,8 +193,8 @@ func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize, rtlSource = string(complexRunes[:runeLimit]) } } - simpleText := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(Font{}), fixed.I(fontSize), lineWidth, locale, []rune(simpleSource)) - complexText := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(Font{}), fixed.I(fontSize), lineWidth, locale, []rune(complexSource)) + 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)) shaper = testShaper(rtlFace, ltrFace) return simpleText, complexText } diff --git a/text/shaper.go b/text/shaper.go index c13d3aa0..cbf1bce8 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -192,6 +192,8 @@ func (l *Shaper) layoutText(params Parameters, minWidth, maxWidth int, lc system l.txt.append(l.layoutParagraph(params, minWidth, maxWidth, lc, "", nil)) return } + truncating := params.MaxLines > 0 + maxLines := params.MaxLines var done bool var startByte int var endByte int @@ -222,6 +224,12 @@ func (l *Shaper) layoutText(params Parameters, minWidth, maxWidth int, lc system done = endByte == len(str) } l.txt.append(l.layoutParagraph(params, minWidth, maxWidth, lc, str[startByte:endByte], l.paragraph)) + if truncating { + params.MaxLines = maxLines - len(l.txt.lines) + if params.MaxLines == 0 { + done = true + } + } if done { return } diff --git a/text/shaper_test.go b/text/shaper_test.go index 173943ef..4ff47fa8 100644 --- a/text/shaper_test.go +++ b/text/shaper_test.go @@ -10,6 +10,36 @@ import ( "golang.org/x/image/math/fixed" ) +// TestWrappingTruncation checks that the line wrapper's truncation features +// behave as expected. +func TestWrappingTruncation(t *testing.T) { + // Use a test string containing multiple newlines to ensure that they are shaped + // as separate paragraphs. + textInput := "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua." + ltrFace, _ := opentype.Parse(goregular.TTF) + collection := []FontFace{{Face: ltrFace}} + cache := NewShaper(collection) + cache.LayoutString(Parameters{ + Alignment: Middle, + PxPerEm: fixed.I(10), + }, 200, 200, english, textInput) + untruncatedCount := len(cache.txt.lines) + + for i := untruncatedCount + 1; i > 0; i-- { + cache.LayoutString(Parameters{ + Alignment: Middle, + PxPerEm: fixed.I(10), + MaxLines: i, + }, 200, 200, english, textInput) + lineCount := len(cache.txt.lines) + if i <= untruncatedCount && lineCount != i { + t.Errorf("expected %d lines, got %d", i, lineCount) + } else if i > untruncatedCount && lineCount != untruncatedCount { + t.Errorf("expected %d lines, got %d", untruncatedCount, lineCount) + } + } +} + // TestCacheEmptyString ensures that shaping the empty string returns a // single synthetic glyph with ascent/descent info. func TestCacheEmptyString(t *testing.T) {