Skip to content

Commit

Permalink
[shaping/wrapper] return the index of the next line in WrapLine
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitkugler committed Nov 27, 2023
1 parent 3b7c920 commit 8c369d1
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 48 deletions.
49 changes: 32 additions & 17 deletions shaping/wrapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand Down
80 changes: 49 additions & 31 deletions shaping/wrapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
},
},
},
Expand All @@ -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,
},
},
},
Expand All @@ -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,
},
},
},
Expand All @@ -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,
},
},
},
Expand All @@ -851,15 +863,17 @@ func TestWrapLine(t *testing.T) {
withRange(splitShapedAt(shapedBidiText1[1], 5)[1],
Range{Offset: 6, Count: 5}),
},
done: false,
nextLine: 11,
done: false,
},
{
line: []Output{
withRange(splitShapedAt(shapedBidiText1[1], 5)[0],
Range{Offset: 11, Count: 5}),
shapedBidiText1[2],
},
done: true,
nextLine: 20,
done: true,
},
},
},
Expand All @@ -876,7 +890,8 @@ func TestWrapLine(t *testing.T) {
line: []Output{
shapedOneWord,
},
done: true,
nextLine: 4,
done: true,
},
},
},
Expand All @@ -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
)
Expand All @@ -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)
}
}
}
Expand Down

0 comments on commit 8c369d1

Please sign in to comment.