From 83202263b99ab174ed304727fe956ca6d998295c Mon Sep 17 00:00:00 2001 From: Chris Waldon Date: Tue, 22 Aug 2023 16:23:01 -0400 Subject: [PATCH] text: simplify truncation accounting This commit reverts the work of several previous attempts to resolve truncation-related rune accounting problems and adopts a simpler approach. Instead of taking a special codepath when shaping only a newline, we shape the empty string to get its line metrics. Instead of modifying the final glyph conditionally to account for runes we never actually shaped, we track that count on the document type and handle it withing the NextGlyph method. These changes result in much simpler code, and resolve a real bug. We were accidentally corrupting cached paragraphs when doing the truncation post-processing in Shaper.layoutText. The modification made to the final glyph there actually did modify the cached copy, which would then be reused when that string was shaped again (even if there were a different number of truncated runes after it). This changeset ensures that the cached copy of a paragraph is never modified. Signed-off-by: Chris Waldon --- text/gotext.go | 27 +++++---------------------- text/shaper.go | 9 ++++----- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/text/gotext.go b/text/gotext.go index 2d37a96d..3b37c6d1 100644 --- a/text/gotext.go +++ b/text/gotext.go @@ -36,7 +36,8 @@ type document struct { lines []line alignment Alignment // alignWidth is the width used when aligning text. - alignWidth int + alignWidth int + unreadRuneCount int } // append adds the lines of other to the end of l and ensures they @@ -52,6 +53,7 @@ func (l *document) reset() { l.lines = l.lines[:0] l.alignment = Start l.alignWidth = 0 + l.unreadRuneCount = 0 } func max(a, b int) int { @@ -528,28 +530,9 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document { if hasNewline { txt = txt[:len(txt)-1] } - truncatedNewline := false - if hasNewline && len(txt) == 0 { - params.forceTruncate = false - // If we only have a newline, shape a space to get line metrics. - ls, truncated = s.shapeAndWrapText(params, []rune{' '}) - if truncated > 0 { - // Our space was truncated. Since our space didn't exist in any meaningful - // capacity, ensure the truncated count is zeroed out. - truncated = 0 - truncatedNewline = true - } else { - // We shaped a space to get proper line metrics, but we need to drop - // the rune/glyph info since it isn't actually part of the text. - ls[0][0].Glyphs = ls[0][0].Glyphs[:0] - ls[0][0].Advance = 0 - ls[0][0].Runes.Count = 0 - } - } else { - ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt)) - } + ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt)) - didTruncate := truncated > 0 || truncatedNewline || (params.forceTruncate && params.MaxLines == len(ls)) + didTruncate := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls)) if didTruncate && hasNewline { // We've truncated the newline, since it was at the end and we've truncated some amount of runes diff --git a/text/shaper.go b/text/shaper.go index 76a90f39..96f0a36f 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -354,11 +354,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) { unreadRunes++ } } - lastLineIdx := len(lines.lines) - 1 - lastRunIdx := len(lines.lines[lastLineIdx].runs) - 1 - lastGlyphIdx := len(lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs) - 1 - lines.lines[lastLineIdx].runs[lastRunIdx].Runes.Count += unreadRunes - lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs[lastGlyphIdx].runeCount += unreadRunes + l.txt.unreadRuneCount = unreadRunes } } l.txt.append(lines) @@ -508,6 +504,9 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) { } if endOfCluster { glyph.Flags |= FlagClusterBreak + if run.truncator { + glyph.Runes += l.txt.unreadRuneCount + } } else { glyph.Runes = 0 }