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) {