diff --git a/shaping/wrapping.go b/shaping/wrapping.go index 9c2793a..330ea39 100644 --- a/shaping/wrapping.go +++ b/shaping/wrapping.go @@ -767,15 +767,17 @@ func (l *LineWrapper) WrapParagraph(config WrapConfig, maxWidth int, paragraph [ } l.Prepare(config, paragraph, runs) - var done bool + var ( + line WrappedLine + done bool + ) for !done { - var line Line - line, truncated, done = l.WrapNextLine(maxWidth) - if line != nil { - l.scratch.paragraphAppend(line) + line, done = l.WrapNextLine(maxWidth) + if line.Line != nil { + l.scratch.paragraphAppend(line.Line) } } - return l.scratch.finalParagraph(), truncated + return l.scratch.finalParagraph(), line.Truncated } // fillUntil tries to fill the line candidate slice with runs until it reaches a run containing the @@ -816,7 +818,20 @@ type lineConfig struct { truncatedMaxWidth int } -func (l *LineWrapper) postProcessLine(finalLine Line, done bool) (Line, int, bool) { +// WrappedLine is the result of wrapping one line of text. +type WrappedLine struct { + // Line is the content of the line, as a slice of shaped runs + Line Line + // Truncated is the count of runes truncated from the end of the line, + // if this line was truncated. + Truncated int + // NextLine is the indice (in the input text slice) of the begining + // of the next line. It will equal len(text) if all the text + // fit in one line. + NextLine int +} + +func (l *LineWrapper) postProcessLine(finalLine Line, done bool) (WrappedLine, bool) { if len(finalLine) > 0 { finalRun := finalLine[len(finalLine)-1] @@ -862,30 +877,30 @@ func (l *LineWrapper) postProcessLine(finalLine Line, done bool) (Line, int, boo l.more = false } - return finalLine, truncated, done + return WrappedLine{finalLine, truncated, l.lineStartRune}, done } // WrapNextLine wraps the shaped glyphs of a paragraph to a particular max width. // It is meant to be called iteratively to wrap each line, allowing lines to // be wrapped to different widths within the same paragraph. When done is true, // subsequent calls to WrapNextLine (without calling Prepare) will return a nil line. -// The truncated return value is the count of runes truncated from the end of the line, -// if this line was truncated. The returned line is only valid until the next call to +// +// The returned line is only valid until the next call to // [*LineWrapper.Prepare] or [*LineWrapper.WrapParagraph]. -func (l *LineWrapper) WrapNextLine(maxWidth int) (finalLine Line, truncated int, done bool) { +func (l *LineWrapper) WrapNextLine(maxWidth int) (out WrappedLine, done bool) { // If we've already finished the paragraph, don't do any more work. if !l.more { - return nil, 0, true + return WrappedLine{NextLine: l.lineStartRune}, true } defer func() { - finalLine, truncated, done = l.postProcessLine(finalLine, done) + out, done = l.postProcessLine(out.Line, done) }() // If the iterator is empty, return early. _, firstRun, hasFirst := l.glyphRuns.Peek() if !hasFirst { - return nil, 0, true + return WrappedLine{}, true } l.scratch.startLine() truncating := l.config.TruncateAfterLines == 1 @@ -905,7 +920,7 @@ func (l *LineWrapper) WrapNextLine(maxWidth int) (finalLine Line, truncated int, } l.scratch.candidateAppend(firstRun) l.scratch.markCandidateBest() - return l.scratch.finalizeBest(), 0, true + return WrappedLine{Line: l.scratch.finalizeBest()}, true } // Restore iterator state in preparation for real line wrapping algorithm. l.glyphRuns.Restore() @@ -917,8 +932,8 @@ func (l *LineWrapper) WrapNextLine(maxWidth int) (finalLine Line, truncated int, truncatedMaxWidth: maxWidth - l.config.Truncator.Advance.Ceil(), } done = l.wrapNextLine(config) - finalLine = l.scratch.finalizeBest() - return finalLine, 0, done + finalLine := l.scratch.finalizeBest() + return WrappedLine{Line: finalLine}, done } // checkpoint captures both the current candidate line and the corresponding run iteration diff --git a/shaping/wrapping_test.go b/shaping/wrapping_test.go index 50d0c2c..06b9213 100644 --- a/shaping/wrapping_test.go +++ b/shaping/wrapping_test.go @@ -734,8 +734,9 @@ func splitShapedAt(shaped Output, indices ...glyphIndex) []Output { func TestWrapLine(t *testing.T) { type expected struct { - line Line - done bool + line Line + nextLine int + done bool } type testcase struct { name string @@ -752,20 +753,24 @@ func TestWrapLine(t *testing.T) { maxWidth: 40, expected: []expected{ { - line: []Output{splitShapedAt(shapedText1, 5)[0]}, - done: false, + line: []Output{splitShapedAt(shapedText1, 5)[0]}, + nextLine: 5, + done: false, }, { - line: []Output{splitShapedAt(shapedText1, 5, 9)[1]}, - done: false, + line: []Output{splitShapedAt(shapedText1, 5, 9)[1]}, + nextLine: 9, + done: false, }, { - line: []Output{splitShapedAt(shapedText1, 9, 12)[1]}, - done: false, + line: []Output{splitShapedAt(shapedText1, 9, 12)[1]}, + nextLine: 12, + done: false, }, { - line: []Output{splitShapedAt(shapedText1, 12)[1]}, - done: true, + line: []Output{splitShapedAt(shapedText1, 12)[1]}, + nextLine: 15, + done: true, }, }, }, @@ -779,8 +784,9 @@ func TestWrapLine(t *testing.T) { maxWidth: 40, expected: []expected{ { - line: splitShapedAt(shapedText1, 1, 2, 3, 4, 5)[:5], - done: false, + line: splitShapedAt(shapedText1, 1, 2, 3, 4, 5)[:5], + nextLine: 5, + done: false, }, }, }, @@ -794,12 +800,14 @@ func TestWrapLine(t *testing.T) { maxWidth: 40, expected: []expected{ { - line: splitShapedAt(shapedText1, 3, 5)[:2], - done: false, + line: splitShapedAt(shapedText1, 3, 5)[:2], + nextLine: 5, + done: false, }, { - line: splitShapedAt(shapedText1, 5, 6, 9)[1:3], - done: false, + line: splitShapedAt(shapedText1, 5, 6, 9)[1:3], + nextLine: 9, + done: false, }, }, }, @@ -814,28 +822,32 @@ func TestWrapLine(t *testing.T) { withRange(splitShapedAt(shapedText3, 10)[1], Range{Count: 5}), }, - done: false, + nextLine: 5, + done: false, }, { line: []Output{ withRange(splitShapedAt(shapedText3, 7, 10)[1], Range{Offset: 5, Count: 5}), }, - done: false, + nextLine: 10, + done: false, }, { line: []Output{ withRange(splitShapedAt(shapedText3, 2, 7)[1], Range{Offset: 10, Count: 5}), }, - done: false, + nextLine: 15, + done: false, }, { line: []Output{ withRange(splitShapedAt(shapedText3, 2)[0], Range{Offset: 15, Count: 4}), }, - done: true, + nextLine: 19, + done: true, }, }, }, @@ -851,7 +863,8 @@ func TestWrapLine(t *testing.T) { withRange(splitShapedAt(shapedBidiText1[1], 5)[1], Range{Offset: 6, Count: 5}), }, - done: false, + nextLine: 11, + done: false, }, { line: []Output{ @@ -859,7 +872,8 @@ func TestWrapLine(t *testing.T) { Range{Offset: 11, Count: 5}), shapedBidiText1[2], }, - done: true, + nextLine: 20, + done: true, }, }, }, @@ -876,7 +890,8 @@ func TestWrapLine(t *testing.T) { line: []Output{ shapedOneWord, }, - done: true, + nextLine: 4, + done: true, }, }, }, @@ -887,15 +902,16 @@ func TestWrapLine(t *testing.T) { maxWidth: 200, expected: []expected{ { - line: Line{shapedText1}, - done: true, + line: Line{shapedText1}, + nextLine: 15, + done: true, }, }, }, } { t.Run(tc.name, func(t *testing.T) { var ( - line Line + line WrappedLine done bool l LineWrapper ) @@ -904,16 +920,18 @@ func TestWrapLine(t *testing.T) { // allows test cases to be exhaustive if they need to wihtout forcing // every case to wrap entire paragraphs. for lineNumber, expected := range tc.expected { - line, _, done = l.WrapNextLine(tc.maxWidth) - compareLines(t, lineNumber, expected.line, line) + line, done = l.WrapNextLine(tc.maxWidth) + compareLines(t, lineNumber, expected.line, line.Line) if done != expected.done { t.Errorf("done mismatch! expected %v, got %v", expected.done, done) } + tu.AssertC(t, line.NextLine == expected.nextLine, fmt.Sprintf("expected %d, got %d", expected.nextLine, line.NextLine)) + if expected.done { // check WrapNextLine is now a no-op - line, _, done = l.WrapNextLine(200) - if line != nil || !done { - t.Errorf("expect nil output for WrapNextLine, got %p and %v", line, done) + line, done = l.WrapNextLine(200) + if line.Line != nil || !done { + t.Errorf("expect nil output for WrapNextLine, got %p and %v", line.Line, done) } } }