2019-12-30 23:12:17 +00:00
|
|
|
package cview
|
2017-12-15 14:29:21 +00:00
|
|
|
|
|
|
|
import (
|
2020-10-07 01:43:34 +00:00
|
|
|
"bytes"
|
2017-12-15 14:29:21 +00:00
|
|
|
"math"
|
2017-12-15 22:03:01 +00:00
|
|
|
"regexp"
|
2019-07-08 08:34:06 +00:00
|
|
|
"sync"
|
2018-01-11 15:13:01 +00:00
|
|
|
"unicode/utf8"
|
2017-12-15 14:29:21 +00:00
|
|
|
|
2020-08-30 15:36:03 +00:00
|
|
|
"github.com/gdamore/tcell/v2"
|
2020-10-06 20:11:45 +00:00
|
|
|
"github.com/mattn/go-runewidth"
|
2017-12-15 14:29:21 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// InputField is a one-line box (three lines if there is a title) where the
|
2018-10-18 06:34:08 +00:00
|
|
|
// user can enter text. Use SetAcceptanceFunc() to accept or reject input,
|
|
|
|
// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
|
|
|
|
// from onlookers (e.g. for password input).
|
2018-01-07 15:39:06 +00:00
|
|
|
//
|
2018-10-18 06:34:08 +00:00
|
|
|
// The following keys can be used for navigation and editing:
|
|
|
|
//
|
|
|
|
// - Left arrow: Move left by one character.
|
|
|
|
// - Right arrow: Move right by one character.
|
|
|
|
// - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
|
|
|
|
// - End, Ctrl-E, Alt-e: Move to the end of the line.
|
|
|
|
// - Alt-left, Alt-b: Move left by one word.
|
|
|
|
// - Alt-right, Alt-f: Move right by one word.
|
|
|
|
// - Backspace: Delete the character before the cursor.
|
|
|
|
// - Delete: Delete the character after the cursor.
|
|
|
|
// - Ctrl-K: Delete from the cursor to the end of the line.
|
|
|
|
// - Ctrl-W: Delete the last word before the cursor.
|
|
|
|
// - Ctrl-U: Delete the entire line.
|
2017-12-15 14:29:21 +00:00
|
|
|
type InputField struct {
|
2017-12-16 21:48:26 +00:00
|
|
|
*Box
|
2017-12-15 14:29:21 +00:00
|
|
|
|
|
|
|
// The text that was entered.
|
2020-10-07 01:43:34 +00:00
|
|
|
text []byte
|
2017-12-15 14:29:21 +00:00
|
|
|
|
|
|
|
// The text to be displayed before the input area.
|
2020-10-07 01:43:34 +00:00
|
|
|
label []byte
|
2017-12-15 14:29:21 +00:00
|
|
|
|
2018-03-15 16:14:14 +00:00
|
|
|
// The text to be displayed in the input area when "text" is empty.
|
2020-10-07 01:43:34 +00:00
|
|
|
placeholder []byte
|
2018-03-15 16:14:14 +00:00
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// The label color.
|
|
|
|
labelColor tcell.Color
|
|
|
|
|
2020-09-16 14:29:12 +00:00
|
|
|
// The label color when focused.
|
|
|
|
labelColorFocused tcell.Color
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// The background color of the input area.
|
|
|
|
fieldBackgroundColor tcell.Color
|
|
|
|
|
2020-09-16 14:29:12 +00:00
|
|
|
// The background color of the input area when focused.
|
|
|
|
fieldBackgroundColorFocused tcell.Color
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// The text color of the input area.
|
|
|
|
fieldTextColor tcell.Color
|
|
|
|
|
2020-09-16 14:29:12 +00:00
|
|
|
// The text color of the input area when focused.
|
|
|
|
fieldTextColorFocused tcell.Color
|
|
|
|
|
2018-03-15 16:14:14 +00:00
|
|
|
// The text color of the placeholder.
|
|
|
|
placeholderTextColor tcell.Color
|
|
|
|
|
2020-09-16 16:54:54 +00:00
|
|
|
// The text color of the placeholder when focused.
|
|
|
|
placeholderTextColorFocused tcell.Color
|
|
|
|
|
|
|
|
// The text color of the list items.
|
|
|
|
autocompleteListTextColor tcell.Color
|
|
|
|
|
|
|
|
// The background color of the autocomplete list.
|
|
|
|
autocompleteListBackgroundColor tcell.Color
|
|
|
|
|
|
|
|
// The text color of the selected ListItem.
|
|
|
|
autocompleteListSelectedTextColor tcell.Color
|
|
|
|
|
|
|
|
// The background color of the selected ListItem.
|
|
|
|
autocompleteListSelectedBackgroundColor tcell.Color
|
|
|
|
|
2020-09-16 18:33:02 +00:00
|
|
|
// The text color of the suggestion.
|
|
|
|
autocompleteSuggestionTextColor tcell.Color
|
|
|
|
|
2020-09-17 15:14:01 +00:00
|
|
|
// The text color of the note below the input field.
|
|
|
|
fieldNoteTextColor tcell.Color
|
|
|
|
|
|
|
|
// The note to show below the input field.
|
2020-10-07 01:43:34 +00:00
|
|
|
fieldNote []byte
|
2020-09-17 15:14:01 +00:00
|
|
|
|
2018-04-09 19:07:03 +00:00
|
|
|
// The screen width of the label area. A value of 0 means use the width of
|
|
|
|
// the label text.
|
|
|
|
labelWidth int
|
|
|
|
|
2018-01-17 20:17:59 +00:00
|
|
|
// The screen width of the input area. A value of 0 means extend as much as
|
2017-12-15 14:29:21 +00:00
|
|
|
// possible.
|
2018-01-17 20:17:59 +00:00
|
|
|
fieldWidth int
|
2017-12-15 14:29:21 +00:00
|
|
|
|
2018-01-11 15:13:01 +00:00
|
|
|
// A character to mask entered text (useful for password fields). A value of 0
|
|
|
|
// disables masking.
|
|
|
|
maskCharacter rune
|
|
|
|
|
2018-10-18 06:34:08 +00:00
|
|
|
// The cursor position as a byte index into the text string.
|
|
|
|
cursorPos int
|
|
|
|
|
2019-07-08 08:34:06 +00:00
|
|
|
// An optional autocomplete function which receives the current text of the
|
2020-09-16 17:55:33 +00:00
|
|
|
// input field and returns a slice of ListItems to be displayed in a drop-down
|
2020-09-30 19:14:51 +00:00
|
|
|
// selection. Items' main text is displayed in the autocomplete list. When
|
|
|
|
// set, items' secondary text is used as the selection value. Otherwise,
|
|
|
|
// the main text is used.
|
2020-09-16 17:55:33 +00:00
|
|
|
autocomplete func(text string) []*ListItem
|
2019-07-08 08:34:06 +00:00
|
|
|
|
|
|
|
// The List object which shows the selectable autocomplete entries. If not
|
|
|
|
// nil, the list's main texts represent the current autocomplete entries.
|
2020-03-25 14:32:57 +00:00
|
|
|
autocompleteList *List
|
2019-07-08 08:34:06 +00:00
|
|
|
|
2020-09-16 18:33:02 +00:00
|
|
|
// The suggested completion of the current autocomplete ListItem.
|
2020-10-07 01:43:34 +00:00
|
|
|
autocompleteListSuggestion []byte
|
2020-09-16 18:33:02 +00:00
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// An optional function which may reject the last character that was entered.
|
|
|
|
accept func(text string, ch rune) bool
|
|
|
|
|
2018-01-10 09:40:51 +00:00
|
|
|
// An optional function which is called when the input has changed.
|
|
|
|
changed func(text string)
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// An optional function which is called when the user indicated that they
|
2017-12-15 22:03:01 +00:00
|
|
|
// are done entering text. The key which was pressed is provided (tab,
|
|
|
|
// shift-tab, enter, or escape).
|
2017-12-15 14:29:21 +00:00
|
|
|
done func(tcell.Key)
|
2018-04-19 19:34:03 +00:00
|
|
|
|
|
|
|
// A callback function set by the Form class and called when the user leaves
|
|
|
|
// this form item.
|
|
|
|
finished func(tcell.Key)
|
2020-03-25 14:32:57 +00:00
|
|
|
|
2020-03-29 19:43:46 +00:00
|
|
|
// The x-coordinate of the input field as determined during the last call to Draw().
|
|
|
|
fieldX int
|
|
|
|
|
|
|
|
// The number of bytes of the text string skipped ahead while drawing.
|
|
|
|
offset int
|
|
|
|
|
2020-03-25 14:32:57 +00:00
|
|
|
sync.RWMutex
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewInputField returns a new input field.
|
|
|
|
func NewInputField() *InputField {
|
|
|
|
return &InputField{
|
2020-09-16 16:54:54 +00:00
|
|
|
Box: NewBox(),
|
|
|
|
labelColor: Styles.SecondaryTextColor,
|
|
|
|
fieldBackgroundColor: Styles.ContrastBackgroundColor,
|
|
|
|
fieldTextColor: Styles.PrimaryTextColor,
|
|
|
|
placeholderTextColor: Styles.ContrastSecondaryTextColor,
|
|
|
|
autocompleteListTextColor: Styles.PrimitiveBackgroundColor,
|
|
|
|
autocompleteListBackgroundColor: Styles.MoreContrastBackgroundColor,
|
|
|
|
autocompleteListSelectedTextColor: Styles.PrimitiveBackgroundColor,
|
|
|
|
autocompleteListSelectedBackgroundColor: Styles.PrimaryTextColor,
|
2020-09-16 18:33:02 +00:00
|
|
|
autocompleteSuggestionTextColor: Styles.ContrastPrimaryTextColor,
|
2020-09-17 15:14:01 +00:00
|
|
|
fieldNoteTextColor: Styles.SecondaryTextColor,
|
2020-09-29 20:21:53 +00:00
|
|
|
labelColorFocused: ColorUnset,
|
|
|
|
fieldBackgroundColorFocused: ColorUnset,
|
|
|
|
fieldTextColorFocused: ColorUnset,
|
|
|
|
placeholderTextColorFocused: ColorUnset,
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetText sets the current text of the input field.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetText(text string) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
|
2020-10-07 01:43:34 +00:00
|
|
|
i.text = []byte(text)
|
2018-11-10 08:17:39 +00:00
|
|
|
i.cursorPos = len(text)
|
2018-01-10 09:40:51 +00:00
|
|
|
if i.changed != nil {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Unlock()
|
2018-01-10 09:40:51 +00:00
|
|
|
i.changed(text)
|
2020-03-25 14:32:57 +00:00
|
|
|
} else {
|
|
|
|
i.Unlock()
|
2018-01-10 09:40:51 +00:00
|
|
|
}
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetText returns the current text of the input field.
|
|
|
|
func (i *InputField) GetText() string {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.RLock()
|
|
|
|
defer i.RUnlock()
|
|
|
|
|
2020-10-07 01:43:34 +00:00
|
|
|
return string(i.text)
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetLabel sets the text to be displayed before the input area.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetLabel(label string) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2020-10-07 01:43:34 +00:00
|
|
|
i.label = []byte(label)
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetLabel returns the text to be displayed before the input area.
|
|
|
|
func (i *InputField) GetLabel() string {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.RLock()
|
|
|
|
defer i.RUnlock()
|
|
|
|
|
2020-10-07 01:43:34 +00:00
|
|
|
return string(i.label)
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
|
2018-04-09 19:07:03 +00:00
|
|
|
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
|
|
|
|
// primitive to use the width of the label string.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetLabelWidth(width int) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2018-04-09 19:07:03 +00:00
|
|
|
i.labelWidth = width
|
|
|
|
}
|
|
|
|
|
2018-03-15 16:14:14 +00:00
|
|
|
// SetPlaceholder sets the text to be displayed when the input text is empty.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetPlaceholder(text string) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2020-10-07 01:43:34 +00:00
|
|
|
i.placeholder = []byte(text)
|
2018-03-15 16:14:14 +00:00
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// SetLabelColor sets the color of the label.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetLabelColor(color tcell.Color) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
i.labelColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-16 14:29:12 +00:00
|
|
|
// SetLabelColorFocused sets the color of the label when focused.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetLabelColorFocused(color tcell.Color) {
|
2020-09-16 14:29:12 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
i.labelColorFocused = color
|
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// SetFieldBackgroundColor sets the background color of the input area.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetFieldBackgroundColor(color tcell.Color) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
i.fieldBackgroundColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-23 23:14:14 +00:00
|
|
|
// SetFieldBackgroundColorFocused sets the background color of the input area
|
|
|
|
// when focused.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetFieldBackgroundColorFocused(color tcell.Color) {
|
2020-09-16 14:29:12 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
i.fieldBackgroundColorFocused = color
|
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// SetFieldTextColor sets the text color of the input area.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetFieldTextColor(color tcell.Color) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
i.fieldTextColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-16 14:29:12 +00:00
|
|
|
// SetFieldTextColorFocused sets the text color of the input area when focused.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetFieldTextColorFocused(color tcell.Color) {
|
2020-09-16 14:29:12 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
i.fieldTextColorFocused = color
|
|
|
|
}
|
|
|
|
|
2018-04-09 19:07:03 +00:00
|
|
|
// SetPlaceholderTextColor sets the text color of placeholder text.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetPlaceholderTextColor(color tcell.Color) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2018-03-15 16:14:14 +00:00
|
|
|
i.placeholderTextColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-23 23:14:14 +00:00
|
|
|
// SetPlaceholderTextColorFocused sets the text color of placeholder text when
|
|
|
|
// focused.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetPlaceholderTextColorFocused(color tcell.Color) {
|
2020-10-02 18:23:49 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2020-09-16 16:54:54 +00:00
|
|
|
i.placeholderTextColorFocused = color
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetAutocompleteListTextColor sets the text color of the ListItems.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetAutocompleteListTextColor(color tcell.Color) {
|
2020-09-16 16:54:54 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
i.autocompleteListTextColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-23 23:14:14 +00:00
|
|
|
// SetAutocompleteListBackgroundColor sets the background color of the
|
|
|
|
// autocomplete list.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetAutocompleteListBackgroundColor(color tcell.Color) {
|
2020-09-16 16:54:54 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
i.autocompleteListBackgroundColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-23 23:14:14 +00:00
|
|
|
// SetAutocompleteListSelectedTextColor sets the text color of the selected
|
|
|
|
// ListItem.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetAutocompleteListSelectedTextColor(color tcell.Color) {
|
2020-09-16 16:54:54 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
i.autocompleteListSelectedTextColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-23 23:14:14 +00:00
|
|
|
// SetAutocompleteListSelectedBackgroundColor sets the background of the
|
|
|
|
// selected ListItem.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetAutocompleteListSelectedBackgroundColor(color tcell.Color) {
|
2020-09-16 16:54:54 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
i.autocompleteListSelectedBackgroundColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-23 23:14:14 +00:00
|
|
|
// SetAutocompleteSuggestionTextColor sets the text color of the autocomplete
|
|
|
|
// suggestion in the input field.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetAutocompleteSuggestionTextColor(color tcell.Color) {
|
2020-10-02 18:23:49 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2020-09-16 18:33:02 +00:00
|
|
|
i.autocompleteSuggestionTextColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-17 15:14:01 +00:00
|
|
|
// SetFieldNoteTextColor sets the text color of the note.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetFieldNoteTextColor(color tcell.Color) {
|
2020-09-17 15:14:01 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
2020-10-02 18:23:49 +00:00
|
|
|
|
2020-09-17 15:14:01 +00:00
|
|
|
i.fieldNoteTextColor = color
|
|
|
|
}
|
|
|
|
|
2020-09-23 23:14:14 +00:00
|
|
|
// SetFieldNote sets the text to show below the input field, e.g. when the
|
|
|
|
// input is invalid.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetFieldNote(note string) {
|
2020-09-17 15:14:01 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
2020-10-02 18:23:49 +00:00
|
|
|
|
2020-10-07 01:43:34 +00:00
|
|
|
i.fieldNote = []byte(note)
|
2020-09-17 15:14:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ResetFieldNote sets the note to an empty string.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) ResetFieldNote() {
|
2020-09-17 15:14:01 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
2020-10-02 18:23:49 +00:00
|
|
|
|
2020-10-07 01:43:34 +00:00
|
|
|
i.fieldNote = nil
|
2020-09-17 15:14:01 +00:00
|
|
|
}
|
|
|
|
|
2018-01-17 20:17:59 +00:00
|
|
|
// SetFieldWidth sets the screen width of the input area. A value of 0 means
|
|
|
|
// extend as much as possible.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetFieldWidth(width int) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2018-01-17 20:17:59 +00:00
|
|
|
i.fieldWidth = width
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
|
2018-01-17 20:17:59 +00:00
|
|
|
// GetFieldWidth returns this primitive's field width.
|
|
|
|
func (i *InputField) GetFieldWidth() int {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.RLock()
|
|
|
|
defer i.RUnlock()
|
|
|
|
|
2018-01-17 20:17:59 +00:00
|
|
|
return i.fieldWidth
|
2018-01-16 19:45:54 +00:00
|
|
|
}
|
|
|
|
|
2020-09-17 15:14:01 +00:00
|
|
|
// GetFieldHeight returns the height of the field.
|
|
|
|
func (i *InputField) GetFieldHeight() int {
|
|
|
|
i.RLock()
|
|
|
|
defer i.RUnlock()
|
2020-10-07 01:43:34 +00:00
|
|
|
if len(i.fieldNote) == 0 {
|
2020-09-17 15:14:01 +00:00
|
|
|
return 1
|
|
|
|
}
|
|
|
|
return 2
|
|
|
|
}
|
|
|
|
|
2020-08-25 02:31:48 +00:00
|
|
|
// GetCursorPosition returns the cursor position.
|
|
|
|
func (i *InputField) GetCursorPosition() int {
|
|
|
|
i.RLock()
|
|
|
|
defer i.RUnlock()
|
|
|
|
|
|
|
|
return i.cursorPos
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetCursorPosition sets the cursor position.
|
|
|
|
func (i *InputField) SetCursorPosition(cursorPos int) {
|
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
i.cursorPos = cursorPos
|
|
|
|
}
|
|
|
|
|
2018-01-11 15:13:01 +00:00
|
|
|
// SetMaskCharacter sets a character that masks user input on a screen. A value
|
|
|
|
// of 0 disables masking.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetMaskCharacter(mask rune) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2018-01-11 15:13:01 +00:00
|
|
|
i.maskCharacter = mask
|
|
|
|
}
|
|
|
|
|
2019-07-08 08:34:06 +00:00
|
|
|
// SetAutocompleteFunc sets an autocomplete callback function which may return
|
2020-09-16 17:55:33 +00:00
|
|
|
// ListItems to be selected from a drop-down based on the current text of the
|
2019-07-08 08:34:06 +00:00
|
|
|
// input field. The drop-down appears only if len(entries) > 0. The callback is
|
|
|
|
// invoked in this function and whenever the current text changes or when
|
|
|
|
// Autocomplete() is called. Entries are cleared when the user selects an entry
|
|
|
|
// or presses Escape.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []*ListItem)) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
2019-07-08 08:34:06 +00:00
|
|
|
i.autocomplete = callback
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Unlock()
|
|
|
|
|
2019-07-08 08:34:06 +00:00
|
|
|
i.Autocomplete()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Autocomplete invokes the autocomplete callback (if there is one). If the
|
|
|
|
// length of the returned autocomplete entries slice is greater than 0, the
|
|
|
|
// input field will present the user with a corresponding drop-down list the
|
|
|
|
// next time the input field is drawn.
|
|
|
|
//
|
|
|
|
// It is safe to call this function from any goroutine. Note that the input
|
|
|
|
// field is not redrawn automatically unless called from the main goroutine
|
|
|
|
// (e.g. in response to events).
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) Autocomplete() {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
2019-07-08 08:34:06 +00:00
|
|
|
if i.autocomplete == nil {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Unlock()
|
2020-10-07 23:34:21 +00:00
|
|
|
return
|
2019-07-08 08:34:06 +00:00
|
|
|
}
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Unlock()
|
2019-07-08 08:34:06 +00:00
|
|
|
|
|
|
|
// Do we have any autocomplete entries?
|
2020-10-07 01:43:34 +00:00
|
|
|
entries := i.autocomplete(string(i.text))
|
2019-07-08 08:34:06 +00:00
|
|
|
if len(entries) == 0 {
|
|
|
|
// No entries, no list.
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
2019-07-08 08:34:06 +00:00
|
|
|
i.autocompleteList = nil
|
2020-10-07 01:43:34 +00:00
|
|
|
i.autocompleteListSuggestion = nil
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Unlock()
|
2020-10-07 23:34:21 +00:00
|
|
|
return
|
2019-07-08 08:34:06 +00:00
|
|
|
}
|
|
|
|
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
|
2019-07-08 08:34:06 +00:00
|
|
|
// Make a list if we have none.
|
|
|
|
if i.autocompleteList == nil {
|
2020-10-07 23:34:21 +00:00
|
|
|
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
|
2019-07-08 08:34:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fill it with the entries.
|
|
|
|
currentEntry := -1
|
|
|
|
i.autocompleteList.Clear()
|
|
|
|
for index, entry := range entries {
|
2020-09-16 17:55:33 +00:00
|
|
|
i.autocompleteList.AddItem(entry)
|
2020-10-07 01:43:34 +00:00
|
|
|
if currentEntry < 0 && entry.GetMainText() == string(i.text) {
|
2019-07-08 08:34:06 +00:00
|
|
|
currentEntry = index
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the selection if we have one.
|
|
|
|
if currentEntry >= 0 {
|
|
|
|
i.autocompleteList.SetCurrentItem(currentEntry)
|
|
|
|
}
|
|
|
|
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Unlock()
|
2019-07-08 08:34:06 +00:00
|
|
|
}
|
|
|
|
|
2020-09-16 18:33:02 +00:00
|
|
|
// autocompleteChanged gets called when another item in the
|
|
|
|
// autocomplete list has been selected.
|
|
|
|
func (i *InputField) autocompleteChanged(_ int, item *ListItem) {
|
2020-10-07 01:43:34 +00:00
|
|
|
mainText := item.GetMainBytes()
|
|
|
|
secondaryText := item.GetSecondaryBytes()
|
|
|
|
if len(i.text) < len(secondaryText) {
|
2020-10-07 01:19:40 +00:00
|
|
|
i.autocompleteListSuggestion = secondaryText[len(i.text):]
|
2020-10-07 01:43:34 +00:00
|
|
|
} else if len(i.text) < len(mainText) {
|
|
|
|
i.autocompleteListSuggestion = mainText[len(i.text):]
|
2020-10-01 17:58:30 +00:00
|
|
|
} else {
|
2020-10-07 01:43:34 +00:00
|
|
|
i.autocompleteListSuggestion = nil
|
2020-09-16 18:33:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// SetAcceptanceFunc sets a handler which may reject the last character that was
|
|
|
|
// entered (by returning false).
|
|
|
|
//
|
2018-10-18 06:34:08 +00:00
|
|
|
// This package defines a number of variables prefixed with InputField which may
|
2017-12-15 14:29:21 +00:00
|
|
|
// be used for common input (e.g. numbers, maximum text length).
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
i.accept = handler
|
|
|
|
}
|
|
|
|
|
2018-01-10 09:40:51 +00:00
|
|
|
// SetChangedFunc sets a handler which is called whenever the text of the input
|
|
|
|
// field has changed. It receives the current text (after the change).
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetChangedFunc(handler func(text string)) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2018-01-10 09:40:51 +00:00
|
|
|
i.changed = handler
|
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// SetDoneFunc sets a handler which is called when the user is done entering
|
|
|
|
// text. The callback function is provided with the key that was pressed, which
|
|
|
|
// is one of the following:
|
|
|
|
//
|
|
|
|
// - KeyEnter: Done entering text.
|
|
|
|
// - KeyEscape: Abort text input.
|
|
|
|
// - KeyTab: Move to the next field.
|
2017-12-15 22:03:01 +00:00
|
|
|
// - KeyBacktab: Move to the previous field.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
i.done = handler
|
|
|
|
}
|
|
|
|
|
2018-04-19 19:34:03 +00:00
|
|
|
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
|
2020-10-07 23:34:21 +00:00
|
|
|
func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2018-04-19 19:34:03 +00:00
|
|
|
i.finished = handler
|
2017-12-18 19:04:52 +00:00
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// Draw draws this primitive onto the screen.
|
|
|
|
func (i *InputField) Draw(screen tcell.Screen) {
|
2020-10-20 02:05:03 +00:00
|
|
|
if !i.GetVisible() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
i.Box.Draw(screen)
|
|
|
|
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2020-09-16 14:29:12 +00:00
|
|
|
// Select colors
|
|
|
|
labelColor := i.labelColor
|
|
|
|
fieldBackgroundColor := i.fieldBackgroundColor
|
|
|
|
fieldTextColor := i.fieldTextColor
|
|
|
|
if i.GetFocusable().HasFocus() {
|
2020-09-29 20:21:53 +00:00
|
|
|
if i.labelColorFocused != ColorUnset {
|
|
|
|
labelColor = i.labelColorFocused
|
|
|
|
}
|
|
|
|
if i.fieldBackgroundColorFocused != ColorUnset {
|
|
|
|
fieldBackgroundColor = i.fieldBackgroundColorFocused
|
|
|
|
}
|
|
|
|
if i.fieldTextColorFocused != ColorUnset {
|
|
|
|
fieldTextColor = i.fieldTextColorFocused
|
|
|
|
}
|
2020-09-16 14:29:12 +00:00
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// Prepare
|
2017-12-21 17:08:53 +00:00
|
|
|
x, y, width, height := i.GetInnerRect()
|
|
|
|
rightLimit := x + width
|
2017-12-15 14:29:21 +00:00
|
|
|
if height < 1 || rightLimit <= x {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw label.
|
2018-04-09 19:07:03 +00:00
|
|
|
if i.labelWidth > 0 {
|
|
|
|
labelWidth := i.labelWidth
|
|
|
|
if labelWidth > rightLimit-x {
|
|
|
|
labelWidth = rightLimit - x
|
|
|
|
}
|
2020-10-07 01:43:34 +00:00
|
|
|
Print(screen, i.label, x, y, labelWidth, AlignLeft, labelColor)
|
2018-04-09 19:07:03 +00:00
|
|
|
x += labelWidth
|
|
|
|
} else {
|
2020-10-07 01:43:34 +00:00
|
|
|
_, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, labelColor)
|
2018-04-09 19:07:03 +00:00
|
|
|
x += drawnWidth
|
|
|
|
}
|
2017-12-15 14:29:21 +00:00
|
|
|
|
|
|
|
// Draw input area.
|
2020-03-29 19:43:46 +00:00
|
|
|
i.fieldX = x
|
2018-01-17 20:17:59 +00:00
|
|
|
fieldWidth := i.fieldWidth
|
2018-01-17 16:13:36 +00:00
|
|
|
if fieldWidth == 0 {
|
|
|
|
fieldWidth = math.MaxInt32
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
2018-01-17 16:13:36 +00:00
|
|
|
if rightLimit-x < fieldWidth {
|
|
|
|
fieldWidth = rightLimit - x
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
2020-09-16 14:29:12 +00:00
|
|
|
fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
|
2018-01-17 16:13:36 +00:00
|
|
|
for index := 0; index < fieldWidth; index++ {
|
2017-12-15 22:03:01 +00:00
|
|
|
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
2017-12-15 22:03:01 +00:00
|
|
|
|
2018-10-18 06:34:08 +00:00
|
|
|
// Text.
|
|
|
|
var cursorScreenPos int
|
2018-01-11 15:13:01 +00:00
|
|
|
text := i.text
|
2020-10-07 01:43:34 +00:00
|
|
|
if len(text) == 0 && len(i.placeholder) > 0 {
|
2018-10-18 06:34:08 +00:00
|
|
|
// Draw placeholder text.
|
2020-09-29 20:21:53 +00:00
|
|
|
placeholderTextColor := i.placeholderTextColor
|
|
|
|
if i.GetFocusable().HasFocus() && i.placeholderTextColorFocused != ColorUnset {
|
|
|
|
placeholderTextColor = i.placeholderTextColorFocused
|
|
|
|
}
|
2020-10-07 01:43:34 +00:00
|
|
|
Print(screen, EscapeBytes(i.placeholder), x, y, fieldWidth, AlignLeft, placeholderTextColor)
|
2018-10-18 06:34:08 +00:00
|
|
|
i.offset = 0
|
2017-12-15 22:03:01 +00:00
|
|
|
} else {
|
2018-05-03 06:05:11 +00:00
|
|
|
// Draw entered text.
|
|
|
|
if i.maskCharacter > 0 {
|
2020-10-07 01:43:34 +00:00
|
|
|
text = bytes.Repeat([]byte(string(i.maskCharacter)), utf8.RuneCount(i.text))
|
2018-05-03 06:05:11 +00:00
|
|
|
}
|
2020-10-07 01:43:34 +00:00
|
|
|
var drawnText []byte
|
|
|
|
if fieldWidth >= runewidth.StringWidth(string(text)) {
|
2018-10-18 06:34:08 +00:00
|
|
|
// We have enough space for the full text.
|
2020-10-07 01:43:34 +00:00
|
|
|
drawnText = EscapeBytes(text)
|
|
|
|
Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor)
|
2018-10-18 06:34:08 +00:00
|
|
|
i.offset = 0
|
2020-10-07 01:43:34 +00:00
|
|
|
iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
2018-10-18 06:34:08 +00:00
|
|
|
if textPos >= i.cursorPos {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
cursorScreenPos += screenWidth
|
|
|
|
return false
|
|
|
|
})
|
2018-05-03 06:05:11 +00:00
|
|
|
} else {
|
2018-10-18 06:34:08 +00:00
|
|
|
// The text doesn't fit. Where is the cursor?
|
|
|
|
if i.cursorPos < 0 {
|
|
|
|
i.cursorPos = 0
|
|
|
|
} else if i.cursorPos > len(text) {
|
|
|
|
i.cursorPos = len(text)
|
|
|
|
}
|
|
|
|
// Shift the text so the cursor is inside the field.
|
|
|
|
var shiftLeft int
|
|
|
|
if i.offset > i.cursorPos {
|
|
|
|
i.offset = i.cursorPos
|
2020-10-07 01:43:34 +00:00
|
|
|
} else if subWidth := runewidth.StringWidth(string(text[i.offset:i.cursorPos])); subWidth > fieldWidth-1 {
|
2018-10-18 06:34:08 +00:00
|
|
|
shiftLeft = subWidth - fieldWidth + 1
|
|
|
|
}
|
|
|
|
currentOffset := i.offset
|
2020-10-07 01:43:34 +00:00
|
|
|
iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
2018-10-18 06:34:08 +00:00
|
|
|
if textPos >= currentOffset {
|
|
|
|
if shiftLeft > 0 {
|
|
|
|
i.offset = textPos + textWidth
|
|
|
|
shiftLeft -= screenWidth
|
|
|
|
} else {
|
|
|
|
if textPos+textWidth > i.cursorPos {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
cursorScreenPos += screenWidth
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
2020-10-07 01:43:34 +00:00
|
|
|
drawnText = EscapeBytes(text[i.offset:])
|
|
|
|
Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor)
|
2020-09-16 18:33:02 +00:00
|
|
|
}
|
|
|
|
// Draw suggestion
|
2020-10-07 01:43:34 +00:00
|
|
|
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)
|
2018-01-17 16:13:36 +00:00
|
|
|
}
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
|
2020-09-17 15:14:01 +00:00
|
|
|
// Draw field note
|
2020-10-07 01:43:34 +00:00
|
|
|
if len(i.fieldNote) > 0 {
|
|
|
|
Print(screen, i.fieldNote, x, y+1, fieldWidth, AlignLeft, i.fieldNoteTextColor)
|
2020-09-17 15:14:01 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 08:34:06 +00:00
|
|
|
// Draw autocomplete list.
|
|
|
|
if i.autocompleteList != nil {
|
|
|
|
// How much space do we need?
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We prefer to drop down but if there is no space, maybe drop up?
|
|
|
|
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
|
|
|
|
}
|
2020-02-13 23:08:35 +00:00
|
|
|
if i.autocompleteList.scrollBarVisibility == ScrollBarAlways || (i.autocompleteList.scrollBarVisibility == ScrollBarAuto && i.autocompleteList.GetItemCount() > lheight) {
|
2020-02-01 05:21:59 +00:00
|
|
|
lwidth++ // Add space for scroll bar
|
|
|
|
}
|
2019-07-08 08:34:06 +00:00
|
|
|
i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
|
|
|
|
i.autocompleteList.Draw(screen)
|
|
|
|
}
|
|
|
|
|
2017-12-15 14:29:21 +00:00
|
|
|
// Set cursor.
|
2017-12-16 21:48:26 +00:00
|
|
|
if i.focus.HasFocus() {
|
2018-10-18 06:34:08 +00:00
|
|
|
screen.ShowCursor(x+cursorScreenPos, y)
|
2018-04-09 19:07:03 +00:00
|
|
|
}
|
2017-12-15 14:29:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// InputHandler returns the handler for this primitive.
|
2017-12-18 19:04:52 +00:00
|
|
|
func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
|
2018-03-19 20:25:30 +00:00
|
|
|
return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
|
2018-01-10 09:40:51 +00:00
|
|
|
// Trigger changed events.
|
|
|
|
currentText := i.text
|
|
|
|
defer func() {
|
2020-03-25 14:32:57 +00:00
|
|
|
i.Lock()
|
|
|
|
newText := i.text
|
|
|
|
i.Unlock()
|
|
|
|
|
|