...

Source file src/github.com/rivo/tview/inputfield.go

Documentation: github.com/rivo/tview

     1  package tview
     2  
     3  import (
     4  	"math"
     5  	"regexp"
     6  	"strings"
     7  	"sync"
     8  	"unicode/utf8"
     9  
    10  	"github.com/gdamore/tcell/v2"
    11  	"github.com/rivo/uniseg"
    12  )
    13  
    14  const (
    15  	AutocompletedNavigate = iota // The user navigated the autocomplete list (using the errow keys).
    16  	AutocompletedTab             // The user selected an autocomplete entry using the tab key.
    17  	AutocompletedEnter           // The user selected an autocomplete entry using the enter key.
    18  	AutocompletedClick           // The user selected an autocomplete entry by clicking the mouse button on it.
    19  )
    20  
    21  // InputField is a one-line box (three lines if there is a title) where the
    22  // user can enter text. Use [InputField.SetAcceptanceFunc] to accept or reject
    23  // input, [InputField.SetChangedFunc] to listen for changes, and
    24  // [InputField.SetMaskCharacter] to hide input from onlookers (e.g. for password
    25  // input).
    26  //
    27  // The input field also has an optional autocomplete feature. It is initialized
    28  // by the [InputField.SetAutocompleteFunc] function. For more control over the
    29  // autocomplete drop-down's behavior, you can also set the
    30  // [InputField.SetAutocompletedFunc].
    31  //
    32  // The following keys can be used for navigation and editing:
    33  //
    34  //   - Left arrow: Move left by one character.
    35  //   - Right arrow: Move right by one character.
    36  //   - Down arrow: Open the autocomplete drop-down.
    37  //   - Tab, Enter: Select the current autocomplete entry.
    38  //   - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
    39  //   - End, Ctrl-E, Alt-e: Move to the end of the line.
    40  //   - Alt-left, Alt-b: Move left by one word.
    41  //   - Alt-right, Alt-f: Move right by one word.
    42  //   - Backspace: Delete the character before the cursor.
    43  //   - Delete: Delete the character after the cursor.
    44  //   - Ctrl-K: Delete from the cursor to the end of the line.
    45  //   - Ctrl-W: Delete the last word before the cursor.
    46  //   - Ctrl-U: Delete the entire line.
    47  //
    48  // See https://github.com/rivo/tview/wiki/InputField for an example.
    49  type InputField struct {
    50  	*Box
    51  
    52  	// Whether or not this input field is disabled/read-only.
    53  	disabled bool
    54  
    55  	// The text that was entered.
    56  	text string
    57  
    58  	// The text to be displayed before the input area.
    59  	label string
    60  
    61  	// The text to be displayed in the input area when "text" is empty.
    62  	placeholder string
    63  
    64  	// The label style.
    65  	labelStyle tcell.Style
    66  
    67  	// The style of the input area with input text.
    68  	fieldStyle tcell.Style
    69  
    70  	// The style of the input area with placeholder text.
    71  	placeholderStyle tcell.Style
    72  
    73  	// The screen width of the label area. A value of 0 means use the width of
    74  	// the label text.
    75  	labelWidth int
    76  
    77  	// The screen width of the input area. A value of 0 means extend as much as
    78  	// possible.
    79  	fieldWidth int
    80  
    81  	// A character to mask entered text (useful for password fields). A value of 0
    82  	// disables masking.
    83  	maskCharacter rune
    84  
    85  	// The cursor position as a byte index into the text string.
    86  	cursorPos int
    87  
    88  	// An optional autocomplete function which receives the current text of the
    89  	// input field and returns a slice of strings to be displayed in a drop-down
    90  	// selection.
    91  	autocomplete func(text string) []string
    92  
    93  	// The List object which shows the selectable autocomplete entries. If not
    94  	// nil, the list's main texts represent the current autocomplete entries.
    95  	autocompleteList      *List
    96  	autocompleteListMutex sync.Mutex
    97  
    98  	// The styles of the autocomplete entries.
    99  	autocompleteStyles struct {
   100  		main       tcell.Style
   101  		selected   tcell.Style
   102  		background tcell.Color
   103  	}
   104  
   105  	// An optional function which is called when the user selects an
   106  	// autocomplete entry. The text and index of the selected entry (within the
   107  	// list) is provided, as well as the user action causing the selection (one
   108  	// of the "Autocompleted" values). The function should return true if the
   109  	// autocomplete list should be closed. If nil, the input field will be
   110  	// updated automatically when the user navigates the autocomplete list.
   111  	autocompleted func(text string, index int, source int) bool
   112  
   113  	// An optional function which may reject the last character that was entered.
   114  	accept func(text string, ch rune) bool
   115  
   116  	// An optional function which is called when the input has changed.
   117  	changed func(text string)
   118  
   119  	// An optional function which is called when the user indicated that they
   120  	// are done entering text. The key which was pressed is provided (tab,
   121  	// shift-tab, enter, or escape).
   122  	done func(tcell.Key)
   123  
   124  	// A callback function set by the Form class and called when the user leaves
   125  	// this form item.
   126  	finished func(tcell.Key)
   127  
   128  	fieldX int // The x-coordinate of the input field as determined during the last call to Draw().
   129  	offset int // The number of bytes of the text string skipped ahead while drawing.
   130  }
   131  
   132  // NewInputField returns a new input field.
   133  func NewInputField() *InputField {
   134  	i := &InputField{
   135  		Box:              NewBox(),
   136  		labelStyle:       tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
   137  		fieldStyle:       tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
   138  		placeholderStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.ContrastSecondaryTextColor),
   139  	}
   140  	i.autocompleteStyles.main = tcell.StyleDefault.Foreground(Styles.PrimitiveBackgroundColor)
   141  	i.autocompleteStyles.selected = tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.PrimitiveBackgroundColor)
   142  	i.autocompleteStyles.background = Styles.MoreContrastBackgroundColor
   143  	return i
   144  }
   145  
   146  // SetText sets the current text of the input field.
   147  func (i *InputField) SetText(text string) *InputField {
   148  	i.text = text
   149  	i.cursorPos = len(text)
   150  	if i.changed != nil {
   151  		i.changed(text)
   152  	}
   153  	return i
   154  }
   155  
   156  // GetText returns the current text of the input field.
   157  func (i *InputField) GetText() string {
   158  	return i.text
   159  }
   160  
   161  // SetLabel sets the text to be displayed before the input area.
   162  func (i *InputField) SetLabel(label string) *InputField {
   163  	i.label = label
   164  	return i
   165  }
   166  
   167  // GetLabel returns the text to be displayed before the input area.
   168  func (i *InputField) GetLabel() string {
   169  	return i.label
   170  }
   171  
   172  // SetLabelWidth sets the screen width of the label. A value of 0 will cause the
   173  // primitive to use the width of the label string.
   174  func (i *InputField) SetLabelWidth(width int) *InputField {
   175  	i.labelWidth = width
   176  	return i
   177  }
   178  
   179  // SetPlaceholder sets the text to be displayed when the input text is empty.
   180  func (i *InputField) SetPlaceholder(text string) *InputField {
   181  	i.placeholder = text
   182  	return i
   183  }
   184  
   185  // SetLabelColor sets the text color of the label.
   186  func (i *InputField) SetLabelColor(color tcell.Color) *InputField {
   187  	i.labelStyle = i.labelStyle.Foreground(color)
   188  	return i
   189  }
   190  
   191  // SetLabelStyle sets the style of the label.
   192  func (i *InputField) SetLabelStyle(style tcell.Style) *InputField {
   193  	i.labelStyle = style
   194  	return i
   195  }
   196  
   197  // GetLabelStyle returns the style of the label.
   198  func (i *InputField) GetLabelStyle() tcell.Style {
   199  	return i.labelStyle
   200  }
   201  
   202  // SetFieldBackgroundColor sets the background color of the input area.
   203  func (i *InputField) SetFieldBackgroundColor(color tcell.Color) *InputField {
   204  	i.fieldStyle = i.fieldStyle.Background(color)
   205  	return i
   206  }
   207  
   208  // SetFieldTextColor sets the text color of the input area.
   209  func (i *InputField) SetFieldTextColor(color tcell.Color) *InputField {
   210  	i.fieldStyle = i.fieldStyle.Foreground(color)
   211  	return i
   212  }
   213  
   214  // SetFieldStyle sets the style of the input area (when no placeholder is
   215  // shown).
   216  func (i *InputField) SetFieldStyle(style tcell.Style) *InputField {
   217  	i.fieldStyle = style
   218  	return i
   219  }
   220  
   221  // GetFieldStyle returns the style of the input area (when no placeholder is
   222  // shown).
   223  func (i *InputField) GetFieldStyle() tcell.Style {
   224  	return i.fieldStyle
   225  }
   226  
   227  // SetPlaceholderTextColor sets the text color of placeholder text.
   228  func (i *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField {
   229  	i.placeholderStyle = i.placeholderStyle.Foreground(color)
   230  	return i
   231  }
   232  
   233  // SetPlaceholderStyle sets the style of the input area (when a placeholder is
   234  // shown).
   235  func (i *InputField) SetPlaceholderStyle(style tcell.Style) *InputField {
   236  	i.placeholderStyle = style
   237  	return i
   238  }
   239  
   240  // GetPlaceholderStyle returns the style of the input area (when a placeholder
   241  // is shown).
   242  func (i *InputField) GetPlaceholderStyle() tcell.Style {
   243  	return i.placeholderStyle
   244  }
   245  
   246  // SetAutocompleteStyles sets the colors and style of the autocomplete entries.
   247  // For details, see List.SetMainTextStyle(), List.SetSelectedStyle(), and
   248  // Box.SetBackgroundColor().
   249  func (i *InputField) SetAutocompleteStyles(background tcell.Color, main, selected tcell.Style) *InputField {
   250  	i.autocompleteStyles.background = background
   251  	i.autocompleteStyles.main = main
   252  	i.autocompleteStyles.selected = selected
   253  	return i
   254  }
   255  
   256  // SetFormAttributes sets attributes shared by all form items.
   257  func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
   258  	i.labelWidth = labelWidth
   259  	i.backgroundColor = bgColor
   260  	i.SetLabelColor(labelColor).
   261  		SetFieldTextColor(fieldTextColor).
   262  		SetFieldBackgroundColor(fieldBgColor)
   263  	return i
   264  }
   265  
   266  // SetFieldWidth sets the screen width of the input area. A value of 0 means
   267  // extend as much as possible.
   268  func (i *InputField) SetFieldWidth(width int) *InputField {
   269  	i.fieldWidth = width
   270  	return i
   271  }
   272  
   273  // GetFieldWidth returns this primitive's field width.
   274  func (i *InputField) GetFieldWidth() int {
   275  	return i.fieldWidth
   276  }
   277  
   278  // GetFieldHeight returns this primitive's field height.
   279  func (i *InputField) GetFieldHeight() int {
   280  	return 1
   281  }
   282  
   283  // SetDisabled sets whether or not the item is disabled / read-only.
   284  func (i *InputField) SetDisabled(disabled bool) FormItem {
   285  	i.disabled = disabled
   286  	if i.finished != nil {
   287  		i.finished(-1)
   288  	}
   289  	return i
   290  }
   291  
   292  // SetMaskCharacter sets a character that masks user input on a screen. A value
   293  // of 0 disables masking.
   294  func (i *InputField) SetMaskCharacter(mask rune) *InputField {
   295  	i.maskCharacter = mask
   296  	return i
   297  }
   298  
   299  // SetAutocompleteFunc sets an autocomplete callback function which may return
   300  // strings to be selected from a drop-down based on the current text of the
   301  // input field. The drop-down appears only if len(entries) > 0. The callback is
   302  // invoked in this function and whenever the current text changes or when
   303  // Autocomplete() is called. Entries are cleared when the user selects an entry
   304  // or presses Escape.
   305  func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []string)) *InputField {
   306  	i.autocomplete = callback
   307  	i.Autocomplete()
   308  	return i
   309  }
   310  
   311  // SetAutocompletedFunc sets a callback function which is invoked when the user
   312  // selects an entry from the autocomplete drop-down list. The function is passed
   313  // the text of the selected entry (stripped of any color tags), the index of the
   314  // entry, and the user action that caused the selection, e.g.
   315  // [AutocompletedNavigate]. It returns true if the autocomplete drop-down should
   316  // be closed after the callback returns or false if it should remain open, in
   317  // which case [InputField.Autocomplete] is called to update the drop-down's
   318  // contents.
   319  //
   320  // If no such callback is set (or nil is provided), the input field will be
   321  // updated with the selection any time the user navigates the autocomplete
   322  // drop-down list. So this function essentially gives you more control over the
   323  // autocomplete functionality.
   324  func (i *InputField) SetAutocompletedFunc(autocompleted func(text string, index int, source int) bool) *InputField {
   325  	i.autocompleted = autocompleted
   326  	return i
   327  }
   328  
   329  // Autocomplete invokes the autocomplete callback (if there is one). If the
   330  // length of the returned autocomplete entries slice is greater than 0, the
   331  // input field will present the user with a corresponding drop-down list the
   332  // next time the input field is drawn.
   333  //
   334  // It is safe to call this function from any goroutine. Note that the input
   335  // field is not redrawn automatically unless called from the main goroutine
   336  // (e.g. in response to events).
   337  func (i *InputField) Autocomplete() *InputField {
   338  	i.autocompleteListMutex.Lock()
   339  	defer i.autocompleteListMutex.Unlock()
   340  	if i.autocomplete == nil {
   341  		return i
   342  	}
   343  
   344  	// Do we have any autocomplete entries?
   345  	entries := i.autocomplete(i.text)
   346  	if len(entries) == 0 {
   347  		// No entries, no list.
   348  		i.autocompleteList = nil
   349  		return i
   350  	}
   351  
   352  	// Make a list if we have none.
   353  	if i.autocompleteList == nil {
   354  		i.autocompleteList = NewList()
   355  		i.autocompleteList.ShowSecondaryText(false).
   356  			SetMainTextStyle(i.autocompleteStyles.main).
   357  			SetSelectedStyle(i.autocompleteStyles.selected).
   358  			SetHighlightFullLine(true).
   359  			SetBackgroundColor(i.autocompleteStyles.background)
   360  	}
   361  
   362  	// Fill it with the entries.
   363  	currentEntry := -1
   364  	suffixLength := 9999 // I'm just waiting for the day somebody opens an issue with this number being too small.
   365  	i.autocompleteList.Clear()
   366  	for index, entry := range entries {
   367  		i.autocompleteList.AddItem(entry, "", 0, nil)
   368  		if strings.HasPrefix(entry, i.text) && len(entry)-len(i.text) < suffixLength {
   369  			currentEntry = index
   370  			suffixLength = len(i.text) - len(entry)
   371  		}
   372  	}
   373  
   374  	// Set the selection if we have one.
   375  	if currentEntry >= 0 {
   376  		i.autocompleteList.SetCurrentItem(currentEntry)
   377  	}
   378  
   379  	return i
   380  }
   381  
   382  // SetAcceptanceFunc sets a handler which may reject the last character that was
   383  // entered (by returning false).
   384  //
   385  // This package defines a number of variables prefixed with InputField which may
   386  // be used for common input (e.g. numbers, maximum text length).
   387  func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
   388  	i.accept = handler
   389  	return i
   390  }
   391  
   392  // SetChangedFunc sets a handler which is called whenever the text of the input
   393  // field has changed. It receives the current text (after the change).
   394  func (i *InputField) SetChangedFunc(handler func(text string)) *InputField {
   395  	i.changed = handler
   396  	return i
   397  }
   398  
   399  // SetDoneFunc sets a handler which is called when the user is done entering
   400  // text. The callback function is provided with the key that was pressed, which
   401  // is one of the following:
   402  //
   403  //   - KeyEnter: Done entering text.
   404  //   - KeyEscape: Abort text input.
   405  //   - KeyTab: Move to the next field.
   406  //   - KeyBacktab: Move to the previous field.
   407  func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) *InputField {
   408  	i.done = handler
   409  	return i
   410  }
   411  
   412  // SetFinishedFunc sets a callback invoked when the user leaves this form item.
   413  func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
   414  	i.finished = handler
   415  	return i
   416  }
   417  
   418  // Focus is called when this primitive receives focus.
   419  func (i *InputField) Focus(delegate func(p Primitive)) {
   420  	// If we're part of a form and this item is disabled, there's nothing the
   421  	// user can do here so we're finished.
   422  	if i.finished != nil && i.disabled {
   423  		i.finished(-1)
   424  		return
   425  	}
   426  
   427  	i.Box.Focus(delegate)
   428  }
   429  
   430  // Blur is called when this primitive loses focus.
   431  func (i *InputField) Blur() {
   432  	i.Box.Blur()
   433  	i.autocompleteList = nil // Hide the autocomplete drop-down.
   434  }
   435  
   436  // Draw draws this primitive onto the screen.
   437  func (i *InputField) Draw(screen tcell.Screen) {
   438  	i.Box.DrawForSubclass(screen, i)
   439  
   440  	// Prepare
   441  	x, y, width, height := i.GetInnerRect()
   442  	rightLimit := x + width
   443  	if height < 1 || rightLimit <= x {
   444  		return
   445  	}
   446  
   447  	// Draw label.
   448  	_, labelBg, _ := i.labelStyle.Decompose()
   449  	if i.labelWidth > 0 {
   450  		labelWidth := i.labelWidth
   451  		if labelWidth > width {
   452  			labelWidth = width
   453  		}
   454  		printWithStyle(screen, i.label, x, y, 0, labelWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
   455  		x += labelWidth
   456  	} else {
   457  		_, drawnWidth, _, _ := printWithStyle(screen, i.label, x, y, 0, width, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
   458  		x += drawnWidth
   459  	}
   460  
   461  	// Draw input area.
   462  	i.fieldX = x
   463  	fieldWidth := i.fieldWidth
   464  	text := i.text
   465  	inputStyle := i.fieldStyle
   466  	placeholder := text == "" && i.placeholder != ""
   467  	if placeholder {
   468  		inputStyle = i.placeholderStyle
   469  	}
   470  	_, inputBg, _ := inputStyle.Decompose()
   471  	if fieldWidth == 0 {
   472  		fieldWidth = math.MaxInt32
   473  	}
   474  	if rightLimit-x < fieldWidth {
   475  		fieldWidth = rightLimit - x
   476  	}
   477  	if i.disabled {
   478  		inputStyle = inputStyle.Background(i.backgroundColor)
   479  	}
   480  	if inputBg != tcell.ColorDefault {
   481  		for index := 0; index < fieldWidth; index++ {
   482  			screen.SetContent(x+index, y, ' ', nil, inputStyle)
   483  		}
   484  	}
   485  
   486  	// Text.
   487  	var cursorScreenPos int
   488  	if placeholder {
   489  		// Draw placeholder text.
   490  		printWithStyle(screen, Escape(i.placeholder), x, y, 0, fieldWidth, AlignLeft, i.placeholderStyle, true)
   491  		i.offset = 0
   492  	} else {
   493  		// Draw entered text.
   494  		if i.maskCharacter > 0 {
   495  			text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
   496  		}
   497  		if fieldWidth >= uniseg.StringWidth(text) {
   498  			// We have enough space for the full text.
   499  			printWithStyle(screen, Escape(text), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true)
   500  			i.offset = 0
   501  			iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
   502  				if textPos >= i.cursorPos {
   503  					return true
   504  				}
   505  				cursorScreenPos += screenWidth
   506  				return false
   507  			})
   508  		} else {
   509  			// The text doesn't fit. Where is the cursor?
   510  			if i.cursorPos < 0 {
   511  				i.cursorPos = 0
   512  			} else if i.cursorPos > len(text) {
   513  				i.cursorPos = len(text)
   514  			}
   515  			// Shift the text so the cursor is inside the field.
   516  			var shiftLeft int
   517  			if i.offset > i.cursorPos {
   518  				i.offset = i.cursorPos
   519  			} else if subWidth := uniseg.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 {
   520  				shiftLeft = subWidth - fieldWidth + 1
   521  			}
   522  			currentOffset := i.offset
   523  			iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
   524  				if textPos >= currentOffset {
   525  					if shiftLeft > 0 {
   526  						i.offset = textPos + textWidth
   527  						shiftLeft -= screenWidth
   528  					} else {
   529  						if textPos+textWidth > i.cursorPos {
   530  							return true
   531  						}
   532  						cursorScreenPos += screenWidth
   533  					}
   534  				}
   535  				return false
   536  			})
   537  			printWithStyle(screen, Escape(text[i.offset:]), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true)
   538  		}
   539  	}
   540  
   541  	// Draw autocomplete list.
   542  	i.autocompleteListMutex.Lock()
   543  	defer i.autocompleteListMutex.Unlock()
   544  	if i.autocompleteList != nil {
   545  		// How much space do we need?
   546  		lheight := i.autocompleteList.GetItemCount()
   547  		lwidth := 0
   548  		for index := 0; index < lheight; index++ {
   549  			entry, _ := i.autocompleteList.GetItemText(index)
   550  			width := TaggedStringWidth(entry)
   551  			if width > lwidth {
   552  				lwidth = width
   553  			}
   554  		}
   555  
   556  		// We prefer to drop down but if there is no space, maybe drop up?
   557  		lx := x
   558  		ly := y + 1
   559  		_, sheight := screen.Size()
   560  		if ly+lheight >= sheight && ly-2 > lheight-ly {
   561  			ly = y - lheight
   562  			if ly < 0 {
   563  				ly = 0
   564  			}
   565  		}
   566  		if ly+lheight >= sheight {
   567  			lheight = sheight - ly
   568  		}
   569  		i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
   570  		i.autocompleteList.Draw(screen)
   571  	}
   572  
   573  	// Set cursor.
   574  	if i.HasFocus() {
   575  		screen.ShowCursor(x+cursorScreenPos, y)
   576  	}
   577  }
   578  
   579  // InputHandler returns the handler for this primitive.
   580  func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   581  	return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
   582  		if i.disabled {
   583  			return
   584  		}
   585  
   586  		// Trigger changed events.
   587  		currentText := i.text
   588  		defer func() {
   589  			if i.text != currentText {
   590  				i.Autocomplete()
   591  				if i.changed != nil {
   592  					i.changed(i.text)
   593  				}
   594  			}
   595  		}()
   596  
   597  		// Movement functions.
   598  		home := func() { i.cursorPos = 0 }
   599  		end := func() { i.cursorPos = len(i.text) }
   600  		moveLeft := func() {
   601  			iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
   602  				i.cursorPos -= textWidth
   603  				return true
   604  			})
   605  		}
   606  		moveRight := func() {
   607  			iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
   608  				i.cursorPos += textWidth
   609  				return true
   610  			})
   611  		}
   612  		moveWordLeft := func() {
   613  			i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
   614  		}
   615  		moveWordRight := func() {
   616  			i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], ""))
   617  		}
   618  
   619  		// Add character function. Returns whether or not the rune character is
   620  		// accepted.
   621  		add := func(r rune) bool {
   622  			newText := i.text[:i.cursorPos] + string(r) + i.text[i.cursorPos:]
   623  			if i.accept != nil && !i.accept(newText, r) {
   624  				return false
   625  			}
   626  			i.text = newText
   627  			i.cursorPos += len(string(r))
   628  			return true
   629  		}
   630  
   631  		// Finish up.
   632  		finish := func(key tcell.Key) {
   633  			if i.done != nil {
   634  				i.done(key)
   635  			}
   636  			if i.finished != nil {
   637  				i.finished(key)
   638  			}
   639  		}
   640  
   641  		// If we have an autocomplete list, there are certain keys we will
   642  		// forward to it.
   643  		i.autocompleteListMutex.Lock()
   644  		defer i.autocompleteListMutex.Unlock()
   645  		if i.autocompleteList != nil {
   646  			i.autocompleteList.SetChangedFunc(nil)
   647  			switch key := event.Key(); key {
   648  			case tcell.KeyEscape: // Close the list.
   649  				i.autocompleteList = nil
   650  				return
   651  			case tcell.KeyEnter, tcell.KeyTab: // Intentional selection.
   652  				if i.autocompleted != nil {
   653  					index := i.autocompleteList.GetCurrentItem()
   654  					text, _ := i.autocompleteList.GetItemText(index)
   655  					source := AutocompletedEnter
   656  					if key == tcell.KeyTab {
   657  						source = AutocompletedTab
   658  					}
   659  					if i.autocompleted(stripTags(text), index, source) {
   660  						i.autocompleteList = nil
   661  						currentText = i.GetText()
   662  					}
   663  				} else {
   664  					i.autocompleteList = nil
   665  				}
   666  				return
   667  			case tcell.KeyDown, tcell.KeyUp, tcell.KeyPgDn, tcell.KeyPgUp:
   668  				i.autocompleteList.SetChangedFunc(func(index int, text, secondaryText string, shortcut rune) {
   669  					text = stripTags(text)
   670  					if i.autocompleted != nil {
   671  						if i.autocompleted(text, index, AutocompletedNavigate) {
   672  							i.autocompleteList = nil
   673  							currentText = i.GetText()
   674  						}
   675  					} else {
   676  						i.SetText(text)
   677  						currentText = stripTags(text) // We want to keep the autocomplete list open and unchanged.
   678  					}
   679  				})
   680  				i.autocompleteList.InputHandler()(event, setFocus)
   681  				return
   682  			}
   683  		}
   684  
   685  		// Process key event for the input field.
   686  		switch key := event.Key(); key {
   687  		case tcell.KeyRune: // Regular character.
   688  			if event.Modifiers()&tcell.ModAlt > 0 {
   689  				// We accept some Alt- key combinations.
   690  				switch event.Rune() {
   691  				case 'a': // Home.
   692  					home()
   693  				case 'e': // End.
   694  					end()
   695  				case 'b': // Move word left.
   696  					moveWordLeft()
   697  				case 'f': // Move word right.
   698  					moveWordRight()
   699  				default:
   700  					if !add(event.Rune()) {
   701  						return
   702  					}
   703  				}
   704  			} else {
   705  				// Other keys are simply accepted as regular characters.
   706  				if !add(event.Rune()) {
   707  					return
   708  				}
   709  			}
   710  		case tcell.KeyCtrlU: // Delete all.
   711  			i.text = ""
   712  			i.cursorPos = 0
   713  		case tcell.KeyCtrlK: // Delete until the end of the line.
   714  			i.text = i.text[:i.cursorPos]
   715  		case tcell.KeyCtrlW: // Delete last word.
   716  			lastWord := regexp.MustCompile(`\S+\s*$`)
   717  			newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:]
   718  			i.cursorPos -= len(i.text) - len(newText)
   719  			i.text = newText
   720  		case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
   721  			iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
   722  				i.text = i.text[:textPos] + i.text[textPos+textWidth:]
   723  				i.cursorPos -= textWidth
   724  				return true
   725  			})
   726  			if i.offset >= i.cursorPos {
   727  				i.offset = 0
   728  			}
   729  		case tcell.KeyDelete, tcell.KeyCtrlD: // Delete character after the cursor.
   730  			iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
   731  				i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
   732  				return true
   733  			})
   734  		case tcell.KeyLeft:
   735  			if event.Modifiers()&tcell.ModAlt > 0 {
   736  				moveWordLeft()
   737  			} else {
   738  				moveLeft()
   739  			}
   740  		case tcell.KeyCtrlB:
   741  			moveLeft()
   742  		case tcell.KeyRight:
   743  			if event.Modifiers()&tcell.ModAlt > 0 {
   744  				moveWordRight()
   745  			} else {
   746  				moveRight()
   747  			}
   748  		case tcell.KeyCtrlF:
   749  			moveRight()
   750  		case tcell.KeyHome, tcell.KeyCtrlA:
   751  			home()
   752  		case tcell.KeyEnd, tcell.KeyCtrlE:
   753  			end()
   754  		case tcell.KeyDown:
   755  			i.autocompleteListMutex.Unlock() // We're still holding a lock.
   756  			i.Autocomplete()
   757  			i.autocompleteListMutex.Lock()
   758  		case tcell.KeyEnter, tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
   759  			finish(key)
   760  		}
   761  	})
   762  }
   763  
   764  // MouseHandler returns the mouse handler for this primitive.
   765  func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   766  	return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   767  		if i.disabled {
   768  			return false, nil
   769  		}
   770  
   771  		currentText := i.GetText()
   772  		defer func() {
   773  			if i.GetText() != currentText {
   774  				i.Autocomplete()
   775  				if i.changed != nil {
   776  					i.changed(i.text)
   777  				}
   778  			}
   779  		}()
   780  
   781  		// If we have an autocomplete list, forward the mouse event to it.
   782  		i.autocompleteListMutex.Lock()
   783  		defer i.autocompleteListMutex.Unlock()
   784  		if i.autocompleteList != nil {
   785  			i.autocompleteList.SetChangedFunc(func(index int, text, secondaryText string, shortcut rune) {
   786  				text = stripTags(text)
   787  				if i.autocompleted != nil {
   788  					if i.autocompleted(text, index, AutocompletedClick) {
   789  						i.autocompleteList = nil
   790  						currentText = i.GetText()
   791  					}
   792  					return
   793  				}
   794  				i.SetText(text)
   795  				i.autocompleteList = nil
   796  			})
   797  			if consumed, _ = i.autocompleteList.MouseHandler()(action, event, setFocus); consumed {
   798  				setFocus(i)
   799  				return
   800  			}
   801  		}
   802  
   803  		// Is mouse event within the input field?
   804  		x, y := event.Position()
   805  		_, rectY, _, _ := i.GetInnerRect()
   806  		if !i.InRect(x, y) {
   807  			return false, nil
   808  		}
   809  
   810  		// Process mouse event.
   811  		if y == rectY {
   812  			if action == MouseLeftDown {
   813  				setFocus(i)
   814  				consumed = true
   815  			} else if action == MouseLeftClick {
   816  				// Determine where to place the cursor.
   817  				if x >= i.fieldX {
   818  					if !iterateString(i.text[i.offset:], func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth, boundaries int) bool {
   819  						if x-i.fieldX < screenPos+screenWidth {
   820  							i.cursorPos = textPos + i.offset
   821  							return true
   822  						}
   823  						return false
   824  					}) {
   825  						i.cursorPos = len(i.text)
   826  					}
   827  				}
   828  				consumed = true
   829  			}
   830  		}
   831  
   832  		return
   833  	})
   834  }
   835  

View as plain text