package cview
import (
"bytes"
"math"
"regexp"
"sync"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
type InputField struct {
*Box
text []byte
label []byte
placeholder []byte
labelColor tcell .Color
labelColorFocused tcell .Color
fieldBackgroundColor tcell .Color
fieldBackgroundColorFocused tcell .Color
fieldTextColor tcell .Color
fieldTextColorFocused tcell .Color
placeholderTextColor tcell .Color
placeholderTextColorFocused tcell .Color
autocompleteListTextColor tcell .Color
autocompleteListBackgroundColor tcell .Color
autocompleteListSelectedTextColor tcell .Color
autocompleteListSelectedBackgroundColor tcell .Color
autocompleteSuggestionTextColor tcell .Color
fieldNoteTextColor tcell .Color
fieldNote []byte
labelWidth int
fieldWidth int
maskCharacter rune
cursorPos int
autocomplete func (text string ) []*ListItem
autocompleteList *List
autocompleteListSuggestion []byte
accept func (text string , ch rune ) bool
changed func (text string )
done func (tcell .Key )
finished func (tcell .Key )
fieldX int
offset int
sync .RWMutex
}
func NewInputField () *InputField {
return &InputField {
Box : NewBox (),
labelColor : Styles .SecondaryTextColor ,
fieldBackgroundColor : Styles .MoreContrastBackgroundColor ,
fieldBackgroundColorFocused : Styles .ContrastBackgroundColor ,
fieldTextColor : Styles .PrimaryTextColor ,
fieldTextColorFocused : Styles .PrimaryTextColor ,
placeholderTextColor : Styles .ContrastSecondaryTextColor ,
autocompleteListTextColor : Styles .PrimitiveBackgroundColor ,
autocompleteListBackgroundColor : Styles .MoreContrastBackgroundColor ,
autocompleteListSelectedTextColor : Styles .PrimitiveBackgroundColor ,
autocompleteListSelectedBackgroundColor : Styles .PrimaryTextColor ,
autocompleteSuggestionTextColor : Styles .ContrastSecondaryTextColor ,
fieldNoteTextColor : Styles .SecondaryTextColor ,
labelColorFocused : ColorUnset ,
placeholderTextColorFocused : ColorUnset ,
}
}
func (i *InputField ) SetText (text string ) {
i .Lock ()
i .text = []byte (text )
i .cursorPos = len (text )
if i .changed != nil {
i .Unlock ()
i .changed (text )
} else {
i .Unlock ()
}
}
func (i *InputField ) GetText () string {
i .RLock ()
defer i .RUnlock ()
return string (i .text )
}
func (i *InputField ) SetLabel (label string ) {
i .Lock ()
defer i .Unlock ()
i .label = []byte (label )
}
func (i *InputField ) GetLabel () string {
i .RLock ()
defer i .RUnlock ()
return string (i .label )
}
func (i *InputField ) SetLabelWidth (width int ) {
i .Lock ()
defer i .Unlock ()
i .labelWidth = width
}
func (i *InputField ) SetPlaceholder (text string ) {
i .Lock ()
defer i .Unlock ()
i .placeholder = []byte (text )
}
func (i *InputField ) SetLabelColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .labelColor = color
}
func (i *InputField ) SetLabelColorFocused (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .labelColorFocused = color
}
func (i *InputField ) SetFieldBackgroundColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .fieldBackgroundColor = color
}
func (i *InputField ) SetFieldBackgroundColorFocused (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .fieldBackgroundColorFocused = color
}
func (i *InputField ) SetFieldTextColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .fieldTextColor = color
}
func (i *InputField ) SetFieldTextColorFocused (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .fieldTextColorFocused = color
}
func (i *InputField ) SetPlaceholderTextColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .placeholderTextColor = color
}
func (i *InputField ) SetPlaceholderTextColorFocused (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .placeholderTextColorFocused = color
}
func (i *InputField ) SetAutocompleteListTextColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .autocompleteListTextColor = color
}
func (i *InputField ) SetAutocompleteListBackgroundColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .autocompleteListBackgroundColor = color
}
func (i *InputField ) SetAutocompleteListSelectedTextColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .autocompleteListSelectedTextColor = color
}
func (i *InputField ) SetAutocompleteListSelectedBackgroundColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .autocompleteListSelectedBackgroundColor = color
}
func (i *InputField ) SetAutocompleteSuggestionTextColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .autocompleteSuggestionTextColor = color
}
func (i *InputField ) SetFieldNoteTextColor (color tcell .Color ) {
i .Lock ()
defer i .Unlock ()
i .fieldNoteTextColor = color
}
func (i *InputField ) SetFieldNote (note string ) {
i .Lock ()
defer i .Unlock ()
i .fieldNote = []byte (note )
}
func (i *InputField ) ResetFieldNote () {
i .Lock ()
defer i .Unlock ()
i .fieldNote = nil
}
func (i *InputField ) SetFieldWidth (width int ) {
i .Lock ()
defer i .Unlock ()
i .fieldWidth = width
}
func (i *InputField ) GetFieldWidth () int {
i .RLock ()
defer i .RUnlock ()
return i .fieldWidth
}
func (i *InputField ) GetFieldHeight () int {
i .RLock ()
defer i .RUnlock ()
if len (i .fieldNote ) == 0 {
return 1
}
return 2
}
func (i *InputField ) GetCursorPosition () int {
i .RLock ()
defer i .RUnlock ()
return i .cursorPos
}
func (i *InputField ) SetCursorPosition (cursorPos int ) {
i .Lock ()
defer i .Unlock ()
i .cursorPos = cursorPos
}
func (i *InputField ) SetMaskCharacter (mask rune ) {
i .Lock ()
defer i .Unlock ()
i .maskCharacter = mask
}
func (i *InputField ) SetAutocompleteFunc (callback func (currentText string ) (entries []*ListItem )) {
i .Lock ()
i .autocomplete = callback
i .Unlock ()
i .Autocomplete ()
}
func (i *InputField ) Autocomplete () {
i .Lock ()
if i .autocomplete == nil {
i .Unlock ()
return
}
i .Unlock ()
entries := i .autocomplete (string (i .text ))
if len (entries ) == 0 {
i .Lock ()
i .autocompleteList = nil
i .autocompleteListSuggestion = nil
i .Unlock ()
return
}
i .Lock ()
if i .autocompleteList == nil {
l := NewList ()
l .SetChangedFunc (i .autocompleteChanged )
l .ShowSecondaryText (false )
l .SetMainTextColor (i .autocompleteListTextColor )
l .SetSelectedTextColor (i .autocompleteListSelectedTextColor )
l .SetSelectedBackgroundColor (i .autocompleteListSelectedBackgroundColor )
l .SetHighlightFullLine (true )
l .SetBackgroundColor (i .autocompleteListBackgroundColor )
i .autocompleteList = l
}
currentEntry := -1
i .autocompleteList .Clear ()
for index , entry := range entries {
i .autocompleteList .AddItem (entry )
if currentEntry < 0 && entry .GetMainText () == string (i .text ) {
currentEntry = index
}
}
if currentEntry >= 0 {
i .autocompleteList .SetCurrentItem (currentEntry )
}
i .Unlock ()
}
func (i *InputField ) autocompleteChanged (_ int , item *ListItem ) {
mainText := item .GetMainBytes ()
secondaryText := item .GetSecondaryBytes ()
if len (i .text ) < len (secondaryText ) {
i .autocompleteListSuggestion = secondaryText [len (i .text ):]
} else if len (i .text ) < len (mainText ) {
i .autocompleteListSuggestion = mainText [len (i .text ):]
} else {
i .autocompleteListSuggestion = nil
}
}
func (i *InputField ) SetAcceptanceFunc (handler func (textToCheck string , lastChar rune ) bool ) {
i .Lock ()
defer i .Unlock ()
i .accept = handler
}
func (i *InputField ) SetChangedFunc (handler func (text string )) {
i .Lock ()
defer i .Unlock ()
i .changed = handler
}
func (i *InputField ) SetDoneFunc (handler func (key tcell .Key )) {
i .Lock ()
defer i .Unlock ()
i .done = handler
}
func (i *InputField ) SetFinishedFunc (handler func (key tcell .Key )) {
i .Lock ()
defer i .Unlock ()
i .finished = handler
}
func (i *InputField ) Draw (screen tcell .Screen ) {
if !i .GetVisible () {
return
}
i .Box .Draw (screen )
i .Lock ()
defer i .Unlock ()
labelColor := i .labelColor
fieldBackgroundColor := i .fieldBackgroundColor
fieldTextColor := i .fieldTextColor
if i .GetFocusable ().HasFocus () {
if i .labelColorFocused != ColorUnset {
labelColor = i .labelColorFocused
}
if i .fieldBackgroundColorFocused != ColorUnset {
fieldBackgroundColor = i .fieldBackgroundColorFocused
}
if i .fieldTextColorFocused != ColorUnset {
fieldTextColor = i .fieldTextColorFocused
}
}
x , y , width , height := i .GetInnerRect ()
rightLimit := x + width
if height < 1 || rightLimit <= x {
return
}
if i .labelWidth > 0 {
labelWidth := i .labelWidth
if labelWidth > rightLimit -x {
labelWidth = rightLimit - x
}
Print (screen , i .label , x , y , labelWidth , AlignLeft , labelColor )
x += labelWidth
} else {
_ , drawnWidth := Print (screen , i .label , x , y , rightLimit -x , AlignLeft , labelColor )
x += drawnWidth
}
i .fieldX = x
fieldWidth := i .fieldWidth
if fieldWidth == 0 {
fieldWidth = math .MaxInt32
}
if rightLimit -x < fieldWidth {
fieldWidth = rightLimit - x
}
fieldStyle := tcell .StyleDefault .Background (fieldBackgroundColor )
for index := 0 ; index < fieldWidth ; index ++ {
screen .SetContent (x +index , y , ' ' , nil , fieldStyle )
}
var cursorScreenPos int
text := i .text
if len (text ) == 0 && len (i .placeholder ) > 0 {
placeholderTextColor := i .placeholderTextColor
if i .GetFocusable ().HasFocus () && i .placeholderTextColorFocused != ColorUnset {
placeholderTextColor = i .placeholderTextColorFocused
}
Print (screen , EscapeBytes (i .placeholder ), x , y , fieldWidth , AlignLeft , placeholderTextColor )
i .offset = 0
} else {
if i .maskCharacter > 0 {
text = bytes .Repeat ([]byte (string (i .maskCharacter )), utf8 .RuneCount (i .text ))
}
var drawnText []byte
if fieldWidth > runewidth .StringWidth (string (text )) {
drawnText = EscapeBytes (text )
Print (screen , drawnText , x , y , fieldWidth , AlignLeft , fieldTextColor )
i .offset = 0
iterateString (string (text ), func (main rune , comb []rune , textPos , textWidth , screenPos , screenWidth int ) bool {
if textPos >= i .cursorPos {
return true
}
cursorScreenPos += screenWidth
return false
})
} else {
if i .cursorPos < 0 {
i .cursorPos = 0
} else if i .cursorPos > len (text ) {
i .cursorPos = len (text )
}
var shiftLeft int
if i .offset > i .cursorPos {
i .offset = i .cursorPos
} else if subWidth := runewidth .StringWidth (string (text [i .offset :i .cursorPos ])); subWidth > fieldWidth -1 {
shiftLeft = subWidth - fieldWidth + 1
}
currentOffset := i .offset
iterateString (string (text ), func (main rune , comb []rune , textPos , textWidth , screenPos , screenWidth int ) bool {
if textPos >= currentOffset {
if shiftLeft > 0 {
i .offset = textPos + textWidth
shiftLeft -= screenWidth
} else {
if textPos +textWidth > i .cursorPos {
return true
}
cursorScreenPos += screenWidth
}
}
return false
})
drawnText = EscapeBytes (text [i .offset :])
Print (screen , drawnText , x , y , fieldWidth , AlignLeft , fieldTextColor )
}
if i .maskCharacter == 0 && len (i .autocompleteListSuggestion ) > 0 {
Print (screen , i .autocompleteListSuggestion , x +runewidth .StringWidth (string (drawnText )), y , fieldWidth -runewidth .StringWidth (string (drawnText )), AlignLeft , i .autocompleteSuggestionTextColor )
}
}
if len (i .fieldNote ) > 0 {
Print (screen , i .fieldNote , x , y +1 , fieldWidth , AlignLeft , i .fieldNoteTextColor )
}
if i .autocompleteList != nil {
lheight := i .autocompleteList .GetItemCount ()
lwidth := 0
for index := 0 ; index < lheight ; index ++ {
entry , _ := i .autocompleteList .GetItemText (index )
width := TaggedStringWidth (entry )
if width > lwidth {
lwidth = width
}
}
lx := x
ly := y + 1
_ , sheight := screen .Size ()
if ly +lheight >= sheight && ly -2 > lheight -ly {
ly = y - lheight
if ly < 0 {
ly = 0
}
}
if ly +lheight >= sheight {
lheight = sheight - ly
}
if i .autocompleteList .scrollBarVisibility == ScrollBarAlways || (i .autocompleteList .scrollBarVisibility == ScrollBarAuto && i .autocompleteList .GetItemCount () > lheight ) {
lwidth ++
}
i .autocompleteList .SetRect (lx , ly , lwidth , lheight )
i .autocompleteList .Draw (screen )
}
if i .focus .HasFocus () {
screen .ShowCursor (x +cursorScreenPos , y )
}
}
func (i *InputField ) InputHandler () func (event *tcell .EventKey , setFocus func (p Primitive )) {
return i .WrapInputHandler (func (event *tcell .EventKey , setFocus func (p Primitive )) {
i .Lock ()
currentText := i .text
defer func () {
i .Lock ()
newText := i .text
i .Unlock ()
if !bytes .Equal (newText , currentText ) {
i .Autocomplete ()
if i .changed != nil {
i .changed (string (i .text ))
}
}
}()
home := func () { i .cursorPos = 0 }
end := func () { i .cursorPos = len (i .text ) }
moveLeft := func () {
iterateStringReverse (string (i .text [:i .cursorPos ]), func (main rune , comb []rune , textPos , textWidth , screenPos , screenWidth int ) bool {
i .cursorPos -= textWidth
return true
})
}
moveRight := func () {
iterateString (string (i .text [i .cursorPos :]), func (main rune , comb []rune , textPos , textWidth , screenPos , screenWidth int ) bool {
i .cursorPos += textWidth
return true
})
}
moveWordLeft := func () {
i .cursorPos = len (regexRightWord .ReplaceAll (i .text [:i .cursorPos ], nil ))
}
moveWordRight := func () {
i .cursorPos = len (i .text ) - len (regexLeftWord .ReplaceAll (i .text [i .cursorPos :], nil ))
}
add := func (r rune ) bool {
newText := append (append (i .text [:i .cursorPos ], []byte (string (r ))...), i .text [i .cursorPos :]...)
if i .accept != nil && !i .accept (string (newText ), r ) {
return false
}
i .text = newText
i .cursorPos += len (string (r ))
return true
}
finish := func (key tcell .Key ) {
if i .done != nil {
i .done (key )
}
if i .finished != nil {
i .finished (key )
}
}
switch key := event .Key (); key {
case tcell .KeyRune :
if event .Modifiers ()&tcell .ModAlt > 0 {
switch event .Rune () {
case 'a' :
home ()
case 'e' :
end ()
case 'b' :
moveWordLeft ()
case 'f' :
moveWordRight ()
default :
if !add (event .Rune ()) {
i .Unlock ()
return
}
}
} else {
if !add (event .Rune ()) {
i .Unlock ()
return
}
}
case tcell .KeyCtrlU :
i .text = nil
i .cursorPos = 0
case tcell .KeyCtrlK :
i .text = i .text [:i .cursorPos ]
case tcell .KeyCtrlW :
newText := append (regexRightWord .ReplaceAll (i .text [:i .cursorPos ], nil ), i .text [i .cursorPos :]...)
i .cursorPos -= len (i .text ) - len (newText )
i .text = newText
case tcell .KeyBackspace , tcell .KeyBackspace2 :
iterateStringReverse (string (i .text [:i .cursorPos ]), func (main rune , comb []rune , textPos , textWidth , screenPos , screenWidth int ) bool {
i .text = append (i .text [:textPos ], i .text [textPos +textWidth :]...)
i .cursorPos -= textWidth
return true
})
if i .offset >= i .cursorPos {
i .offset = 0
}
case tcell .KeyDelete :
iterateString (string (i .text [i .cursorPos :]), func (main rune , comb []rune , textPos , textWidth , screenPos , screenWidth int ) bool {
i .text = append (i .text [:i .cursorPos ], i .text [i .cursorPos +textWidth :]...)
return true
})
case tcell .KeyLeft :
if event .Modifiers ()&tcell .ModAlt > 0 {
moveWordLeft ()
} else {
moveLeft ()
}
case tcell .KeyRight :
if event .Modifiers ()&tcell .ModAlt > 0 {
moveWordRight ()
} else {
moveRight ()
}
case tcell .KeyHome , tcell .KeyCtrlA :
home ()
case tcell .KeyEnd , tcell .KeyCtrlE :
end ()
case tcell .KeyEnter :
if i .autocompleteList != nil {
currentItem := i .autocompleteList .GetCurrentItem ()
selectionText := currentItem .GetMainText ()
if currentItem .GetSecondaryText () != "" {
selectionText = currentItem .GetSecondaryText ()
}
i .Unlock ()
i .SetText (selectionText )
i .Lock ()
i .autocompleteList = nil
i .autocompleteListSuggestion = nil
i .Unlock ()
} else {
i .Unlock ()
finish (key )
}
return
case tcell .KeyEscape :
if i .autocompleteList != nil {
i .autocompleteList = nil
i .autocompleteListSuggestion = nil
i .Unlock ()
} else {
i .Unlock ()
finish (key )
}
return
case tcell .KeyDown , tcell .KeyTab :
if i .autocompleteList != nil {
count := i .autocompleteList .GetItemCount ()
newEntry := i .autocompleteList .GetCurrentItemIndex () + 1
if newEntry >= count {
newEntry = 0
}
i .autocompleteList .SetCurrentItem (newEntry )
i .Unlock ()
} else {
i .Unlock ()
finish (key )
}
return
case tcell .KeyUp , tcell .KeyBacktab :
if i .autocompleteList != nil {
newEntry := i .autocompleteList .GetCurrentItemIndex () - 1
if newEntry < 0 {
newEntry = i .autocompleteList .GetItemCount () - 1
}
i .autocompleteList .SetCurrentItem (newEntry )
i .Unlock ()
} else {
i .Unlock ()
finish (key )
}
return
}
i .Unlock ()
})
}
func (i *InputField ) MouseHandler () func (action MouseAction , event *tcell .EventMouse , setFocus func (p Primitive )) (consumed bool , capture Primitive ) {
return i .WrapMouseHandler (func (action MouseAction , event *tcell .EventMouse , setFocus func (p Primitive )) (consumed bool , capture Primitive ) {
x , y := event .Position ()
_ , rectY , _ , _ := i .GetInnerRect ()
if !i .InRect (x , y ) {
return false , nil
}
if action == MouseLeftClick && y == rectY {
if x >= i .fieldX {
if !iterateString (string (i .text ), func (main rune , comb []rune , textPos int , textWidth int , screenPos int , screenWidth int ) bool {
if x -i .fieldX < screenPos +screenWidth {
i .cursorPos = textPos
return true
}
return false
}) {
i .cursorPos = len (i .text )
}
}
setFocus (i )
consumed = true
}
return
})
}
var (
regexRightWord = regexp .MustCompile (`(\w*|\W)$` )
regexLeftWord = regexp .MustCompile (`^(\W|\w*)` )
)
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .