diff --git a/button.go b/button.go index 5efc31a..d3a6aed 100644 --- a/button.go +++ b/button.go @@ -33,7 +33,7 @@ type Button struct { // NewButton returns a new input field. func NewButton(label string) *Button { box := NewBox().SetBackgroundColor(Styles.ContrastBackgroundColor) - box.SetRect(0, 0, StringWidth(label)+4, 1) + box.SetRect(0, 0, TaggedStringWidth(label)+4, 1) return &Button{ Box: box, label: label, diff --git a/dropdown.go b/dropdown.go index 79be05e..2a83930 100644 --- a/dropdown.go +++ b/dropdown.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/gdamore/tcell" - runewidth "github.com/mattn/go-runewidth" ) // dropDownOption is one option that can be selected in a drop-down primitive. @@ -182,7 +181,7 @@ func (d *DropDown) GetFieldWidth() int { } fieldWidth := 0 for _, option := range d.options { - width := StringWidth(option.Text) + width := TaggedStringWidth(option.Text) if width > fieldWidth { fieldWidth = width } @@ -268,7 +267,7 @@ func (d *DropDown) Draw(screen tcell.Screen) { // What's the longest option text? maxWidth := 0 for _, option := range d.options { - strWidth := StringWidth(option.Text) + strWidth := TaggedStringWidth(option.Text) if strWidth > maxWidth { maxWidth = strWidth } @@ -294,7 +293,7 @@ func (d *DropDown) Draw(screen tcell.Screen) { if d.open && len(d.prefix) > 0 { // Show the prefix. Print(screen, d.prefix, x, y, fieldWidth, AlignLeft, d.prefixTextColor) - prefixWidth := runewidth.StringWidth(d.prefix) + prefixWidth := stringWidth(d.prefix) listItemText := d.options[d.list.GetCurrentItem()].Text if prefixWidth < fieldWidth && len(d.prefix) < len(listItemText) { Print(screen, listItemText[len(d.prefix):], x+prefixWidth, y, fieldWidth-prefixWidth, AlignLeft, d.fieldTextColor) diff --git a/form.go b/form.go index aaa9ed7..16ee41b 100644 --- a/form.go +++ b/form.go @@ -335,7 +335,7 @@ func (f *Form) Draw(screen tcell.Screen) { // Find the longest label. var maxLabelWidth int for _, item := range f.items { - labelWidth := StringWidth(item.GetLabel()) + labelWidth := TaggedStringWidth(item.GetLabel()) if labelWidth > maxLabelWidth { maxLabelWidth = labelWidth } @@ -347,7 +347,7 @@ func (f *Form) Draw(screen tcell.Screen) { var focusedPosition struct{ x, y, width, height int } for index, item := range f.items { // Calculate the space needed. - labelWidth := StringWidth(item.GetLabel()) + labelWidth := TaggedStringWidth(item.GetLabel()) var itemWidth int if f.horizontal { fieldWidth := item.GetFieldWidth() @@ -401,7 +401,7 @@ func (f *Form) Draw(screen tcell.Screen) { buttonWidths := make([]int, len(f.buttons)) buttonsWidth := 0 for index, button := range f.buttons { - w := StringWidth(button.GetLabel()) + 4 + w := TaggedStringWidth(button.GetLabel()) + 4 buttonWidths[index] = w buttonsWidth += w + 1 } diff --git a/inputfield.go b/inputfield.go index 856cf3b..f1d87a3 100644 --- a/inputfield.go +++ b/inputfield.go @@ -7,7 +7,6 @@ import ( "unicode/utf8" "github.com/gdamore/tcell" - runewidth "github.com/mattn/go-runewidth" ) // InputField is a one-line box (three lines if there is a title) where the @@ -276,8 +275,7 @@ func (i *InputField) Draw(screen tcell.Screen) { if i.maskCharacter > 0 { text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text)) } - stringWidth := runewidth.StringWidth(text) - if fieldWidth >= stringWidth { + if fieldWidth >= stringWidth(text) { // We have enough space for the full text. Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor) i.offset = 0 @@ -299,7 +297,7 @@ func (i *InputField) Draw(screen tcell.Screen) { var shiftLeft int if i.offset > i.cursorPos { i.offset = i.cursorPos - } else if subWidth := runewidth.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 { + } else if subWidth := stringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 { shiftLeft = subWidth - fieldWidth + 1 } currentOffset := i.offset diff --git a/list.go b/list.go index e97a65b..7fa323a 100644 --- a/list.go +++ b/list.go @@ -426,7 +426,7 @@ func (l *List) Draw(screen tcell.Screen) { if index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus()) { textWidth := width if !l.highlightFullLine { - if w := StringWidth(item.MainText); w < textWidth { + if w := TaggedStringWidth(item.MainText); w < textWidth { textWidth = w } } diff --git a/modal.go b/modal.go index c388f22..11453d4 100644 --- a/modal.go +++ b/modal.go @@ -116,7 +116,7 @@ func (m *Modal) Draw(screen tcell.Screen) { // Calculate the width of this modal. buttonsWidth := 0 for _, button := range m.form.buttons { - buttonsWidth += StringWidth(button.label) + 4 + 2 + buttonsWidth += TaggedStringWidth(button.label) + 4 + 2 } buttonsWidth -= 2 screenWidth, screenHeight := screen.Size() diff --git a/table.go b/table.go index 2e411ea..0c810e8 100644 --- a/table.go +++ b/table.go @@ -792,7 +792,7 @@ ColumnLoop: } cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth _, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes)) - if StringWidth(cell.Text)-printed > 0 && printed > 0 { + if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY) printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style) } diff --git a/textview.go b/textview.go index 4f66bcb..7b99543 100644 --- a/textview.go +++ b/textview.go @@ -691,7 +691,7 @@ func (t *TextView) reindexBuffer(width int) { line := len(t.index) if t.fromHighlight < 0 { t.fromHighlight, t.toHighlight = line, line - t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart]) + t.posHighlight = stringWidth(splitLine[:strippedTagStart]) } else if line > t.toHighlight { t.toHighlight = line } @@ -709,7 +709,7 @@ func (t *TextView) reindexBuffer(width int) { // Append this line. line.NextPos = originalPos - line.Width = runewidth.StringWidth(splitLine) + line.Width = stringWidth(splitLine) t.index = append(t.index, line) } @@ -721,7 +721,7 @@ func (t *TextView) reindexBuffer(width int) { if spaces != nil && spaces[len(spaces)-1][1] == len(str) { oldNextPos := line.NextPos line.NextPos -= spaces[len(spaces)-1][1] - spaces[len(spaces)-1][0] - line.Width -= runewidth.StringWidth(t.buffer[line.Line][line.NextPos:oldNextPos]) + line.Width -= stringWidth(t.buffer[line.Line][line.NextPos:oldNextPos]) } } } diff --git a/util.go b/util.go index 46c5a27..a2415dd 100644 --- a/util.go +++ b/util.go @@ -171,7 +171,7 @@ func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgC func decomposeString(text string, findColors, findRegions bool) (colorIndices [][]int, colors [][]string, regionIndices [][]int, regions [][]string, escapeIndices [][]int, stripped string, width int) { // Shortcut for the trivial case. if !findColors && !findRegions { - return nil, nil, nil, nil, nil, text, runewidth.StringWidth(text) + return nil, nil, nil, nil, nil, text, stringWidth(text) } // Get positions of any tags. @@ -222,7 +222,7 @@ func decomposeString(text string, findColors, findRegions bool) (colorIndices [] stripped = string(escapePattern.ReplaceAll(buf, []byte("[$1$2]"))) // Get the width of the stripped string. - width = runewidth.StringWidth(stripped) + width = stringWidth(stripped) return } @@ -409,13 +409,31 @@ func PrintSimple(screen tcell.Screen, text string, x, y int) { Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor) } -// StringWidth returns the width of the given string needed to print it on +// TaggedStringWidth returns the width of the given string needed to print it on // screen. The text may contain color tags which are not counted. -func StringWidth(text string) int { +func TaggedStringWidth(text string) int { _, _, _, _, _, _, width := decomposeString(text, true, false) return width } +// stringWidth returns the number of horizontal cells needed to print the given +// text. It splits the text into its grapheme clusters, calculates each +// cluster's width, and adds them up to a total. +func stringWidth(text string) (width int) { + g := uniseg.NewGraphemes(text) + for g.Next() { + var chWidth int + for _, r := range g.Runes() { + chWidth = runewidth.RuneWidth(r) + if chWidth > 0 { + break // Our best guess at this point is to use the width of the first non-zero-width rune. + } + } + width += chWidth + } + return +} + // WordWrap splits a text such that each resulting line does not exceed the // given screen width. Possible split points are after any punctuation or // whitespace. Whitespace after split points will be dropped. @@ -534,7 +552,7 @@ func iterateString(text string, callback func(main rune, comb []rune, textPos, t for gr.Next() { r := gr.Runes() from, to := gr.Positions() - width := runewidth.StringWidth(gr.Str()) + width := stringWidth(gr.Str()) var comb []rune if len(r) > 1 { comb = r[1:]