Added color tag functionality to all strings. Resolves #25

This commit is contained in:
Oliver 2018-01-17 17:13:36 +01:00
parent 13cf1c1ee4
commit 258f212e5e
14 changed files with 332 additions and 185 deletions

View File

@ -62,6 +62,8 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio
## Releases
- v0.8 (2018-01-17)
- Color tags can now be used almost everywhere.
- v0.7 (2018-01-16)
- Forms can now also have a horizontal layout.
- v0.6 (2018-01-14)

22
box.go
View File

@ -2,7 +2,6 @@ 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
@ -222,23 +221,12 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw title.
if b.title != "" && b.width >= 4 {
width := b.width - 2
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
}
_, printed := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
if StringWidth(b.title)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(b.x+b.width-2, b.y)
fg, _, _ := style.Decompose()
Print(screen, string(GraphicsEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg)
}
Print(screen, title, b.x+1, b.y, width, b.titleAlign, b.titleColor)
}
}
}

View File

@ -2,7 +2,6 @@ package tview
import (
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// Button is labeled box that triggers an action when selected.
@ -34,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, runewidth.StringWidth(label)+4, 1)
box.SetRect(0, 0, StringWidth(label)+4, 1)
return &Button{
Box: box,
label: label,

View File

@ -0,0 +1,33 @@
package main
import (
"strings"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
const colorsText = `You can use color tags almost everywhere to partially change the color of a string. Simply put a color name or hex string in square brackets to change the following characters' color. H[green]er[white]e i[yellow]s a[darkcyan]n ex[red]amp[white]le. The tags look like this: [red[] [#00ff00[]`
// Colors demonstrates how to use colors.
func Colors(nextSlide func()) (title string, content tview.Primitive) {
table := tview.NewTable().
SetBorders(true).
SetBordersColor(tcell.ColorBlue).
SetDoneFunc(func(key tcell.Key) {
nextSlide()
})
var row, column int
for _, word := range strings.Split(colorsText, " ") {
table.SetCellSimple(row, column, word)
column++
if column > 6 {
column = 0
row++
}
}
table.SetBorderPadding(1, 1, 2, 2).
SetBorder(true).
SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] title")
return "Colors", Center(73, 19, table)
}

View File

@ -41,6 +41,7 @@ func main() {
TextView2,
Table,
Flex,
Colors,
End,
}

30
doc.go
View File

@ -59,6 +59,32 @@ You will find more demos in the "demos" subdirectory. It also contains a
presentation (written using tview) which gives an overview of the different
widgets and how they can be used.
Colors
Throughout this package, colors are specified using the tcell.Color type.
Functions such as tcell.GetColor(), tcell.NewHexColor(), and tcell.NewRGBColor()
can be used to create colors from W3C color names or RGB values.
Almost all strings which are displayed can contain color tags. Color tags are
W3C color names or six hexadecimal digits following a hash tag, wrapped in
square brackets. Examples:
This is a [red]warning[white]!
The sky is [#8080ff]blue[#ffffff].
A color tag changes the color of the characters following that color tag. This
applies to almost everything from box titles, list text, form item labels, to
table cells. In a TextView, this functionality has to be switched on explicitly.
See the TextView documentation for more information.
In the rare event that you want to display a string such as "[red]" or
"[#00ff1a]" without applying its effect, you need to put an opening square
bracket before the closing square bracket. Examples:
[red[] will be output as [red]
["123"[] will be output as ["123"]
[#6aff00[[] will be output as [#6aff00[]
Styles
When primitives are instantiated, they are initialized with colors taken from
@ -77,8 +103,8 @@ therefore available for all widgets, too.
All widgets also implement the Primitive interface. There is also the Focusable
interface which is used to override functions in subclassing types.
The tview package is based on github.com/gdamore/tcell. It uses types and
constants from that package (e.g. colors and keyboard values).
The tview package is based on https://github.com/gdamore/tcell. It uses types
and constants from that package (e.g. colors and keyboard values).
This package does not process mouse input (yet).
*/

View File

@ -2,7 +2,6 @@ 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.
@ -145,7 +144,7 @@ func (d *DropDown) GetFieldLength() int {
}
fieldLength := 0
for _, option := range d.options {
length := runewidth.StringWidth(option.Text)
length := StringWidth(option.Text)
if length > fieldLength {
fieldLength = length
}
@ -215,7 +214,7 @@ func (d *DropDown) Draw(screen tcell.Screen) {
// What's the longest option text?
maxLength := 0
for _, option := range d.options {
length := runewidth.StringWidth(option.Text)
length := StringWidth(option.Text)
if length > maxLength {
maxLength = length
}

View File

@ -4,7 +4,6 @@ import (
"strings"
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// DefaultFormFieldLength is the default field length of form elements whose
@ -262,7 +261,7 @@ func (f *Form) Draw(screen tcell.Screen) {
var labelLength int
for _, item := range f.items {
label := strings.TrimSpace(item.GetLabel())
labelWidth := runewidth.StringWidth(label)
labelWidth := StringWidth(label)
if labelWidth > labelLength {
labelLength = labelWidth
}
@ -278,7 +277,7 @@ func (f *Form) Draw(screen tcell.Screen) {
// Calculate the space needed.
label := strings.TrimSpace(item.GetLabel())
labelWidth := runewidth.StringWidth(label)
labelWidth := StringWidth(label)
var itemWidth int
if f.horizontal {
fieldLength := item.GetFieldLength()
@ -331,7 +330,7 @@ func (f *Form) Draw(screen tcell.Screen) {
buttonWidths := make([]int, len(f.buttons))
buttonsWidth := 0
for index, button := range f.buttons {
width := runewidth.StringWidth(button.GetLabel()) + 4
width := StringWidth(button.GetLabel()) + 4
buttonWidths[index] = width
buttonsWidth += width + 1
}

View File

@ -188,15 +188,15 @@ func (i *InputField) Draw(screen tcell.Screen) {
x += drawnWidth
// Draw input area.
fieldLength := i.fieldLength
if fieldLength == 0 {
fieldLength = math.MaxInt32
fieldWidth := i.fieldLength
if fieldWidth == 0 {
fieldWidth = math.MaxInt32
}
if rightLimit-x < fieldLength {
fieldLength = rightLimit - x
if rightLimit-x < fieldWidth {
fieldWidth = rightLimit - x
}
fieldStyle := tcell.StyleDefault.Background(i.fieldBackgroundColor)
for index := 0; index < fieldLength; index++ {
for index := 0; index < fieldWidth; index++ {
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
}
@ -205,11 +205,35 @@ func (i *InputField) Draw(screen tcell.Screen) {
if i.maskCharacter > 0 {
text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
}
fieldLength-- // We need one cell for the cursor.
if fieldLength < runewidth.StringWidth(i.text) {
Print(screen, text, x, y, fieldLength, AlignRight, i.fieldTextColor)
fieldWidth-- // We need one cell for the cursor.
if fieldWidth < runewidth.StringWidth(i.text) {
runes := []rune(i.text)
for pos := len(runes) - 1; pos >= 0; pos-- {
ch := runes[pos]
w := runewidth.RuneWidth(ch)
if fieldWidth-w < 0 {
break
}
_, _, style, _ := screen.GetContent(x+fieldWidth-w, y)
style = style.Foreground(i.fieldTextColor)
for w > 0 {
fieldWidth--
screen.SetContent(x+fieldWidth, y, ch, nil, style)
w--
}
}
} else {
Print(screen, text, x, y, fieldLength, AlignLeft, i.fieldTextColor)
pos := 0
for _, ch := range text {
w := runewidth.RuneWidth(ch)
_, _, style, _ := screen.GetContent(x+pos, y)
style = style.Foreground(i.fieldTextColor)
for w > 0 {
screen.SetContent(x+pos, y, ch, nil, style)
pos++
w--
}
}
}
// Set cursor.
@ -232,7 +256,7 @@ func (i *InputField) setCursor(screen tcell.Screen) {
if i.fieldLength > 0 && fieldLength > i.fieldLength-1 {
fieldLength = i.fieldLength - 1
}
x += runewidth.StringWidth(i.label) + fieldLength
x += StringWidth(i.label) + fieldLength
if x >= rightLimit {
x = rightLimit - 1
}

19
list.go
View File

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// listItem represents one item in a List.
@ -206,16 +205,22 @@ func (l *List) Draw(screen tcell.Screen) {
}
// Main text.
color := l.mainTextColor
Print(screen, item.MainText, x, y, width, AlignLeft, l.mainTextColor)
// Background color of selected text.
if index == l.currentItem {
textLength := runewidth.StringWidth(item.MainText)
style := tcell.StyleDefault.Background(l.selectedBackgroundColor)
textLength := StringWidth(item.MainText)
for bx := 0; bx < textLength && bx < width; bx++ {
screen.SetContent(x+bx, y, ' ', nil, style)
m, c, style, _ := screen.GetContent(x+bx, y)
fg, _, _ := style.Decompose()
if fg == l.mainTextColor {
fg = l.selectedTextColor
}
style = style.Background(l.selectedBackgroundColor).Foreground(fg)
screen.SetContent(x+bx, y, m, c, style)
}
color = l.selectedTextColor
}
Print(screen, item.MainText, x, y, width, AlignLeft, color)
y++
if y >= bottomLimit {

View File

@ -2,7 +2,6 @@ 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
@ -102,7 +101,7 @@ func (m *Modal) Draw(screen tcell.Screen) {
// Calculate the width of this modal.
buttonsWidth := 0
for _, button := range m.form.buttons {
buttonsWidth += runewidth.StringWidth(button.label) + 4 + 2
buttonsWidth += StringWidth(button.label) + 4 + 2
}
buttonsWidth -= 2
screenWidth, screenHeight := screen.Size()

View File

@ -2,7 +2,6 @@ package tview
import (
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
)
// TableCell represents one cell inside a Table. You can instantiate this type
@ -144,6 +143,8 @@ func (c *TableCell) GetLastPosition() (x, y, width int) {
// rows and columns). When there is a selection, the user moves the selection.
// The class will attempt to keep the selection from moving out of the screen.
//
// Use SetInputCapture() to override or modify keyboard input.
//
// See https://github.com/rivo/tview/wiki/Table for an example.
type Table struct {
*Box
@ -550,7 +551,7 @@ ColumnLoop:
maxWidth := -1
for _, row := range rows {
if cell := getCell(row, column); cell != nil {
cellWidth := runewidth.StringWidth(cell.Text)
cellWidth := StringWidth(cell.Text)
if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth {
cellWidth = cell.MaxWidth
}
@ -628,59 +629,61 @@ ColumnLoop:
continue
}
// Determine cell colors.
bgColor := t.backgroundColor
textColor := cell.Color
if cell.BackgroundColor != tcell.ColorDefault {
bgColor = cell.BackgroundColor
}
if cellSelected && !cell.NotSelectable {
textColor, bgColor = bgColor, textColor
}
// Draw cell background.
bgStyle := tcell.StyleDefault.Background(bgColor)
// Draw text.
finalWidth := columnWidth
if columnX+1+columnWidth >= width {
finalWidth = width - columnX - 1
}
for pos := 0; pos < finalWidth; pos++ {
screen.SetContent(x+columnX+1+pos, y+rowY, ' ', nil, bgStyle)
}
cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth
_, printed := Print(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, cell.Color)
if StringWidth(cell.Text)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY)
fg, _, _ := style.Decompose()
Print(screen, string(GraphicsEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, fg)
}
// Draw cell background.
if cellSelected && !cell.NotSelectable || cell.BackgroundColor != tcell.ColorDefault {
for pos := 0; pos < finalWidth; pos++ {
m, c, style, _ := screen.GetContent(x+columnX+1+pos, y+rowY)
if cellSelected && !cell.NotSelectable {
// Create style for a selected cell.
fg, _, _ := style.Decompose()
if fg == cell.Color {
fg = cell.BackgroundColor
if fg == tcell.ColorDefault {
fg = t.backgroundColor
}
}
style = style.Background(cell.Color).Foreground(fg)
} else {
// Create style for a cell with a colored background.
style = style.Background(cell.BackgroundColor)
}
screen.SetContent(x+columnX+1+pos, y+rowY, m, c, style)
}
}
// We may want continuous background colors in rows so change
// border/separator background colors, too.
if cell.BackgroundColor != tcell.ColorDefault && column > 0 {
cellBackground := cell.BackgroundColor
if cellSelected && !cell.NotSelectable {
cellBackground = cell.Color
}
if cellBackground != tcell.ColorDefault && column > 0 {
leftCell := getCell(row, column-1)
if leftCell != nil {
if cell.BackgroundColor == leftCell.BackgroundColor {
_, _, style, _ := screen.GetContent(x+columnX+1, y+rowY)
m, c, _, _ := screen.GetContent(x+columnX, y+rowY)
_, bgColor, _ := style.Decompose()
if t.columnsSelectable && column == t.selectedColumn {
bgColor = cell.BackgroundColor
}
ch, _, style, _ := screen.GetContent(x+columnX, y+rowY)
screen.SetContent(x+columnX, y+rowY, ch, nil, style.Background(bgColor))
screen.SetContent(x+columnX, y+rowY, m, c, style.Background(bgColor).Foreground(t.bordersColor))
}
}
}
// Draw text.
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, text, x+columnX+1, y+rowY, finalWidth, cell.Align, textColor)
}
// Draw bottom border.

View File

@ -10,15 +10,6 @@ import (
runewidth "github.com/mattn/go-runewidth"
)
// Regular expressions commonly used throughout the TextView class.
var (
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})\]`)
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
escapePattern = regexp.MustCompile(`\[("[a-zA-Z0-9_,;: \-\.]*"|[a-zA-Z]+|#[0-9a-zA-Z]{6})\[(\[*)\]`)
boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
spacePattern = regexp.MustCompile(`\s+`)
)
// TabSize is the number of spaces with which a tab character will be replaced.
var TabSize = 4
@ -55,21 +46,14 @@ type textViewIndex struct {
// If the text is not scrollable, any text above the top visible line is
// discarded.
//
// Navigation can be intercepted by installing a callback function via
// SetCaptureFunc() which receives all keyboard events and decides which ones
// to forward to the default handler.
// Use SetInputCapture() to override or modify keyboard input.
//
// Colors
//
// If dynamic colors are enabled via SetDynamicColors(), text color can be
// changed dynamically by embedding color strings in square brackets. For
// example,
//
// This is a [red]warning[white]!
//
// will print the word "warning" in red. You can provide W3C color names or
// hex strings starting with "#", followed by 6 hexadecimal digits. See
// tcell.GetColor() for more information.
// changed dynamically by embedding color strings in square brackets. This works
// the same way as anywhere else. Please see the package documentation for more
// information.
//
// Regions and Highlights
//
@ -92,17 +76,6 @@ type textViewIndex struct {
// The ScrollToHighlight() function can be used to jump to the currently
// highlighted region once when the text view is drawn the next time.
//
// Escape Tags
//
// In the rare case that you have color tags or regions enabled but still want
// to output a tag as text instead of causing its functionality, you can close
// the tag with an opening and closing bracket "[]" instead of only a closing
// bracket "]". Examples:
//
// [red[] will be output as [red]
// ["123"[] will be output as ["123"]
// [#6aff00[[] will be output as [#6aff00[]
//
// See https://github.com/rivo/tview/wiki/TextView for an example.
type TextView struct {
sync.Mutex
@ -174,10 +147,6 @@ type TextView struct {
// highlight(s) into the visible screen.
scrollToHighlights bool
// An optional function which will receive all key events sent to this text
// view. Returning true also invokes the default key handling.
capture func(*tcell.EventKey) bool
// An optional function which is called when the content of the text view has
// changed.
changed func()
@ -273,15 +242,6 @@ func (t *TextView) SetRegions(regions bool) *TextView {
return t
}
// SetCaptureFunc sets a handler which is called whenever a key is pressed.
// This allows you to override the default key handling of the text view.
// Returning true will allow the default key handling to go forward after the
// handler returns. Returning false will disable any default key handling.
func (t *TextView) SetCaptureFunc(handler func(event *tcell.EventKey) bool) *TextView {
t.capture = handler
return t
}
// SetChangedFunc sets a handler function which is called when the text of the
// text view has changed. This is typically used to cause the application to
// redraw the screen.
@ -600,20 +560,15 @@ func (t *TextView) reindexBuffer(width int) {
}
// Shift original position with tags.
lineWidth := 0
for index, ch := range splitLine {
// Get the width of the current rune.
lineWidth += runewidth.RuneWidth(ch)
// Process color tags.
for colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+index {
lineLength := len(splitLine)
for {
if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength {
// Process color tags.
originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
color = tcell.GetColor(colorTags[colorPos][1])
colorPos++
}
// Process region tags.
for regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+index {
} else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength {
// Process region tags.
originalPos += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionID = regions[regionPos][1]
_, highlighted = t.highlights[regionID]
@ -629,22 +584,22 @@ func (t *TextView) reindexBuffer(width int) {
}
regionPos++
}
// Process escape tags.
for escapePos < len(escapeIndices) && escapeIndices[escapePos][0] <= originalPos+index {
} else if escapePos < len(escapeIndices) && escapeIndices[escapePos][0] <= originalPos+lineLength {
// Process escape tags.
originalPos++
escapePos++
} else {
break
}
}
// Advance to next line.
startPos += len(splitLine)
originalPos += len(splitLine)
startPos += lineLength
originalPos += lineLength
// Append this line.
line.NextPos = originalPos
line.Width = lineWidth
line.Width = runewidth.StringWidth(splitLine)
t.index = append(t.index, line)
}
@ -877,13 +832,6 @@ func (t *TextView) Draw(screen tcell.Screen) {
// InputHandler returns the handler for this primitive.
func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.wrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// Do we pass this event on?
if t.capture != nil {
if !t.capture(event) {
return
}
}
key := event.Key()
if key == tcell.KeyEscape || key == tcell.KeyEnter || key == tcell.KeyTab || key == tcell.KeyBacktab {

181
util.go
View File

@ -2,6 +2,7 @@ package tview
import (
"math"
"regexp"
"strconv"
"strings"
@ -38,6 +39,16 @@ const (
GraphicsEllipsis = '\u2026'
)
// Common regular expressions.
var (
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})\]`)
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
escapePattern = regexp.MustCompile(`\[("[a-zA-Z0-9_,;: \-\.]*"|[a-zA-Z]+|#[0-9a-zA-Z]{6})\[(\[*)\]`)
boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
spacePattern = regexp.MustCompile(`\s+`)
)
// Predefined InputField acceptance functions.
var (
// InputFieldInteger accepts integers.
InputFieldInteger func(text string, ch rune) bool
@ -63,7 +74,7 @@ func init() {
return err == nil
}
InputFieldFloat = func(text string, ch rune) bool {
if text == "-" || text == "." {
if text == "-" || text == "." || text == "-." {
return true
}
_, err := strconv.ParseFloat(text, 64)
@ -80,18 +91,80 @@ 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 and the actual width used for the
// printed runes.
// You can change the text color mid-text by inserting a color tag. See the
// package description for details.
//
// Returns the number of actual runes printed (not including color tags) 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, 0
}
// AlignCenter is a special case.
if align == AlignCenter {
width := runewidth.StringWidth(text)
// Get positions of color and escape tags. Remove them from original string.
colorIndices := colorPattern.FindAllStringIndex(text, -1)
colors := colorPattern.FindAllStringSubmatch(text, -1)
escapeIndices := escapePattern.FindAllStringIndex(text, -1)
strippedText := escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]")
// We deal with runes, not with bytes.
runes := []rune(strippedText)
// This helper function takes positions for a substring of "runes" and a start
// color and returns the substring with the original tags and the new start
// color.
substring := func(from, to int, color tcell.Color) (string, tcell.Color) {
var colorPos, escapePos, runePos, startPos int
for pos := range text {
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
if pos == colorIndices[colorPos][1]-1 {
if runePos <= from {
color = tcell.GetColor(colors[colorPos][1])
}
colorPos++
}
continue
}
// Handle escape tags.
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
if pos == escapeIndices[escapePos][1]-1 {
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
}
}
// Check boundaries.
if runePos == from {
startPos = pos
} else if runePos >= to {
return text[startPos:pos], color
}
runePos++
}
return text[startPos:len(text)], color
}
// We want to reduce everything to AlignLeft.
if align == AlignRight {
width := 0
start := len(runes)
for index := start - 1; index >= 0; index-- {
w := runewidth.RuneWidth(runes[index])
if width+w > maxWidth {
break
}
width += w
start = index
}
text, color = substring(start, len(runes), color)
return Print(screen, text, x+maxWidth-width, y, width, AlignLeft, color)
} else if align == AlignCenter {
width := runewidth.StringWidth(strippedText)
if width == maxWidth {
// Use the exact space.
return Print(screen, text, x, y, maxWidth, AlignLeft, color)
@ -101,43 +174,62 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc
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])
var choppedLeft, choppedRight, leftIndex, rightIndex int
rightIndex = len(runes) - 1
for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth {
leftWidth := runewidth.RuneWidth(runes[leftIndex])
rightWidth := runewidth.RuneWidth(runes[rightIndex])
if choppedLeft < choppedRight {
start++
choppedLeft += leftWidth
ru = ru[1:]
leftIndex++
} else {
choppedRight += rightWidth
ru = ru[:len(ru)-1]
rightIndex--
}
}
return Print(screen, string(ru), x, y, maxWidth, AlignLeft, color)
text, color = substring(leftIndex, rightIndex, color)
return Print(screen, text, x, y, maxWidth, AlignLeft, color)
}
}
// Draw text.
drawn := 0
drawnWidth := 0
for pos, ch := range runes {
var colorPos, escapePos int
for pos, ch := range text {
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
if pos == colorIndices[colorPos][1]-1 {
color = tcell.GetColor(colors[colorPos][1])
colorPos++
}
continue
}
// Handle escape tags.
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
if pos == escapeIndices[escapePos][1]-1 {
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
}
}
// Check if we have enough space for this rune.
chWidth := runewidth.RuneWidth(ch)
if drawnWidth+chWidth > maxWidth {
break
}
finalX := x + drawnWidth
if align == AlignRight {
ch = runes[len(runes)-1-pos]
finalX = x + maxWidth - chWidth - drawnWidth
}
// Print the rune.
_, _, style, _ := screen.GetContent(finalX, y)
style = style.Foreground(color)
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
}
@ -150,9 +242,17 @@ 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
// screen. The text may contain color tags which are not counted.
func StringWidth(text string) int {
return runewidth.StringWidth(escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]"))
}
// WordWrap splits a text such that each resulting line does not exceed the
// given screen 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 any punctuation or
// whitespace. Whitespace after split points will be dropped.
//
// This function considers color tags to have no width.
//
// Text is always split at newline characters ('\n').
func WordWrap(text string, width int) (lines []string) {
@ -163,8 +263,29 @@ func WordWrap(text string, width int) (lines []string) {
countAfterCandidate := 0
var evaluatingCandidate bool
text = strings.TrimSpace(text)
colorIndices := colorPattern.FindAllStringIndex(text, -1)
escapeIndices := escapePattern.FindAllStringIndex(text, -1)
var colorPos, escapePos int
for pos, ch := range text {
// Skip color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
if pos == colorIndices[colorPos][1]-1 {
colorPos++
}
continue
}
// Handle escape tags.
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
if pos == escapeIndices[escapePos][1]-1 {
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
}
}
// What's the width of this rune?
chWidth := runewidth.RuneWidth(ch)
if !evaluatingCandidate && x >= width {
@ -182,21 +303,21 @@ func WordWrap(text string, width int) (lines []string) {
evaluatingCandidate = false
}
switch ch {
switch {
// We have a candidate.
case ',', '.', '-':
case ch >= '!' && ch <= '/', ch >= ':' && ch <= '@', ch >= '[' && ch <= '`', ch >= '{' && ch <= '~':
if x > 0 {
candidate = pos + 1
evaluatingCandidate = true
}
// If we've had a candidate, skip whitespace. If not, we have a candidate.
case ' ', '\t':
// If we've had a candidate, skip whitespace. If not, we have a candidate.
case ch == ' ', ch == '\t':
if x > 0 && !evaluatingCandidate {
candidate = pos
evaluatingCandidate = true
}
// Split in any case.
case '\n':
// Split in any case.
case ch == '\n':
lines = append(lines, text[start:pos])
start = pos + 1
evaluatingCandidate = false