...

Source file src/gitlab.com/tslocum/cview/inputfield.go

Documentation: gitlab.com/tslocum/cview

     1  package cview
     2  
     3  import (
     4  	"bytes"
     5  	"math"
     6  	"regexp"
     7  	"sync"
     8  	"unicode/utf8"
     9  
    10  	"github.com/gdamore/tcell/v2"
    11  	"github.com/mattn/go-runewidth"
    12  )
    13  
    14  // InputField is a one-line box (three lines if there is a title) where the
    15  // user can enter text. Use SetAcceptanceFunc() to accept or reject input,
    16  // SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
    17  // from onlookers (e.g. for password input).
    18  //
    19  // The following keys can be used for navigation and editing:
    20  //
    21  //   - Left arrow: Move left by one character.
    22  //   - Right arrow: Move right by one character.
    23  //   - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
    24  //   - End, Ctrl-E, Alt-e: Move to the end of the line.
    25  //   - Alt-left, Alt-b: Move left by one word.
    26  //   - Alt-right, Alt-f: Move right by one word.
    27  //   - Backspace: Delete the character before the cursor.
    28  //   - Delete: Delete the character after the cursor.
    29  //   - Ctrl-K: Delete from the cursor to the end of the line.
    30  //   - Ctrl-W: Delete the last word before the cursor.
    31  //   - Ctrl-U: Delete the entire line.
    32  type InputField struct {
    33  	*Box
    34  
    35  	// The text that was entered.
    36  	text []byte
    37  
    38  	// The text to be displayed before the input area.
    39  	label []byte
    40  
    41  	// The text to be displayed in the input area when "text" is empty.
    42  	placeholder []byte
    43  
    44  	// The label color.
    45  	labelColor tcell.Color
    46  
    47  	// The label color when focused.
    48  	labelColorFocused tcell.Color
    49  
    50  	// The background color of the input area.
    51  	fieldBackgroundColor tcell.Color
    52  
    53  	// The background color of the input area when focused.
    54  	fieldBackgroundColorFocused tcell.Color
    55  
    56  	// The text color of the input area.
    57  	fieldTextColor tcell.Color
    58  
    59  	// The text color of the input area when focused.
    60  	fieldTextColorFocused tcell.Color
    61  
    62  	// The text color of the placeholder.
    63  	placeholderTextColor tcell.Color
    64  
    65  	// The text color of the placeholder when focused.
    66  	placeholderTextColorFocused tcell.Color
    67  
    68  	// The text color of the list items.
    69  	autocompleteListTextColor tcell.Color
    70  
    71  	// The background color of the autocomplete list.
    72  	autocompleteListBackgroundColor tcell.Color
    73  
    74  	// The text color of the selected ListItem.
    75  	autocompleteListSelectedTextColor tcell.Color
    76  
    77  	// The background color of the selected ListItem.
    78  	autocompleteListSelectedBackgroundColor tcell.Color
    79  
    80  	// The text color of the suggestion.
    81  	autocompleteSuggestionTextColor tcell.Color
    82  
    83  	// The text color of the note below the input field.
    84  	fieldNoteTextColor tcell.Color
    85  
    86  	// The note to show below the input field.
    87  	fieldNote []byte
    88  
    89  	// The screen width of the label area. A value of 0 means use the width of
    90  	// the label text.
    91  	labelWidth int
    92  
    93  	// The screen width of the input area. A value of 0 means extend as much as
    94  	// possible.
    95  	fieldWidth int
    96  
    97  	// A character to mask entered text (useful for password fields). A value of 0
    98  	// disables masking.
    99  	maskCharacter rune
   100  
   101  	// The cursor position as a byte index into the text string.
   102  	cursorPos int
   103  
   104  	// An optional autocomplete function which receives the current text of the
   105  	// input field and returns a slice of ListItems to be displayed in a drop-down
   106  	// selection. Items' main text is displayed in the autocomplete list. When
   107  	// set, items' secondary text is used as the selection value. Otherwise,
   108  	// the main text is used.
   109  	autocomplete func(text string) []*ListItem
   110  
   111  	// The List object which shows the selectable autocomplete entries. If not
   112  	// nil, the list's main texts represent the current autocomplete entries.
   113  	autocompleteList *List
   114  
   115  	// The suggested completion of the current autocomplete ListItem.
   116  	autocompleteListSuggestion []byte
   117  
   118  	// An optional function which may reject the last character that was entered.
   119  	accept func(text string, ch rune) bool
   120  
   121  	// An optional function which is called when the input has changed.
   122  	changed func(text string)
   123  
   124  	// An optional function which is called when the user indicated that they
   125  	// are done entering text. The key which was pressed is provided (tab,
   126  	// shift-tab, enter, or escape).
   127  	done func(tcell.Key)
   128  
   129  	// A callback function set by the Form class and called when the user leaves
   130  	// this form item.
   131  	finished func(tcell.Key)
   132  
   133  	// The x-coordinate of the input field as determined during the last call to Draw().
   134  	fieldX int
   135  
   136  	// The number of bytes of the text string skipped ahead while drawing.
   137  	offset int
   138  
   139  	sync.RWMutex
   140  }
   141  
   142  // NewInputField returns a new input field.
   143  func NewInputField() *InputField {
   144  	return &InputField{
   145  		Box:                                     NewBox(),
   146  		labelColor:                              Styles.SecondaryTextColor,
   147  		fieldBackgroundColor:                    Styles.ContrastBackgroundColor,
   148  		fieldTextColor:                          Styles.PrimaryTextColor,
   149  		placeholderTextColor:                    Styles.ContrastSecondaryTextColor,
   150  		autocompleteListTextColor:               Styles.PrimitiveBackgroundColor,
   151  		autocompleteListBackgroundColor:         Styles.MoreContrastBackgroundColor,
   152  		autocompleteListSelectedTextColor:       Styles.PrimitiveBackgroundColor,
   153  		autocompleteListSelectedBackgroundColor: Styles.PrimaryTextColor,
   154  		autocompleteSuggestionTextColor:         Styles.ContrastPrimaryTextColor,
   155  		fieldNoteTextColor:                      Styles.SecondaryTextColor,
   156  		labelColorFocused:                       ColorUnset,
   157  		fieldBackgroundColorFocused:             ColorUnset,
   158  		fieldTextColorFocused:                   ColorUnset,
   159  		placeholderTextColorFocused:             ColorUnset,
   160  	}
   161  }
   162  
   163  // SetText sets the current text of the input field.
   164  func (i *InputField) SetText(text string) {
   165  	i.Lock()
   166  
   167  	i.text = []byte(text)
   168  	i.cursorPos = len(text)
   169  	if i.changed != nil {
   170  		i.Unlock()
   171  		i.changed(text)
   172  	} else {
   173  		i.Unlock()
   174  	}
   175  }
   176  
   177  // GetText returns the current text of the input field.
   178  func (i *InputField) GetText() string {
   179  	i.RLock()
   180  	defer i.RUnlock()
   181  
   182  	return string(i.text)
   183  }
   184  
   185  // SetLabel sets the text to be displayed before the input area.
   186  func (i *InputField) SetLabel(label string) {
   187  	i.Lock()
   188  	defer i.Unlock()
   189  
   190  	i.label = []byte(label)
   191  }
   192  
   193  // GetLabel returns the text to be displayed before the input area.
   194  func (i *InputField) GetLabel() string {
   195  	i.RLock()
   196  	defer i.RUnlock()
   197  
   198  	return string(i.label)
   199  }
   200  
   201  // SetLabelWidth sets the screen width of the label. A value of 0 will cause the
   202  // primitive to use the width of the label string.
   203  func (i *InputField) SetLabelWidth(width int) {
   204  	i.Lock()
   205  	defer i.Unlock()
   206  
   207  	i.labelWidth = width
   208  }
   209  
   210  // SetPlaceholder sets the text to be displayed when the input text is empty.
   211  func (i *InputField) SetPlaceholder(text string) {
   212  	i.Lock()
   213  	defer i.Unlock()
   214  
   215  	i.placeholder = []byte(text)
   216  }
   217  
   218  // SetLabelColor sets the color of the label.
   219  func (i *InputField) SetLabelColor(color tcell.Color) {
   220  	i.Lock()
   221  	defer i.Unlock()
   222  
   223  	i.labelColor = color
   224  }
   225  
   226  // SetLabelColorFocused sets the color of the label when focused.
   227  func (i *InputField) SetLabelColorFocused(color tcell.Color) {
   228  	i.Lock()
   229  	defer i.Unlock()
   230  
   231  	i.labelColorFocused = color
   232  }
   233  
   234  // SetFieldBackgroundColor sets the background color of the input area.
   235  func (i *InputField) SetFieldBackgroundColor(color tcell.Color) {
   236  	i.Lock()
   237  	defer i.Unlock()
   238  
   239  	i.fieldBackgroundColor = color
   240  }
   241  
   242  // SetFieldBackgroundColorFocused sets the background color of the input area
   243  // when focused.
   244  func (i *InputField) SetFieldBackgroundColorFocused(color tcell.Color) {
   245  	i.Lock()
   246  	defer i.Unlock()
   247  
   248  	i.fieldBackgroundColorFocused = color
   249  }
   250  
   251  // SetFieldTextColor sets the text color of the input area.
   252  func (i *InputField) SetFieldTextColor(color tcell.Color) {
   253  	i.Lock()
   254  	defer i.Unlock()
   255  
   256  	i.fieldTextColor = color
   257  }
   258  
   259  // SetFieldTextColorFocused sets the text color of the input area when focused.
   260  func (i *InputField) SetFieldTextColorFocused(color tcell.Color) {
   261  	i.Lock()
   262  	defer i.Unlock()
   263  
   264  	i.fieldTextColorFocused = color
   265  }
   266  
   267  // SetPlaceholderTextColor sets the text color of placeholder text.
   268  func (i *InputField) SetPlaceholderTextColor(color tcell.Color) {
   269  	i.Lock()
   270  	defer i.Unlock()
   271  
   272  	i.placeholderTextColor = color
   273  }
   274  
   275  // SetPlaceholderTextColorFocused sets the text color of placeholder text when
   276  // focused.
   277  func (i *InputField) SetPlaceholderTextColorFocused(color tcell.Color) {
   278  	i.Lock()
   279  	defer i.Unlock()
   280  
   281  	i.placeholderTextColorFocused = color
   282  }
   283  
   284  // SetAutocompleteListTextColor sets the text color of the ListItems.
   285  func (i *InputField) SetAutocompleteListTextColor(color tcell.Color) {
   286  	i.Lock()
   287  	defer i.Unlock()
   288  
   289  	i.autocompleteListTextColor = color
   290  }
   291  
   292  // SetAutocompleteListBackgroundColor sets the background color of the
   293  // autocomplete list.
   294  func (i *InputField) SetAutocompleteListBackgroundColor(color tcell.Color) {
   295  	i.Lock()
   296  	defer i.Unlock()
   297  
   298  	i.autocompleteListBackgroundColor = color
   299  }
   300  
   301  // SetAutocompleteListSelectedTextColor sets the text color of the selected
   302  // ListItem.
   303  func (i *InputField) SetAutocompleteListSelectedTextColor(color tcell.Color) {
   304  	i.Lock()
   305  	defer i.Unlock()
   306  
   307  	i.autocompleteListSelectedTextColor = color
   308  }
   309  
   310  // SetAutocompleteListSelectedBackgroundColor sets the background of the
   311  // selected ListItem.
   312  func (i *InputField) SetAutocompleteListSelectedBackgroundColor(color tcell.Color) {
   313  	i.Lock()
   314  	defer i.Unlock()
   315  
   316  	i.autocompleteListSelectedBackgroundColor = color
   317  }
   318  
   319  // SetAutocompleteSuggestionTextColor sets the text color of the autocomplete
   320  // suggestion in the input field.
   321  func (i *InputField) SetAutocompleteSuggestionTextColor(color tcell.Color) {
   322  	i.Lock()
   323  	defer i.Unlock()
   324  
   325  	i.autocompleteSuggestionTextColor = color
   326  }
   327  
   328  // SetFieldNoteTextColor sets the text color of the note.
   329  func (i *InputField) SetFieldNoteTextColor(color tcell.Color) {
   330  	i.Lock()
   331  	defer i.Unlock()
   332  
   333  	i.fieldNoteTextColor = color
   334  }
   335  
   336  // SetFieldNote sets the text to show below the input field, e.g. when the
   337  // input is invalid.
   338  func (i *InputField) SetFieldNote(note string) {
   339  	i.Lock()
   340  	defer i.Unlock()
   341  
   342  	i.fieldNote = []byte(note)
   343  }
   344  
   345  // ResetFieldNote sets the note to an empty string.
   346  func (i *InputField) ResetFieldNote() {
   347  	i.Lock()
   348  	defer i.Unlock()
   349  
   350  	i.fieldNote = nil
   351  }
   352  
   353  // SetFieldWidth sets the screen width of the input area. A value of 0 means
   354  // extend as much as possible.
   355  func (i *InputField) SetFieldWidth(width int) {
   356  	i.Lock()
   357  	defer i.Unlock()
   358  
   359  	i.fieldWidth = width
   360  }
   361  
   362  // GetFieldWidth returns this primitive's field width.
   363  func (i *InputField) GetFieldWidth() int {
   364  	i.RLock()
   365  	defer i.RUnlock()
   366  
   367  	return i.fieldWidth
   368  }
   369  
   370  // GetFieldHeight returns the height of the field.
   371  func (i *InputField) GetFieldHeight() int {
   372  	i.RLock()
   373  	defer i.RUnlock()
   374  	if len(i.fieldNote) == 0 {
   375  		return 1
   376  	}
   377  	return 2
   378  }
   379  
   380  // GetCursorPosition returns the cursor position.
   381  func (i *InputField) GetCursorPosition() int {
   382  	i.RLock()
   383  	defer i.RUnlock()
   384  
   385  	return i.cursorPos
   386  }
   387  
   388  // SetCursorPosition sets the cursor position.
   389  func (i *InputField) SetCursorPosition(cursorPos int) {
   390  	i.Lock()
   391  	defer i.Unlock()
   392  
   393  	i.cursorPos = cursorPos
   394  }
   395  
   396  // SetMaskCharacter sets a character that masks user input on a screen. A value
   397  // of 0 disables masking.
   398  func (i *InputField) SetMaskCharacter(mask rune) {
   399  	i.Lock()
   400  	defer i.Unlock()
   401  
   402  	i.maskCharacter = mask
   403  }
   404  
   405  // SetAutocompleteFunc sets an autocomplete callback function which may return
   406  // ListItems to be selected from a drop-down based on the current text of the
   407  // input field. The drop-down appears only if len(entries) > 0. The callback is
   408  // invoked in this function and whenever the current text changes or when
   409  // Autocomplete() is called. Entries are cleared when the user selects an entry
   410  // or presses Escape.
   411  func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []*ListItem)) {
   412  	i.Lock()
   413  	i.autocomplete = callback
   414  	i.Unlock()
   415  
   416  	i.Autocomplete()
   417  }
   418  
   419  // Autocomplete invokes the autocomplete callback (if there is one). If the
   420  // length of the returned autocomplete entries slice is greater than 0, the
   421  // input field will present the user with a corresponding drop-down list the
   422  // next time the input field is drawn.
   423  //
   424  // It is safe to call this function from any goroutine. Note that the input
   425  // field is not redrawn automatically unless called from the main goroutine
   426  // (e.g. in response to events).
   427  func (i *InputField) Autocomplete() {
   428  	i.Lock()
   429  	if i.autocomplete == nil {
   430  		i.Unlock()
   431  		return
   432  	}
   433  	i.Unlock()
   434  
   435  	// Do we have any autocomplete entries?
   436  	entries := i.autocomplete(string(i.text))
   437  	if len(entries) == 0 {
   438  		// No entries, no list.
   439  		i.Lock()
   440  		i.autocompleteList = nil
   441  		i.autocompleteListSuggestion = nil
   442  		i.Unlock()
   443  		return
   444  	}
   445  
   446  	i.Lock()
   447  
   448  	// Make a list if we have none.
   449  	if i.autocompleteList == nil {
   450  		l := NewList()
   451  		l.SetChangedFunc(i.autocompleteChanged)
   452  		l.ShowSecondaryText(false)
   453  		l.SetMainTextColor(i.autocompleteListTextColor)
   454  		l.SetSelectedTextColor(i.autocompleteListSelectedTextColor)
   455  		l.SetSelectedBackgroundColor(i.autocompleteListSelectedBackgroundColor)
   456  		l.SetHighlightFullLine(true)
   457  		l.SetBackgroundColor(i.autocompleteListBackgroundColor)
   458  
   459  		i.autocompleteList = l
   460  	}
   461  
   462  	// Fill it with the entries.
   463  	currentEntry := -1
   464  	i.autocompleteList.Clear()
   465  	for index, entry := range entries {
   466  		i.autocompleteList.AddItem(entry)
   467  		if currentEntry < 0 && entry.GetMainText() == string(i.text) {
   468  			currentEntry = index
   469  		}
   470  	}
   471  
   472  	// Set the selection if we have one.
   473  	if currentEntry >= 0 {
   474  		i.autocompleteList.SetCurrentItem(currentEntry)
   475  	}
   476  
   477  	i.Unlock()
   478  }
   479  
   480  // autocompleteChanged gets called when another item in the
   481  // autocomplete list has been selected.
   482  func (i *InputField) autocompleteChanged(_ int, item *ListItem) {
   483  	mainText := item.GetMainBytes()
   484  	secondaryText := item.GetSecondaryBytes()
   485  	if len(i.text) < len(secondaryText) {
   486  		i.autocompleteListSuggestion = secondaryText[len(i.text):]
   487  	} else if len(i.text) < len(mainText) {
   488  		i.autocompleteListSuggestion = mainText[len(i.text):]
   489  	} else {
   490  		i.autocompleteListSuggestion = nil
   491  	}
   492  }
   493  
   494  // SetAcceptanceFunc sets a handler which may reject the last character that was
   495  // entered (by returning false).
   496  //
   497  // This package defines a number of variables prefixed with InputField which may
   498  // be used for common input (e.g. numbers, maximum text length).
   499  func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) {
   500  	i.Lock()
   501  	defer i.Unlock()
   502  
   503  	i.accept = handler
   504  }
   505  
   506  // SetChangedFunc sets a handler which is called whenever the text of the input
   507  // field has changed. It receives the current text (after the change).
   508  func (i *InputField) SetChangedFunc(handler func(text string)) {
   509  	i.Lock()
   510  	defer i.Unlock()
   511  
   512  	i.changed = handler
   513  }
   514  
   515  // SetDoneFunc sets a handler which is called when the user is done entering
   516  // text. The callback function is provided with the key that was pressed, which
   517  // is one of the following:
   518  //
   519  //   - KeyEnter: Done entering text.
   520  //   - KeyEscape: Abort text input.
   521  //   - KeyTab: Move to the next field.
   522  //   - KeyBacktab: Move to the previous field.
   523  func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) {
   524  	i.Lock()
   525  	defer i.Unlock()
   526  
   527  	i.done = handler
   528  }
   529  
   530  // SetFinishedFunc sets a callback invoked when the user leaves this form item.
   531  func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) {
   532  	i.Lock()
   533  	defer i.Unlock()
   534  
   535  	i.finished = handler
   536  }
   537  
   538  // Draw draws this primitive onto the screen.
   539  func (i *InputField) Draw(screen tcell.Screen) {
   540  	if !i.GetVisible() {
   541  		return
   542  	}
   543  
   544  	i.Box.Draw(screen)
   545  
   546  	i.Lock()
   547  	defer i.Unlock()
   548  
   549  	// Select colors
   550  	labelColor := i.labelColor
   551  	fieldBackgroundColor := i.fieldBackgroundColor
   552  	fieldTextColor := i.fieldTextColor
   553  	if i.GetFocusable().HasFocus() {
   554  		if i.labelColorFocused != ColorUnset {
   555  			labelColor = i.labelColorFocused
   556  		}
   557  		if i.fieldBackgroundColorFocused != ColorUnset {
   558  			fieldBackgroundColor = i.fieldBackgroundColorFocused
   559  		}
   560  		if i.fieldTextColorFocused != ColorUnset {
   561  			fieldTextColor = i.fieldTextColorFocused
   562  		}
   563  	}
   564  
   565  	// Prepare
   566  	x, y, width, height := i.GetInnerRect()
   567  	rightLimit := x + width
   568  	if height < 1 || rightLimit <= x {
   569  		return
   570  	}
   571  
   572  	// Draw label.
   573  	if i.labelWidth > 0 {
   574  		labelWidth := i.labelWidth
   575  		if labelWidth > rightLimit-x {
   576  			labelWidth = rightLimit - x
   577  		}
   578  		Print(screen, i.label, x, y, labelWidth, AlignLeft, labelColor)
   579  		x += labelWidth
   580  	} else {
   581  		_, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, labelColor)
   582  		x += drawnWidth
   583  	}
   584  
   585  	// Draw input area.
   586  	i.fieldX = x
   587  	fieldWidth := i.fieldWidth
   588  	if fieldWidth == 0 {
   589  		fieldWidth = math.MaxInt32
   590  	}
   591  	if rightLimit-x < fieldWidth {
   592  		fieldWidth = rightLimit - x
   593  	}
   594  	fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
   595  	for index := 0; index < fieldWidth; index++ {
   596  		screen.SetContent(x+index, y, ' ', nil, fieldStyle)
   597  	}
   598  
   599  	// Text.
   600  	var cursorScreenPos int
   601  	text := i.text
   602  	if len(text) == 0 && len(i.placeholder) > 0 {
   603  		// Draw placeholder text.
   604  		placeholderTextColor := i.placeholderTextColor
   605  		if i.GetFocusable().HasFocus() && i.placeholderTextColorFocused != ColorUnset {
   606  			placeholderTextColor = i.placeholderTextColorFocused
   607  		}
   608  		Print(screen, EscapeBytes(i.placeholder), x, y, fieldWidth, AlignLeft, placeholderTextColor)
   609  		i.offset = 0
   610  	} else {
   611  		// Draw entered text.
   612  		if i.maskCharacter > 0 {
   613  			text = bytes.Repeat([]byte(string(i.maskCharacter)), utf8.RuneCount(i.text))
   614  		}
   615  		var drawnText []byte
   616  		if fieldWidth >= runewidth.StringWidth(string(text)) {
   617  			// We have enough space for the full text.
   618  			drawnText = EscapeBytes(text)
   619  			Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor)
   620  			i.offset = 0
   621  			iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
   622  				if textPos >= i.cursorPos {
   623  					return true
   624  				}
   625  				cursorScreenPos += screenWidth
   626  				return false
   627  			})
   628  		} else {
   629  			// The text doesn't fit. Where is the cursor?
   630  			if i.cursorPos < 0 {
   631  				i.cursorPos = 0
   632  			} else if i.cursorPos > len(text) {
   633  				i.cursorPos = len(text)
   634  			}
   635  			// Shift the text so the cursor is inside the field.
   636  			var shiftLeft int
   637  			if i.offset > i.cursorPos {
   638  				i.offset = i.cursorPos
   639  			} else if subWidth := runewidth.StringWidth(string(text[i.offset:i.cursorPos])); subWidth > fieldWidth-1 {
   640  				shiftLeft = subWidth - fieldWidth + 1
   641  			}
   642  			currentOffset := i.offset
   643  			iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
   644  				if textPos >= currentOffset {
   645  					if shiftLeft > 0 {
   646  						i.offset = textPos + textWidth
   647  						shiftLeft -= screenWidth
   648  					} else {
   649  						if textPos+textWidth > i.cursorPos {
   650  							return true
   651  						}
   652  						cursorScreenPos += screenWidth
   653  					}
   654  				}
   655  				return false
   656  			})
   657  			drawnText = EscapeBytes(text[i.offset:])
   658  			Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor)
   659  		}
   660  		// Draw suggestion
   661  		if i.maskCharacter == 0 && len(i.autocompleteListSuggestion) > 0 {
   662  			Print(screen, i.autocompleteListSuggestion, x+runewidth.StringWidth(string(drawnText)), y, fieldWidth-runewidth.StringWidth(string(drawnText)), AlignLeft, i.autocompleteSuggestionTextColor)
   663  		}
   664  	}
   665  
   666  	// Draw field note
   667  	if len(i.fieldNote) > 0 {
   668  		Print(screen, i.fieldNote, x, y+1, fieldWidth, AlignLeft, i.fieldNoteTextColor)
   669  	}
   670  
   671  	// Draw autocomplete list.
   672  	if i.autocompleteList != nil {
   673  		// How much space do we need?
   674  		lheight := i.autocompleteList.GetItemCount()
   675  		lwidth := 0
   676  		for index := 0; index < lheight; index++ {
   677  			entry, _ := i.autocompleteList.GetItemText(index)
   678  			width := TaggedStringWidth(entry)
   679  			if width > lwidth {
   680  				lwidth = width
   681  			}
   682  		}
   683  
   684  		// We prefer to drop down but if there is no space, maybe drop up?
   685  		lx := x
   686  		ly := y + 1
   687  		_, sheight := screen.Size()
   688  		if ly+lheight >= sheight && ly-2 > lheight-ly {
   689  			ly = y - lheight
   690  			if ly < 0 {
   691  				ly = 0
   692  			}
   693  		}
   694  		if ly+lheight >= sheight {
   695  			lheight = sheight - ly
   696  		}
   697  		if i.autocompleteList.scrollBarVisibility == ScrollBarAlways || (i.autocompleteList.scrollBarVisibility == ScrollBarAuto && i.autocompleteList.GetItemCount() > lheight) {
   698  			lwidth++ // Add space for scroll bar
   699  		}
   700  		i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
   701  		i.autocompleteList.Draw(screen)
   702  	}
   703  
   704  	// Set cursor.
   705  	if i.focus.HasFocus() {
   706  		screen.ShowCursor(x+cursorScreenPos, y)
   707  	}
   708  }
   709  
   710  // InputHandler returns the handler for this primitive.
   711  func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   712  	return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
   713  		i.Lock()
   714  
   715  		// Trigger changed events.
   716  		currentText := i.text
   717  		defer func() {
   718  			i.Lock()
   719  			newText := i.text
   720  			i.Unlock()
   721  
   722  			if !bytes.Equal(newText, currentText) {
   723  				i.Autocomplete()
   724  				if i.changed != nil {
   725  					i.changed(string(i.text))
   726  				}
   727  			}
   728  		}()
   729  
   730  		// Movement functions.
   731  		home := func() { i.cursorPos = 0 }
   732  		end := func() { i.cursorPos = len(i.text) }
   733  		moveLeft := func() {
   734  			iterateStringReverse(string(i.text[:i.cursorPos]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
   735  				i.cursorPos -= textWidth
   736  				return true
   737  			})
   738  		}
   739  		moveRight := func() {
   740  			iterateString(string(i.text[i.cursorPos:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
   741  				i.cursorPos += textWidth
   742  				return true
   743  			})
   744  		}
   745  		moveWordLeft := func() {
   746  			i.cursorPos = len(regexRightWord.ReplaceAll(i.text[:i.cursorPos], nil))
   747  		}
   748  		moveWordRight := func() {
   749  			i.cursorPos = len(i.text) - len(regexLeftWord.ReplaceAll(i.text[i.cursorPos:], nil))
   750  		}
   751  
   752  		// Add character function. Returns whether or not the rune character is
   753  		// accepted.
   754  		add := func(r rune) bool {
   755  			newText := append(append(i.text[:i.cursorPos], []byte(string(r))...), i.text[i.cursorPos:]...)
   756  			if i.accept != nil && !i.accept(string(newText), r) {
   757  				return false
   758  			}
   759  			i.text = newText
   760  			i.cursorPos += len(string(r))
   761  			return true
   762  		}
   763  
   764  		// Finish up.
   765  		finish := func(key tcell.Key) {
   766  			if i.done != nil {
   767  				i.done(key)
   768  			}
   769  			if i.finished != nil {
   770  				i.finished(key)
   771  			}
   772  		}
   773  
   774  		// Process key event.
   775  		switch key := event.Key(); key {
   776  		case tcell.KeyRune: // Regular character.
   777  			if event.Modifiers()&tcell.ModAlt > 0 {
   778  				// We accept some Alt- key combinations.
   779  				switch event.Rune() {
   780  				case 'a': // Home.
   781  					home()
   782  				case 'e': // End.
   783  					end()
   784  				case 'b': // Move word left.
   785  					moveWordLeft()
   786  				case 'f': // Move word right.
   787  					moveWordRight()
   788  				default:
   789  					if !add(event.Rune()) {
   790  						i.Unlock()
   791  						return
   792  					}
   793  				}
   794  			} else {
   795  				// Other keys are simply accepted as regular characters.
   796  				if !add(event.Rune()) {
   797  					i.Unlock()
   798  					return
   799  				}
   800  			}
   801  		case tcell.KeyCtrlU: // Delete all.
   802  			i.text = nil
   803  			i.cursorPos = 0
   804  		case tcell.KeyCtrlK: // Delete until the end of the line.
   805  			i.text = i.text[:i.cursorPos]
   806  		case tcell.KeyCtrlW: // Delete last word.
   807  			newText := append(regexRightWord.ReplaceAll(i.text[:i.cursorPos], nil), i.text[i.cursorPos:]...)
   808  			i.cursorPos -= len(i.text) - len(newText)
   809  			i.text = newText
   810  		case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
   811  			iterateStringReverse(string(i.text[:i.cursorPos]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
   812  				i.text = append(i.text[:textPos], i.text[textPos+textWidth:]...)
   813  				i.cursorPos -= textWidth
   814  				return true
   815  			})
   816  			if i.offset >= i.cursorPos {
   817  				i.offset = 0
   818  			}
   819  		case tcell.KeyDelete: // Delete character after the cursor.
   820  			iterateString(string(i.text[i.cursorPos:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
   821  				i.text = append(i.text[:i.cursorPos], i.text[i.cursorPos+textWidth:]...)
   822  				return true
   823  			})
   824  		case tcell.KeyLeft:
   825  			if event.Modifiers()&tcell.ModAlt > 0 {
   826  				moveWordLeft()
   827  			} else {
   828  				moveLeft()
   829  			}
   830  		case tcell.KeyRight:
   831  			if event.Modifiers()&tcell.ModAlt > 0 {
   832  				moveWordRight()
   833  			} else {
   834  				moveRight()
   835  			}
   836  		case tcell.KeyHome, tcell.KeyCtrlA:
   837  			home()
   838  		case tcell.KeyEnd, tcell.KeyCtrlE:
   839  			end()
   840  		case tcell.KeyEnter: // We might be done.
   841  			if i.autocompleteList != nil {
   842  				currentItem := i.autocompleteList.GetCurrentItem()
   843  				selectionText := currentItem.GetMainText()
   844  				if currentItem.GetSecondaryText() != "" {
   845  					selectionText = currentItem.GetSecondaryText()
   846  				}
   847  				i.Unlock()
   848  				i.SetText(selectionText)
   849  				i.Lock()
   850  				i.autocompleteList = nil
   851  				i.autocompleteListSuggestion = nil
   852  				i.Unlock()
   853  			} else {
   854  				i.Unlock()
   855  				finish(key)
   856  			}
   857  			return
   858  		case tcell.KeyEscape:
   859  			if i.autocompleteList != nil {
   860  				i.autocompleteList = nil
   861  				i.autocompleteListSuggestion = nil
   862  				i.Unlock()
   863  			} else {
   864  				i.Unlock()
   865  				finish(key)
   866  			}
   867  			return
   868  		case tcell.KeyDown, tcell.KeyTab: // Autocomplete selection.
   869  			if i.autocompleteList != nil {
   870  				count := i.autocompleteList.GetItemCount()
   871  				newEntry := i.autocompleteList.GetCurrentItemIndex() + 1
   872  				if newEntry >= count {
   873  					newEntry = 0
   874  				}
   875  				i.autocompleteList.SetCurrentItem(newEntry)
   876  				i.Unlock()
   877  			} else {
   878  				i.Unlock()
   879  				finish(key)
   880  			}
   881  			return
   882  		case tcell.KeyUp, tcell.KeyBacktab: // Autocomplete selection.
   883  			if i.autocompleteList != nil {
   884  				newEntry := i.autocompleteList.GetCurrentItemIndex() - 1
   885  				if newEntry < 0 {
   886  					newEntry = i.autocompleteList.GetItemCount() - 1
   887  				}
   888  				i.autocompleteList.SetCurrentItem(newEntry)
   889  				i.Unlock()
   890  			} else {
   891  				i.Unlock()
   892  				finish(key)
   893  			}
   894  			return
   895  		}
   896  
   897  		i.Unlock()
   898  	})
   899  }
   900  
   901  // MouseHandler returns the mouse handler for this primitive.
   902  func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   903  	return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   904  		x, y := event.Position()
   905  		_, rectY, _, _ := i.GetInnerRect()
   906  		if !i.InRect(x, y) {
   907  			return false, nil
   908  		}
   909  
   910  		// Process mouse event.
   911  		if action == MouseLeftClick && y == rectY {
   912  			// Determine where to place the cursor.
   913  			if x >= i.fieldX {
   914  				if !iterateString(string(i.text), func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
   915  					if x-i.fieldX < screenPos+screenWidth {
   916  						i.cursorPos = textPos
   917  						return true
   918  					}
   919  					return false
   920  				}) {
   921  					i.cursorPos = len(i.text)
   922  				}
   923  			}
   924  			setFocus(i)
   925  			consumed = true
   926  		}
   927  
   928  		return
   929  	})
   930  }
   931  
   932  var (
   933  	regexRightWord = regexp.MustCompile(`(\w*|\W)$`)
   934  	regexLeftWord  = regexp.MustCompile(`^(\W|\w*)`)
   935  )
   936  

View as plain text