Added support for wide unicode characters (e.g. Chinese). Resolves #9
This commit is contained in:
parent
17c785e1f8
commit
61d8ea30f8
20
box.go
20
box.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
4
doc.go
|
@ -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
|
||||
|
|
|
@ -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
10
form.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
3
list.go
3
list.go
|
@ -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)
|
||||
|
|
3
modal.go
3
modal.go
|
@ -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()
|
||||
|
|
28
table.go
28
table.go
|
@ -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.
|
||||
|
|
14
textview.go
14
textview.go
|
@ -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
66
util.go
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue