Browse Source

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

tablepad
Oliver 5 years ago
parent
commit
61d8ea30f8
  1. 20
      box.go
  2. 3
      button.go
  3. 3
      checkbox.go
  4. 4
      doc.go
  5. 6
      dropdown.go
  6. 10
      form.go
  7. 12
      inputfield.go
  8. 3
      list.go
  9. 3
      modal.go
  10. 28
      table.go
  11. 14
      textview.go
  12. 66
      util.go

20
box.go

@ -2,6 +2,7 @@ package tview @@ -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) { @@ -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)
}
}
}

3
button.go

@ -2,6 +2,7 @@ package tview @@ -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 { @@ -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,

3
checkbox.go

@ -133,7 +133,8 @@ func (c *Checkbox) Draw(screen tcell.Screen) { @@ -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

@ -65,6 +65,10 @@ When primitives are instantiated, they are initialized with colors taken from @@ -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

6
dropdown.go

@ -2,6 +2,7 @@ package tview @@ -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) { @@ -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

@ -4,6 +4,7 @@ import ( @@ -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) { @@ -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) { @@ -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) { @@ -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
}

12
inputfield.go

@ -5,6 +5,7 @@ import ( @@ -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) { @@ -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) { @@ -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) { @@ -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 @@ -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

@ -4,6 +4,7 @@ import ( @@ -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) { @@ -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

@ -2,6 +2,7 @@ package tview @@ -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) { @@ -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

@ -2,6 +2,7 @@ package tview @@ -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 { @@ -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: @@ -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: @@ -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

@ -8,6 +8,7 @@ import ( @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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

@ -7,6 +7,7 @@ import ( @@ -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() { @@ -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) { @@ -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) { @@ -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) { @@ -190,8 +222,8 @@ func WordWrap(text string, width int) (lines []string) {
countAfterCandidate = 0
}
}
x++
countAfterCandidate++
x += chWidth
countAfterCandidate += chWidth
}
// Process remaining text.

Loading…
Cancel
Save