Added support for wide unicode characters (e.g. Chinese). Resolves #9

This commit is contained in:
Oliver 2018-01-11 15:45:52 +01:00
parent 17c785e1f8
commit 61d8ea30f8
12 changed files with 123 additions and 49 deletions

20
box.go
View File

@ -2,6 +2,7 @@ package tview
import (
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// Box implements Primitive with a background and optional elements such as a
@ -188,11 +189,22 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw title.
if b.title != "" && b.width >= 4 {
width := b.width - 2
title := []rune(b.title)
if width < len(title) && width > 0 {
title = append(title[:width-1], GraphicsEllipsis)
title := b.title
titleWidth := runewidth.StringWidth(title)
if width < titleWidth && width > 0 {
// Grow title until we hit the end.
abbrWidth := runewidth.RuneWidth(GraphicsEllipsis)
abbrPos := 0
for pos, ch := range title {
if abbrWidth >= width {
title = title[:abbrPos] + string(GraphicsEllipsis)
break
}
abbrWidth += runewidth.RuneWidth(ch)
abbrPos = pos
}
}
Print(screen, string(title), b.x+1, b.y, width, b.titleAlign, b.titleColor)
Print(screen, title, b.x+1, b.y, width, b.titleAlign, b.titleColor)
}
}
}

View File

