@ -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
r u := 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
r ightIndex = len ( runes ) - 1
for rightIndex > leftIndex && width - choppedLeft - choppedRight > maxWidth {
leftWidth := runewidth . RuneWidth ( ru nes[ leftIndex ] )
rightWidth := runewidth . RuneWidth ( ru nes[ rightIndex ] )
if choppedLeft < choppedRight {
start ++
choppedLeft += leftWidth
ru = ru [ 1 : ]
leftIndex++
} else {
choppedRight += rightWidth
r u = ru [ : len ( ru ) - 1 ]
r ightIndex--
}
}
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