diff --git a/shaping/wrapping.go b/shaping/wrapping.go index 1a6e8b4..cbff7a6 100644 --- a/shaping/wrapping.go +++ b/shaping/wrapping.go @@ -437,6 +437,11 @@ type WrapConfig struct { // breaking in between UAX#14 line breaking candidates, or "within words" in // many scripts. BreakPolicy LineBreakPolicy + // DisableTrailingWhitespaceTrim turns off a feature that automatically sets the + // advance of trailing whitespace on a line to zero. In display contexts, you + // usually want this feature enabled, but for text editors it is frequently + // desirable to allow trailing whitespace to occupy space itself. + DisableTrailingWhitespaceTrim bool } // LineBreakPolicy specifies when considering a line break within a "word" or UAX#14 @@ -843,77 +848,83 @@ type WrappedLine struct { NextLine int } -// resolveBidi inverts the visual index of runs in line[start:end] and tracks the lowest and highest -// known visual positions. -func resolveBidi(line Line, start, end, maxVisual, minVisual int) (newMaxVisual, newMinVisual int) { - // Resove bidi from line[bidiStart:idx] - for bidiIdx := range line[start:end] { +// resolveBidi inverts the visual index of runs in line[start:end]. +func resolveBidi(line Line, start, end int) { + // Resove bidi from line[start:end] by swapping pairs of visual indices across the midpoint + // of the range. + elementsCount := end - start + for bidiIdx := range line[start : start+elementsCount/2] { absIndex := start + bidiIdx - visualIndex := len(line) - 1 - absIndex - line[absIndex].VisualIndex = int32(visualIndex) - if visualIndex > maxVisual { - maxVisual = absIndex + absIndex2 := start + (elementsCount - bidiIdx) - 1 + line[absIndex].VisualIndex, line[absIndex2].VisualIndex = line[absIndex2].VisualIndex, line[absIndex].VisualIndex + } +} + +func computeBidiOrdering(dir di.Direction, finalLine Line) { + // Here we populate the VisualIndex of each run. + bidiStart := -1 + for idx, run := range finalLine { + basePosition := idx + if dir.Progression() == di.TowardTopLeft { + basePosition = len(finalLine) - 1 - idx } - if visualIndex < minVisual { - minVisual = absIndex + finalLine[idx].VisualIndex = int32(basePosition) + if run.Direction == dir { + if bidiStart != -1 { + resolveBidi(finalLine, bidiStart, idx) + bidiStart = -1 + } + } else if bidiStart == -1 { + bidiStart = idx } } - return maxVisual, minVisual + if bidiStart != -1 { + resolveBidi(finalLine, bidiStart, len(finalLine)) + } } func (l *LineWrapper) postProcessLine(finalLine Line, done bool) (WrappedLine, bool) { if len(finalLine) > 0 { - // Here we populate the VisualIndex of each run and track the index of the first - // and last visual runs for the purpose of trimming off trailing whitespace. - bidiStart := -1 - maxVisualRunIdx := -1 - minVisualRunIdx := len(finalLine) - for idx, run := range finalLine { - if run.Direction == l.config.Direction { - if bidiStart != -1 { - maxVisualRunIdx, minVisualRunIdx = resolveBidi(finalLine, bidiStart, idx, maxVisualRunIdx, minVisualRunIdx) - bidiStart = -1 - } - finalLine[idx].VisualIndex = int32(idx) - if idx > maxVisualRunIdx { - maxVisualRunIdx = idx - } - if idx < minVisualRunIdx { - minVisualRunIdx = idx + computeBidiOrdering(l.config.Direction, finalLine) + if !l.config.DisableTrailingWhitespaceTrim { + // Here we find the last visual run in the line. + goalIdx := len(finalLine) - 1 + if l.config.Direction.Progression() == di.TowardTopLeft { + goalIdx = 0 + } + for logicalIdx, run := range finalLine { + if run.VisualIndex == int32(goalIdx) { + goalIdx = logicalIdx + break } - } else if bidiStart == -1 { - bidiStart = idx } - } - if bidiStart != -1 { - maxVisualRunIdx, minVisualRunIdx = resolveBidi(finalLine, bidiStart, len(finalLine), maxVisualRunIdx, minVisualRunIdx) - } - // This next block locates the first/last visual glyph on the line and - // zeroes its advance if it is whitespace. - var finalVisualRun *Output - var finalVisualGlyph *Glyph - if l.config.Direction.Progression() == di.FromTopLeft { - finalVisualRun = &finalLine[maxVisualRunIdx] - } else { - finalVisualRun = &finalLine[minVisualRunIdx] - } - if L := len(finalVisualRun.Glyphs); L > 0 { + // This next block locates the first/last visual glyph on the line and + // zeroes its advance if it is whitespace. + var finalVisualRun *Output + var finalVisualGlyph *Glyph if l.config.Direction.Progression() == di.FromTopLeft { - finalVisualGlyph = &finalVisualRun.Glyphs[L-1] + finalVisualRun = &finalLine[goalIdx] } else { - finalVisualGlyph = &finalVisualRun.Glyphs[0] + finalVisualRun = &finalLine[goalIdx] } - - if finalVisualRun.Direction.IsVertical() { - if finalVisualGlyph.Height == 0 { - finalVisualGlyph.YAdvance = 0 + if L := len(finalVisualRun.Glyphs); L > 0 { + if l.config.Direction.Progression() == di.FromTopLeft { + finalVisualGlyph = &finalVisualRun.Glyphs[L-1] + } else { + finalVisualGlyph = &finalVisualRun.Glyphs[0] } - } else { // horizontal - if finalVisualGlyph.Width == 0 { - finalVisualGlyph.XAdvance = 0 + + if finalVisualRun.Direction.IsVertical() { + if finalVisualGlyph.Height == 0 { + finalVisualGlyph.YAdvance = 0 + } + } else { // horizontal + if finalVisualGlyph.Width == 0 { + finalVisualGlyph.XAdvance = 0 + } } + finalVisualRun.RecomputeAdvance() } - finalVisualRun.RecomputeAdvance() } finalLogicalRun := finalLine[len(finalLine)-1] diff --git a/shaping/wrapping_test.go b/shaping/wrapping_test.go index 507b57d..7a4ad31 100644 --- a/shaping/wrapping_test.go +++ b/shaping/wrapping_test.go @@ -2239,7 +2239,10 @@ func TestGraphemeBreakingRegression(t *testing.T) { for i, run := range shaped { runs[i] = run.copy() } - lines, truncated := wrapper.WrapParagraph(WrapConfig{BreakPolicy: Always}, maxWidth, bidiText2, NewSliceIterator(runs)) + lines, truncated := wrapper.WrapParagraph(WrapConfig{ + BreakPolicy: Always, + DisableTrailingWhitespaceTrim: true, + }, maxWidth, bidiText2, NewSliceIterator(runs)) checkRuneCounts(t, bidiText2, lines, truncated) for i, baseLine := range lines[:len(lines)-1] { @@ -3385,3 +3388,103 @@ func TestLineWrapPostProcess(t *testing.T) { } }) } + +func TestComputeBidiOrdering(t *testing.T) { + type testcase struct { + name string + input []Output + direction di.Direction + expectedVisualOrder []int + } + for _, tc := range []testcase{ + { + name: "ltr", + direction: di.DirectionLTR, + input: []Output{ + {Direction: di.DirectionLTR}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionLTR}, + }, + expectedVisualOrder: []int{0, 1, 2}, + }, + { + name: "rtl", + direction: di.DirectionRTL, + input: []Output{ + {Direction: di.DirectionRTL}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionRTL}, + }, + expectedVisualOrder: []int{2, 1, 0}, + }, + { + name: "bidi-ltr", + direction: di.DirectionLTR, + input: []Output{ + {Direction: di.DirectionLTR}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionLTR}, + }, + expectedVisualOrder: []int{0, 3, 2, 1, 4}, + }, + { + name: "bidi-ltr-complex", + direction: di.DirectionLTR, + input: []Output{ + {Direction: di.DirectionRTL}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionRTL}, + }, + expectedVisualOrder: []int{1, 0, 2, 4, 3, 5, 7, 6, 8, 10, 9}, + }, + { + name: "bidi-rtl", + direction: di.DirectionRTL, + input: []Output{ + {Direction: di.DirectionRTL}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionRTL}, + }, + expectedVisualOrder: []int{4, 1, 2, 3, 0}, + }, + { + name: "bidi-rtl-complex", + direction: di.DirectionRTL, + input: []Output{ + {Direction: di.DirectionLTR}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionRTL}, + {Direction: di.DirectionLTR}, + {Direction: di.DirectionLTR}, + }, + expectedVisualOrder: []int{9, 10, 8, 6, 7, 5, 3, 4, 2, 0, 1}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + computeBidiOrdering(tc.direction, tc.input) + for visualIndex, logicalIndex := range tc.expectedVisualOrder { + if tc.input[logicalIndex].VisualIndex != int32(visualIndex) { + t.Errorf("line[%d]: expected visual index %v, got %v", logicalIndex, visualIndex, tc.input[logicalIndex].VisualIndex) + } + } + }) + } +}