|
|
@@ -179,15 +179,33 @@ func wrapStyledLine(line tview.Line, width int) []tview.Line {
|
|
|
current := make(tview.Line, 0, len(line))
|
|
|
currentWidth := 0
|
|
|
|
|
|
- pushSegment := func(text string, style tcell.Style) {
|
|
|
+ // Word-wrap state: track the current word so we can break before it.
|
|
|
+ wordLine := make(tview.Line, 0, 4) // segments in the current word
|
|
|
+ wordWidth := 0
|
|
|
+ lineBeforeWord := make(tview.Line, 0, len(line)) // line content before the current word
|
|
|
+ lineBeforeWordWidth := 0
|
|
|
+
|
|
|
+ pushSegment := func(dst *tview.Line, text string, style tcell.Style) {
|
|
|
if text == "" {
|
|
|
return
|
|
|
}
|
|
|
- if n := len(current); n > 0 && current[n-1].Style == style {
|
|
|
- current[n-1].Text += text
|
|
|
+ if n := len(*dst); n > 0 && (*dst)[n-1].Style == style {
|
|
|
+ (*dst)[n-1].Text += text
|
|
|
return
|
|
|
}
|
|
|
- current = append(current, tview.Segment{Text: text, Style: style})
|
|
|
+ *dst = append(*dst, tview.Segment{Text: text, Style: style})
|
|
|
+ }
|
|
|
+
|
|
|
+ commitWord := func() {
|
|
|
+ for _, seg := range wordLine {
|
|
|
+ pushSegment(¤t, seg.Text, seg.Style)
|
|
|
+ }
|
|
|
+ currentWidth += wordWidth
|
|
|
+ // Save snapshot as potential break point.
|
|
|
+ lineBeforeWord = append(lineBeforeWord[:0], current...)
|
|
|
+ lineBeforeWordWidth = currentWidth
|
|
|
+ wordLine = wordLine[:0]
|
|
|
+ wordWidth = 0
|
|
|
}
|
|
|
|
|
|
flush := func() {
|
|
|
@@ -196,6 +214,8 @@ func wrapStyledLine(line tview.Line, width int) []tview.Line {
|
|
|
lines = append(lines, lineCopy)
|
|
|
current = current[:0]
|
|
|
currentWidth = 0
|
|
|
+ lineBeforeWord = lineBeforeWord[:0]
|
|
|
+ lineBeforeWordWidth = 0
|
|
|
}
|
|
|
|
|
|
for _, segment := range line {
|
|
|
@@ -209,20 +229,43 @@ func wrapStyledLine(line tview.Line, width int) []tview.Line {
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
- // Use grapheme width (not rune count) so wrapping stays correct with wide glyphs, emoji, and combining characters.
|
|
|
clusterWidth := graphemeClusterWidth(boundaries)
|
|
|
- if currentWidth > 0 && currentWidth+clusterWidth > width {
|
|
|
- flush()
|
|
|
+ isSpace := cluster == " " || cluster == "\t"
|
|
|
+
|
|
|
+ if isSpace {
|
|
|
+ // Space: commit pending word, then add the space to the line.
|
|
|
+ commitWord()
|
|
|
+ if currentWidth+clusterWidth > width {
|
|
|
+ flush()
|
|
|
+ } else {
|
|
|
+ pushSegment(¤t, cluster, segment.Style)
|
|
|
+ currentWidth += clusterWidth
|
|
|
+ lineBeforeWord = append(lineBeforeWord[:0], current...)
|
|
|
+ lineBeforeWordWidth = currentWidth
|
|
|
+ }
|
|
|
+ continue
|
|
|
}
|
|
|
- pushSegment(cluster, segment.Style)
|
|
|
- currentWidth += clusterWidth
|
|
|
|
|
|
- if currentWidth >= width {
|
|
|
- flush()
|
|
|
+ // Non-space: add to current word.
|
|
|
+ if currentWidth+wordWidth+clusterWidth > width {
|
|
|
+ if wordWidth > 0 && lineBeforeWordWidth > 0 {
|
|
|
+ // Break before the current word: rewind to lineBeforeWord.
|
|
|
+ current = append(current[:0], lineBeforeWord...)
|
|
|
+ currentWidth = lineBeforeWordWidth
|
|
|
+ flush()
|
|
|
+ } else if currentWidth > 0 || wordWidth > 0 {
|
|
|
+ // Word is at start of line or single long word: commit what we have and flush.
|
|
|
+ commitWord()
|
|
|
+ flush()
|
|
|
+ }
|
|
|
}
|
|
|
+ pushSegment(&wordLine, cluster, segment.Style)
|
|
|
+ wordWidth += clusterWidth
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Flush remaining word and line.
|
|
|
+ commitWord()
|
|
|
if len(current) > 0 {
|
|
|
flush()
|
|
|
}
|