@ -2,6 +2,7 @@ package tview
import (
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// Button is labeled box that triggers an action when selected.
@ -33,7 +34,7 @@ type Button struct {
// NewButton returns a new input field.
func NewButton(label string) *Button {
box := NewBox().SetBackgroundColor(Styles.ContrastBackgroundColor)
box.SetRect(0, 0, len([]rune(label))+4, 1)
box.SetRect(0, 0, runewidth.StringWidth(label)+4, 1)
return &Button{
Box: box,
label: label,

View File

@ -133,7 +133,8 @@ func (c *Checkbox) Draw(screen tcell.Screen) {
}
// Draw label.
x += Print(screen, c.label, x, y, rightLimit-x, AlignLeft, c.labelColor)
_, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, c.labelColor)
x += drawnWidth
// Draw checkbox.
fieldStyle := tcell.StyleDefault.Background(c.fieldBackgroundColor).Foreground(c.fieldTextColor)

4
doc.go
View File

@ -65,6 +65,10 @@ When primitives are instantiated, they are initialized with colors taken from
the global Styles variable. You may change this variable to adapt the look and
feel of the primitives to your preferred style.
Unicode Support
This package supports unicode characters including wide characters.
Type Hierarchy
All widgets listed above contain the Box type. All of Box's functions are

View File

@ -2,6 +2,7 @@ package tview
import (
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// dropDownOption is one option that can be selected in a drop-down primitive.
@ -193,12 +194,13 @@ func (d *DropDown) Draw(screen tcell.Screen) {
}
// Draw label.
x += Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor)
_, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor)
x += drawnWidth
// What's the longest option text?
maxLength := 0
for _, option := range d.options {
length := len([]rune(option.Text))
length := runewidth.StringWidth(option.Text)
if length > maxLength {
maxLength = length
}

10
form.go
View File

@ -4,6 +4,7 @@ import (
"strings"
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// FormItem is the interface all form items must implement to be able to be
@ -197,8 +198,9 @@ func (f *Form) Draw(screen tcell.Screen) {
var labelLength int
for _, item := range f.items {
label := strings.TrimSpace(item.GetLabel())
if len([]rune(label)) > labelLength {
labelLength = len([]rune(label))
labelWidth := runewidth.StringWidth(label)
if labelWidth > labelLength {
labelLength = labelWidth
}
}
labelLength++ // Add one space.
@ -210,7 +212,7 @@ func (f *Form) Draw(screen tcell.Screen) {
}
label := strings.TrimSpace(item.GetLabel())
item.SetFormAttributes(
label+strings.Repeat(" ", labelLength-len([]rune(label))),
label+strings.Repeat(" ", labelLength-runewidth.StringWidth(label)),
f.labelColor,
f.backgroundColor,
f.fieldTextColor,
@ -228,7 +230,7 @@ func (f *Form) Draw(screen tcell.Screen) {
buttonWidths := make([]int, len(f.buttons))
buttonsWidth := 0
for index, button := range f.buttons {
width := len([]rune(button.GetLabel())) + 4
width := runewidth.StringWidth(button.GetLabel()) + 4
buttonWidths[index] = width
buttonsWidth += width + 2
}

View File

@ -5,6 +5,7 @@ import (
"regexp"
"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
@ -162,7 +163,8 @@ func (i *InputField) Draw(screen tcell.Screen) {
}
// Draw label.
x += Print(screen, i.label, x, y, rightLimit-x, AlignLeft, i.labelColor)
_, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, i.labelColor)
x += drawnWidth
// Draw input area.
fieldLength := i.fieldLength
@ -179,7 +181,7 @@ func (i *InputField) Draw(screen tcell.Screen) {
// Draw entered text.
fieldLength-- // We need one cell for the cursor.
if fieldLength < len([]rune(i.text)) {
if fieldLength < runewidth.StringWidth(i.text) {
Print(screen, i.text, x, y, fieldLength, AlignRight, i.fieldTextColor)
} else {
Print(screen, i.text, x, y, fieldLength, AlignLeft, i.fieldTextColor)
@ -201,11 +203,11 @@ func (i *InputField) setCursor(screen tcell.Screen) {
y++
rightLimit -= 2
}
fieldLength := len([]rune(i.text))
fieldLength := runewidth.StringWidth(i.text)
if i.fieldLength > 0 && fieldLength > i.fieldLength-1 {
fieldLength = i.fieldLength - 1
}
x += len([]rune(i.label)) + fieldLength
x += runewidth.StringWidth(i.label) + fieldLength
if x >= rightLimit {
x = rightLimit - 1
}
@ -239,7 +241,7 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
lastWord := regexp.MustCompile(`\s*\S+\s*$`)
i.text = lastWord.ReplaceAllString(i.text, "")
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character.
if len([]rune(i.text)) == 0 {
if len(i.text) == 0 {
break
}
runes := []rune(i.text)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// listItem represents one item in a List.
@ -207,7 +208,7 @@ func (l *List) Draw(screen tcell.Screen) {
// Main text.
color := l.mainTextColor
if index == l.currentItem {
textLength := len([]rune(item.MainText))
textLength := runewidth.StringWidth(item.MainText)
style := tcell.StyleDefault.Background(l.selectedBackgroundColor)
for bx := 0; bx < textLength && bx < width; bx++ {
screen.SetContent(x+bx, y, ' ', nil, style)

View File

@ -2,6 +2,7 @@ package tview
import (
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// Modal is a centered message window used to inform the user or prompt them
@ -101,7 +102,7 @@ func (m *Modal) Draw(screen tcell.Screen) {
// Calculate the width of this modal.
buttonsWidth := 0
for _, button := range m.form.buttons {
buttonsWidth += len([]rune(button.label)) + 4 + 2
buttonsWidth += runewidth.StringWidth(button.label) + 4 + 2
}
buttonsWidth -= 2
screenWidth, screenHeight := screen.Size()

View File

@ -2,6 +2,7 @@ package tview
import (
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// TableCell represents one cell inside a Table.
@ -13,9 +14,9 @@ type TableCell struct {
// or AlignRight.
Align int
// The maximum width of the cell. This is used to give a column a maximum
// width. Any cell text whose length exceeds this width is cut off. Set to
// 0 if there is no maximum width.
// The maximum width of the cell in screen space. This is used to give a
// column a maximum width. Any cell text whose screen width exceeds this width
// is cut off. Set to 0 if there is no maximum width.
MaxWidth int
// The color of the cell text.
@ -464,7 +465,7 @@ ColumnLoop:
maxWidth := -1
for _, row := range rows {
if cell := getCell(row, column); cell != nil {
cellWidth := len(cell.Text)
cellWidth := runewidth.StringWidth(cell.Text)
if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth {
cellWidth = cell.MaxWidth
}
@ -559,11 +560,22 @@ ColumnLoop:
cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth
// Draw text.
text := []rune(cell.Text)
if finalWidth < len(text) && finalWidth > 0 {
text = append(text[:finalWidth-1], GraphicsEllipsis)
text := cell.Text
textWidth := runewidth.StringWidth(text)
if finalWidth < textWidth && finalWidth > 0 {
// Grow title until we hit the end.
abbrWidth := runewidth.RuneWidth(GraphicsEllipsis)
abbrPos := 0
for pos, ch := range text {
if abbrWidth >= finalWidth {
text = text[:abbrPos] + string(GraphicsEllipsis)
break
}
abbrWidth += runewidth.RuneWidth(ch)
abbrPos = pos
}
}
Print(screen, string(text), x+columnX+1, y+rowY, finalWidth, cell.Align, textColor)
Print(screen, text, x+columnX+1, y+rowY, finalWidth, cell.Align, textColor)
}
// Draw bottom border.

View File

@ -8,6 +8,7 @@ import (
"unicode/utf8"
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// textColors maps color strings which may be embedded in text sent to a
@ -484,7 +485,7 @@ func (t *TextView) reindexBuffer(width int) {
// Break down the line.
var currentTag, currentRegion, currentWidth int
for pos := range str {
for pos, ch := range str {
// Skip any color tags.
if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
if pos == colorTagIndices[currentTag][1]-1 {
@ -529,7 +530,7 @@ func (t *TextView) reindexBuffer(width int) {
}
// Proceed.
currentWidth++
currentWidth += runewidth.RuneWidth(ch)
// Have we crossed the width?
if t.wrap && currentWidth >= width {
@ -652,7 +653,8 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Stop at the right border.
if posX >= width {
chWidth := runewidth.RuneWidth(ch)
if posX+chWidth > width {
break
}
@ -665,10 +667,12 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Draw the character.
screen.SetContent(x+posX, y+line-t.lineOffset, ch, nil, style)
for offset := 0; offset < chWidth; offset++ {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ch, nil, style)
}
// Advance.
posX++
posX += chWidth
}
}

66
util.go
View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// Text alignment within a box.
@ -90,40 +91,69 @@ func init() {
// not exceeding that box. "align" is one of AlignLeft, AlignCenter, or
// AlignRight. The screen's background color will not be changed.
//
// Returns the number of actual runes printed.
func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) int {
// Returns the number of actual runes printed and the actual width used for the
// printed runes.
func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) {
// We deal with runes, not with bytes.
runes := []rune(text)
if maxWidth < 0 {
return 0
return 0, 0
}
// AlignCenter is split into two parts.
// AlignCenter is a special case.
if align == AlignCenter {
half := len(runes) / 2
halfWidth := maxWidth / 2
return Print(screen, string(runes[:half]), x, y, halfWidth, AlignRight, color) +
Print(screen, string(runes[half:]), x+halfWidth, y, maxWidth-halfWidth, AlignLeft, color)
width := runewidth.StringWidth(text)
if width == maxWidth {
// Use the exact space.
return Print(screen, text, x, y, maxWidth, AlignLeft, color)
} else if width < maxWidth {
// We have more space than we need.
half := (maxWidth - width) / 2
return Print(screen, text, x+half, y, maxWidth-half, AlignLeft, color)
} else {
// Chop off runes until we have a perfect fit.
var start, choppedLeft, choppedRight int
ru := runes
for len(ru) > 0 && width-choppedLeft-choppedRight > maxWidth {
leftWidth := runewidth.RuneWidth(ru[0])
rightWidth := runewidth.RuneWidth(ru[len(ru)-1])
if choppedLeft < choppedRight {
start++
choppedLeft += leftWidth
ru = ru[1:]
} else {
choppedRight += rightWidth
ru = ru[:len(ru)-1]
}
}
return Print(screen, string(ru), x, y, maxWidth, AlignLeft, color)
}
}
// Draw text.
drawn := 0
drawnWidth := 0
for pos, ch := range runes {
if pos >= maxWidth {
chWidth := runewidth.RuneWidth(ch)
if drawnWidth+chWidth > maxWidth {
break
}
finalX := x + pos
finalX := x + drawnWidth
if align == AlignRight {
ch = runes[len(runes)-1-pos]
finalX = x + maxWidth - 1 - pos
finalX = x + maxWidth - chWidth - drawnWidth
}
_, _, style, _ := screen.GetContent(finalX, y)
style = style.Foreground(color)
screen.SetContent(finalX, y, ch, nil, style)
for offset := 0; offset < chWidth; offset++ {
// To avoid undesired effects, we place the same character in all cells.
screen.SetContent(finalX+offset, y, ch, nil, style)
}
drawn++
drawnWidth += chWidth
}
return drawn
return drawn, drawnWidth
}
// PrintSimple prints white text to the screen at the given position.
@ -132,8 +162,8 @@ func PrintSimple(screen tcell.Screen, text string, x, y int) {
}
// WordWrap splits a text such that each resulting line does not exceed the
// given width. Possible split points are after commas, dots, dashes, and any
// whitespace. Whitespace at split points will be dropped.
// given screen width. Possible split points are after commas, dots, dashes,
// and any whitespace. Whitespace at split points will be dropped.
//
// Text is always split at newline characters ('\n').
func WordWrap(text string, width int) (lines []string) {
@ -146,6 +176,8 @@ func WordWrap(text string, width int) (lines []string) {
text = strings.TrimSpace(text)
for pos, ch := range text {
chWidth := runewidth.RuneWidth(ch)
if !evaluatingCandidate && x >= width {
// We've exceeded the width, we must split.
if candidate >= 0 {
@ -190,8 +222,8 @@ func WordWrap(text string, width int) (lines []string) {
countAfterCandidate = 0
}
}
x++
countAfterCandidate++
x += chWidth
countAfterCandidate += chWidth
}
// Process remaining text.