...

Source file src/code.rocketnine.space/tslocum/cview/dropdown.go

Documentation: code.rocketnine.space/tslocum/cview

     1  package cview
     2  
     3  import (
     4  	"strings"
     5  	"sync"
     6  
     7  	"github.com/gdamore/tcell/v2"
     8  	"github.com/mattn/go-runewidth"
     9  )
    10  
    11  // DropDownOption is one option that can be selected in a drop-down primitive.
    12  type DropDownOption struct {
    13  	text      string                                  // The text to be displayed in the drop-down.
    14  	selected  func(index int, option *DropDownOption) // The (optional) callback for when this option was selected.
    15  	reference interface{}                             // An optional reference object.
    16  
    17  	sync.RWMutex
    18  }
    19  
    20  // NewDropDownOption returns a new option for a dropdown.
    21  func NewDropDownOption(text string) *DropDownOption {
    22  	return &DropDownOption{text: text}
    23  }
    24  
    25  // GetText returns the text of this dropdown option.
    26  func (d *DropDownOption) GetText() string {
    27  	d.RLock()
    28  	defer d.RUnlock()
    29  
    30  	return d.text
    31  }
    32  
    33  // SetText returns the text of this dropdown option.
    34  func (d *DropDownOption) SetText(text string) {
    35  	d.text = text
    36  }
    37  
    38  // SetSelectedFunc sets the handler to be called when this option is selected.
    39  func (d *DropDownOption) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
    40  	d.selected = handler
    41  }
    42  
    43  // GetReference returns the reference object of this dropdown option.
    44  func (d *DropDownOption) GetReference() interface{} {
    45  	d.RLock()
    46  	defer d.RUnlock()
    47  
    48  	return d.reference
    49  }
    50  
    51  // SetReference allows you to store a reference of any type in this option.
    52  func (d *DropDownOption) SetReference(reference interface{}) {
    53  	d.reference = reference
    54  }
    55  
    56  // DropDown implements a selection widget whose options become visible in a
    57  // drop-down list when activated.
    58  type DropDown struct {
    59  	*Box
    60  
    61  	// The options from which the user can choose.
    62  	options []*DropDownOption
    63  
    64  	// Strings to be placed before and after each drop-down option.
    65  	optionPrefix, optionSuffix string
    66  
    67  	// The index of the currently selected option. Negative if no option is
    68  	// currently selected.
    69  	currentOption int
    70  
    71  	// Strings to be placed before and after the current option.
    72  	currentOptionPrefix, currentOptionSuffix string
    73  
    74  	// The text to be displayed when no option has yet been selected.
    75  	noSelection string
    76  
    77  	// Set to true if the options are visible and selectable.
    78  	open bool
    79  
    80  	// The runes typed so far to directly access one of the list items.
    81  	prefix string
    82  
    83  	// The list element for the options.
    84  	list *List
    85  
    86  	// The text to be displayed before the input area.
    87  	label string
    88  
    89  	// The label color.
    90  	labelColor tcell.Color
    91  
    92  	// The label color when focused.
    93  	labelColorFocused tcell.Color
    94  
    95  	// The background color of the input area.
    96  	fieldBackgroundColor tcell.Color
    97  
    98  	// The background color of the input area when focused.
    99  	fieldBackgroundColorFocused tcell.Color
   100  
   101  	// The text color of the input area.
   102  	fieldTextColor tcell.Color
   103  
   104  	// The text color of the input area when focused.
   105  	fieldTextColorFocused tcell.Color
   106  
   107  	// The color for prefixes.
   108  	prefixTextColor tcell.Color
   109  
   110  	// The screen width of the label area. A value of 0 means use the width of
   111  	// the label text.
   112  	labelWidth int
   113  
   114  	// The screen width of the input area. A value of 0 means extend as much as
   115  	// possible.
   116  	fieldWidth int
   117  
   118  	// An optional function which is called when the user indicated that they
   119  	// are done selecting options. The key which was pressed is provided (tab,
   120  	// shift-tab, or escape).
   121  	done func(tcell.Key)
   122  
   123  	// A callback function set by the Form class and called when the user leaves
   124  	// this form item.
   125  	finished func(tcell.Key)
   126  
   127  	// A callback function which is called when the user changes the drop-down's
   128  	// selection.
   129  	selected func(index int, option *DropDownOption)
   130  
   131  	// Set to true when mouse dragging is in progress.
   132  	dragging bool
   133  
   134  	// The chars to show when the option's text gets shortened.
   135  	abbreviationChars string
   136  
   137  	// The symbol to draw at the end of the field when closed.
   138  	dropDownSymbol rune
   139  
   140  	// The symbol to draw at the end of the field when opened.
   141  	dropDownOpenSymbol rune
   142  
   143  	// The symbol used to draw the selected item when opened.
   144  	dropDownSelectedSymbol rune
   145  
   146  	// A flag that determines whether the drop down symbol is always drawn.
   147  	alwaysDrawDropDownSymbol bool
   148  
   149  	sync.RWMutex
   150  }
   151  
   152  // NewDropDown returns a new drop-down.
   153  func NewDropDown() *DropDown {
   154  	list := NewList()
   155  	list.ShowSecondaryText(false)
   156  	list.SetMainTextColor(Styles.SecondaryTextColor)
   157  	list.SetSelectedTextColor(Styles.PrimitiveBackgroundColor)
   158  	list.SetSelectedBackgroundColor(Styles.PrimaryTextColor)
   159  	list.SetHighlightFullLine(true)
   160  	list.SetBackgroundColor(Styles.ContrastBackgroundColor)
   161  
   162  	d := &DropDown{
   163  		Box:                         NewBox(),
   164  		currentOption:               -1,
   165  		list:                        list,
   166  		labelColor:                  Styles.SecondaryTextColor,
   167  		fieldBackgroundColor:        Styles.MoreContrastBackgroundColor,
   168  		fieldTextColor:              Styles.PrimaryTextColor,
   169  		prefixTextColor:             Styles.ContrastSecondaryTextColor,
   170  		dropDownSymbol:              Styles.DropDownSymbol,
   171  		dropDownOpenSymbol:          Styles.DropDownOpenSymbol,
   172  		dropDownSelectedSymbol:      Styles.DropDownSelectedSymbol,
   173  		abbreviationChars:           Styles.DropDownAbbreviationChars,
   174  		labelColorFocused:           ColorUnset,
   175  		fieldBackgroundColorFocused: ColorUnset,
   176  		fieldTextColorFocused:       ColorUnset,
   177  	}
   178  
   179  	if sym := d.dropDownSelectedSymbol; sym != 0 {
   180  		list.SetIndicators(" "+string(sym)+" ", "", "   ", "")
   181  	}
   182  	d.focus = d
   183  
   184  	return d
   185  }
   186  
   187  // SetDropDownSymbolRune sets the rune to be drawn at the end of the dropdown field
   188  // to indicate that this field is a dropdown.
   189  func (d *DropDown) SetDropDownSymbolRune(symbol rune) {
   190  	d.Lock()
   191  	defer d.Unlock()
   192  	d.dropDownSymbol = symbol
   193  }
   194  
   195  // SetDropDownOpenSymbolRune sets the rune to be drawn at the end of the
   196  // dropdown field to indicate that the a dropdown is open.
   197  func (d *DropDown) SetDropDownOpenSymbolRune(symbol rune) {
   198  	d.Lock()
   199  	defer d.Unlock()
   200  	d.dropDownOpenSymbol = symbol
   201  
   202  	if symbol != 0 {
   203  		d.list.SetIndicators(" "+string(symbol)+" ", "", "   ", "")
   204  	} else {
   205  		d.list.SetIndicators("", "", "", "")
   206  	}
   207  
   208  }
   209  
   210  // SetDropDownSelectedSymbolRune sets the rune to be drawn at the start of the
   211  // selected list item.
   212  func (d *DropDown) SetDropDownSelectedSymbolRune(symbol rune) {
   213  	d.Lock()
   214  	defer d.Unlock()
   215  	d.dropDownSelectedSymbol = symbol
   216  }
   217  
   218  // SetAlwaysDrawDropDownSymbol sets a flad that determines whether the drop
   219  // down symbol is always drawn. The symbol is normally only drawn when focused.
   220  func (d *DropDown) SetAlwaysDrawDropDownSymbol(alwaysDraw bool) {
   221  	d.Lock()
   222  	defer d.Unlock()
   223  	d.alwaysDrawDropDownSymbol = alwaysDraw
   224  }
   225  
   226  // SetCurrentOption sets the index of the currently selected option. This may
   227  // be a negative value to indicate that no option is currently selected. Calling
   228  // this function will also trigger the "selected" callback (if there is one).
   229  func (d *DropDown) SetCurrentOption(index int) {
   230  	d.Lock()
   231  	defer d.Unlock()
   232  
   233  	if index >= 0 && index < len(d.options) {
   234  		d.currentOption = index
   235  		d.list.SetCurrentItem(index)
   236  		if d.selected != nil {
   237  			d.Unlock()
   238  			d.selected(index, d.options[index])
   239  			d.Lock()
   240  		}
   241  		if d.options[index].selected != nil {
   242  			d.Unlock()
   243  			d.options[index].selected(index, d.options[index])
   244  			d.Lock()
   245  		}
   246  	} else {
   247  		d.currentOption = -1
   248  		d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
   249  		if d.selected != nil {
   250  			d.Unlock()
   251  			d.selected(-1, nil)
   252  			d.Lock()
   253  		}
   254  	}
   255  }
   256  
   257  // GetCurrentOption returns the index of the currently selected option as well
   258  // as the option itself. If no option was selected, -1 and nil is returned.
   259  func (d *DropDown) GetCurrentOption() (int, *DropDownOption) {
   260  	d.RLock()
   261  	defer d.RUnlock()
   262  
   263  	var option *DropDownOption
   264  	if d.currentOption >= 0 && d.currentOption < len(d.options) {
   265  		option = d.options[d.currentOption]
   266  	}
   267  	return d.currentOption, option
   268  }
   269  
   270  // SetTextOptions sets the text to be placed before and after each drop-down
   271  // option (prefix/suffix), the text placed before and after the currently
   272  // selected option (currentPrefix/currentSuffix) as well as the text to be
   273  // displayed when no option is currently selected. Per default, all of these
   274  // strings are empty.
   275  func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) {
   276  	d.Lock()
   277  	defer d.Unlock()
   278  
   279  	d.currentOptionPrefix = currentPrefix
   280  	d.currentOptionSuffix = currentSuffix
   281  	d.noSelection = noSelection
   282  	d.optionPrefix = prefix
   283  	d.optionSuffix = suffix
   284  	for index := 0; index < d.list.GetItemCount(); index++ {
   285  		d.list.SetItemText(index, prefix+d.options[index].text+suffix, "")
   286  	}
   287  }
   288  
   289  // SetLabel sets the text to be displayed before the input area.
   290  func (d *DropDown) SetLabel(label string) {
   291  	d.Lock()
   292  	defer d.Unlock()
   293  
   294  	d.label = label
   295  }
   296  
   297  // GetLabel returns the text to be displayed before the input area.
   298  func (d *DropDown) GetLabel() string {
   299  	d.RLock()
   300  	defer d.RUnlock()
   301  
   302  	return d.label
   303  }
   304  
   305  // SetLabelWidth sets the screen width of the label. A value of 0 will cause the
   306  // primitive to use the width of the label string.
   307  func (d *DropDown) SetLabelWidth(width int) {
   308  	d.Lock()
   309  	defer d.Unlock()
   310  
   311  	d.labelWidth = width
   312  }
   313  
   314  // SetLabelColor sets the color of the label.
   315  func (d *DropDown) SetLabelColor(color tcell.Color) {
   316  	d.Lock()
   317  	defer d.Unlock()
   318  
   319  	d.labelColor = color
   320  }
   321  
   322  // SetLabelColorFocused sets the color of the label when focused.
   323  func (d *DropDown) SetLabelColorFocused(color tcell.Color) {
   324  	d.Lock()
   325  	defer d.Unlock()
   326  
   327  	d.labelColorFocused = color
   328  }
   329  
   330  // SetFieldBackgroundColor sets the background color of the options area.
   331  func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) {
   332  	d.Lock()
   333  	defer d.Unlock()
   334  
   335  	d.fieldBackgroundColor = color
   336  }
   337  
   338  // SetFieldBackgroundColorFocused sets the background color of the options area when focused.
   339  func (d *DropDown) SetFieldBackgroundColorFocused(color tcell.Color) {
   340  	d.Lock()
   341  	defer d.Unlock()
   342  
   343  	d.fieldBackgroundColorFocused = color
   344  }
   345  
   346  // SetFieldTextColor sets the text color of the options area.
   347  func (d *DropDown) SetFieldTextColor(color tcell.Color) {
   348  	d.Lock()
   349  	defer d.Unlock()
   350  
   351  	d.fieldTextColor = color
   352  }
   353  
   354  // SetFieldTextColorFocused sets the text color of the options area when focused.
   355  func (d *DropDown) SetFieldTextColorFocused(color tcell.Color) {
   356  	d.Lock()
   357  	defer d.Unlock()
   358  
   359  	d.fieldTextColorFocused = color
   360  }
   361  
   362  // SetDropDownTextColor sets text color of the drop-down list.
   363  func (d *DropDown) SetDropDownTextColor(color tcell.Color) {
   364  	d.Lock()
   365  	defer d.Unlock()
   366  
   367  	d.list.SetMainTextColor(color)
   368  }
   369  
   370  // SetDropDownBackgroundColor sets the background color of the drop-down list.
   371  func (d *DropDown) SetDropDownBackgroundColor(color tcell.Color) {
   372  	d.Lock()
   373  	defer d.Unlock()
   374  
   375  	d.list.SetBackgroundColor(color)
   376  }
   377  
   378  // SetDropDownSelectedTextColor sets the text color of the selected option in
   379  // the drop-down list.
   380  func (d *DropDown) SetDropDownSelectedTextColor(color tcell.Color) {
   381  	d.Lock()
   382  	defer d.Unlock()
   383  
   384  	d.list.SetSelectedTextColor(color)
   385  }
   386  
   387  // SetDropDownSelectedBackgroundColor sets the background color of the selected
   388  // option in the drop-down list.
   389  func (d *DropDown) SetDropDownSelectedBackgroundColor(color tcell.Color) {
   390  	d.Lock()
   391  	defer d.Unlock()
   392  
   393  	d.list.SetSelectedBackgroundColor(color)
   394  }
   395  
   396  // SetPrefixTextColor sets the color of the prefix string. The prefix string is
   397  // shown when the user starts typing text, which directly selects the first
   398  // option that starts with the typed string.
   399  func (d *DropDown) SetPrefixTextColor(color tcell.Color) {
   400  	d.Lock()
   401  	defer d.Unlock()
   402  
   403  	d.prefixTextColor = color
   404  }
   405  
   406  // SetFieldWidth sets the screen width of the options area. A value of 0 means
   407  // extend to as long as the longest option text.
   408  func (d *DropDown) SetFieldWidth(width int) {
   409  	d.Lock()
   410  	defer d.Unlock()
   411  
   412  	d.fieldWidth = width
   413  }
   414  
   415  // GetFieldHeight returns the height of the field.
   416  func (d *DropDown) GetFieldHeight() int {
   417  	return 1
   418  }
   419  
   420  // GetFieldWidth returns this primitive's field screen width.
   421  func (d *DropDown) GetFieldWidth() int {
   422  	d.RLock()
   423  	defer d.RUnlock()
   424  	return d.getFieldWidth()
   425  }
   426  
   427  func (d *DropDown) getFieldWidth() int {
   428  	if d.fieldWidth > 0 {
   429  		return d.fieldWidth
   430  	}
   431  	fieldWidth := 0
   432  	for _, option := range d.options {
   433  		width := TaggedStringWidth(option.text)
   434  		if width > fieldWidth {
   435  			fieldWidth = width
   436  		}
   437  	}
   438  	fieldWidth += len(d.optionPrefix) + len(d.optionSuffix)
   439  	fieldWidth += len(d.currentOptionPrefix) + len(d.currentOptionSuffix)
   440  	// space <text> space + dropDownSymbol + space
   441  	fieldWidth += 4
   442  	return fieldWidth
   443  }
   444  
   445  // AddOptionsSimple adds new selectable options to this drop-down.
   446  func (d *DropDown) AddOptionsSimple(options ...string) {
   447  	optionsToAdd := make([]*DropDownOption, len(options))
   448  	for i, option := range options {
   449  		optionsToAdd[i] = NewDropDownOption(option)
   450  	}
   451  	d.AddOptions(optionsToAdd...)
   452  }
   453  
   454  // AddOptions adds new selectable options to this drop-down.
   455  func (d *DropDown) AddOptions(options ...*DropDownOption) {
   456  	d.Lock()
   457  	defer d.Unlock()
   458  	d.addOptions(options...)
   459  }
   460  
   461  func (d *DropDown) addOptions(options ...*DropDownOption) {
   462  	d.options = append(d.options, options...)
   463  	for _, option := range options {
   464  		d.list.AddItem(NewListItem(d.optionPrefix + option.text + d.optionSuffix))
   465  	}
   466  }
   467  
   468  // SetOptionsSimple replaces all current options with the ones provided and installs
   469  // one callback function which is called when one of the options is selected.
   470  // It will be called with the option's index and the option itself
   471  // The "selected" parameter may be nil.
   472  func (d *DropDown) SetOptionsSimple(selected func(index int, option *DropDownOption), options ...string) {
   473  	optionsToSet := make([]*DropDownOption, len(options))
   474  	for i, option := range options {
   475  		optionsToSet[i] = NewDropDownOption(option)
   476  	}
   477  	d.SetOptions(selected, optionsToSet...)
   478  }
   479  
   480  // SetOptions replaces all current options with the ones provided and installs
   481  // one callback function which is called when one of the options is selected.
   482  // It will be called with the option's index and the option itself.
   483  // The "selected" parameter may be nil.
   484  func (d *DropDown) SetOptions(selected func(index int, option *DropDownOption), options ...*DropDownOption) {
   485  	d.Lock()
   486  	defer d.Unlock()
   487  
   488  	d.list.Clear()
   489  	d.options = nil
   490  	d.addOptions(options...)
   491  	d.selected = selected
   492  }
   493  
   494  // SetChangedFunc sets a handler which is called when the user changes the
   495  // focused drop-down option. The handler is provided with the selected option's
   496  // index and the option itself. If "no option" was selected, these values are
   497  // -1 and nil.
   498  func (d *DropDown) SetChangedFunc(handler func(index int, option *DropDownOption)) {
   499  	d.list.SetChangedFunc(func(index int, item *ListItem) {
   500  		handler(index, d.options[index])
   501  	})
   502  }
   503  
   504  // SetSelectedFunc sets a handler which is called when the user selects a
   505  // drop-down's option. This handler will be called in addition and prior to
   506  // an option's optional individual handler. The handler is provided with the
   507  // selected option's index and the option itself. If "no option" was selected, these values
   508  // are -1 and nil.
   509  func (d *DropDown) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
   510  	d.Lock()
   511  	defer d.Unlock()
   512  
   513  	d.selected = handler
   514  }
   515  
   516  // SetDoneFunc sets a handler which is called when the user is done selecting
   517  // options. The callback function is provided with the key that was pressed,
   518  // which is one of the following:
   519  //
   520  //   - KeyEscape: Abort selection.
   521  //   - KeyTab: Move to the next field.
   522  //   - KeyBacktab: Move to the previous field.
   523  func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) {
   524  	d.Lock()
   525  	defer d.Unlock()
   526  
   527  	d.done = handler
   528  }
   529  
   530  // SetFinishedFunc sets a callback invoked when the user leaves this form item.
   531  func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) {
   532  	d.Lock()
   533  	defer d.Unlock()
   534  
   535  	d.finished = handler
   536  }
   537  
   538  // Draw draws this primitive onto the screen.
   539  func (d *DropDown) Draw(screen tcell.Screen) {
   540  	d.Box.Draw(screen)
   541  	hasFocus := d.GetFocusable().HasFocus()
   542  
   543  	d.Lock()
   544  	defer d.Unlock()
   545  
   546  	// Select colors
   547  	labelColor := d.labelColor
   548  	fieldBackgroundColor := d.fieldBackgroundColor
   549  	fieldTextColor := d.fieldTextColor
   550  	if hasFocus {
   551  		if d.labelColorFocused != ColorUnset {
   552  			labelColor = d.labelColorFocused
   553  		}
   554  		if d.fieldBackgroundColorFocused != ColorUnset {
   555  			fieldBackgroundColor = d.fieldBackgroundColorFocused
   556  		}
   557  		if d.fieldTextColorFocused != ColorUnset {
   558  			fieldTextColor = d.fieldTextColorFocused
   559  		}
   560  	}
   561  
   562  	// Prepare.
   563  	x, y, width, height := d.GetInnerRect()
   564  	rightLimit := x + width
   565  	if height < 1 || rightLimit <= x {
   566  		return
   567  	}
   568  
   569  	// Draw label.
   570  	if d.labelWidth > 0 {
   571  		labelWidth := d.labelWidth
   572  		if labelWidth > rightLimit-x {
   573  			labelWidth = rightLimit - x
   574  		}
   575  		Print(screen, []byte(d.label), x, y, labelWidth, AlignLeft, labelColor)
   576  		x += labelWidth
   577  	} else {
   578  		_, drawnWidth := Print(screen, []byte(d.label), x, y, rightLimit-x, AlignLeft, labelColor)
   579  		x += drawnWidth
   580  	}
   581  
   582  	// What's the longest option text?
   583  	maxWidth := 0
   584  	optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
   585  	for _, option := range d.options {
   586  		strWidth := TaggedStringWidth(option.text) + optionWrapWidth
   587  		if strWidth > maxWidth {
   588  			maxWidth = strWidth
   589  		}
   590  	}
   591  
   592  	// Draw selection area.
   593  	fieldWidth := d.getFieldWidth()
   594  	if fieldWidth == 0 {
   595  		fieldWidth = maxWidth
   596  		if d.currentOption < 0 {
   597  			noSelectionWidth := TaggedStringWidth(d.noSelection)
   598  			if noSelectionWidth > fieldWidth {
   599  				fieldWidth = noSelectionWidth
   600  			}
   601  		} else if d.currentOption < len(d.options) {
   602  			currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix)
   603  			if currentOptionWidth > fieldWidth {
   604  				fieldWidth = currentOptionWidth
   605  			}
   606  		}
   607  	}
   608  	if rightLimit-x < fieldWidth {
   609  		fieldWidth = rightLimit - x
   610  	}
   611  	fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
   612  	for index := 0; index < fieldWidth; index++ {
   613  		screen.SetContent(x+index, y, ' ', nil, fieldStyle)
   614  	}
   615  
   616  	// Draw selected text.
   617  	if d.open && len(d.prefix) > 0 {
   618  		// Show the prefix.
   619  		currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix)
   620  		prefixWidth := runewidth.StringWidth(d.prefix)
   621  		listItemText := d.options[d.list.GetCurrentItemIndex()].text
   622  		Print(screen, []byte(d.currentOptionPrefix), x, y, fieldWidth, AlignLeft, fieldTextColor)
   623  		Print(screen, []byte(d.prefix), x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor)
   624  		if len(d.prefix) < len(listItemText) {
   625  			Print(screen, []byte(listItemText[len(d.prefix):]+d.currentOptionSuffix), x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor)
   626  		}
   627  	} else {
   628  		color := fieldTextColor
   629  		text := d.noSelection
   630  		if d.currentOption >= 0 && d.currentOption < len(d.options) {
   631  			text = d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix
   632  		}
   633  		// Abbreviate text when not fitting
   634  		if fieldWidth > len(d.abbreviationChars)+3 && len(text) > fieldWidth {
   635  			text = text[0:fieldWidth-3-len(d.abbreviationChars)] + d.abbreviationChars
   636  		}
   637  
   638  		// Just show the current selection.
   639  		Print(screen, []byte(text), x, y, fieldWidth, AlignLeft, color)
   640  	}
   641  
   642  	// Draw drop-down symbol
   643  	if d.alwaysDrawDropDownSymbol || d._hasFocus() {
   644  		symbol := d.dropDownSymbol
   645  		if d.open {
   646  			symbol = d.dropDownOpenSymbol
   647  		}
   648  		screen.SetContent(x+fieldWidth-2, y, symbol, nil, new(tcell.Style).Foreground(fieldTextColor).Background(fieldBackgroundColor))
   649  	}
   650  
   651  	// Draw options list.
   652  	if hasFocus && d.open {
   653  		// We prefer to drop-down but if there is no space, maybe drop up?
   654  		lx := x
   655  		ly := y + 1
   656  		lheight := len(d.options)
   657  		_, sheight := screen.Size()
   658  		if ly+lheight >= sheight && ly-2 > lheight-ly {
   659  			ly = y - lheight
   660  			if ly < 0 {
   661  				ly = 0
   662  			}
   663  		}
   664  		if ly+lheight >= sheight {
   665  			lheight = sheight - ly
   666  		}
   667  		lwidth := maxWidth
   668  		if d.list.scrollBarVisibility == ScrollBarAlways || (d.list.scrollBarVisibility == ScrollBarAuto && len(d.options) > lheight) {
   669  			lwidth++ // Add space for scroll bar
   670  		}
   671  		if lwidth < fieldWidth {
   672  			lwidth = fieldWidth
   673  		}
   674  		d.list.SetRect(lx, ly, lwidth, lheight)
   675  		d.list.Draw(screen)
   676  	}
   677  }
   678  
   679  // InputHandler returns the handler for this primitive.
   680  func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   681  	return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
   682  		// Process key event.
   683  		switch key := event.Key(); key {
   684  		case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
   685  			d.Lock()
   686  			defer d.Unlock()
   687  
   688  			d.prefix = ""
   689  
   690  			// If the first key was a letter already, it becomes part of the prefix.
   691  			if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
   692  				d.prefix += string(r)
   693  				d.evalPrefix()
   694  			}
   695  
   696  			d.openList(setFocus)
   697  		case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
   698  			if d.done != nil {
   699  				d.done(key)
   700  			}
   701  			if d.finished != nil {
   702  				d.finished(key)
   703  			}
   704  		}
   705  	})
   706  }
   707  
   708  // evalPrefix selects an item in the drop-down list based on the current prefix.
   709  func (d *DropDown) evalPrefix() {
   710  	if len(d.prefix) > 0 {
   711  		for index, option := range d.options {
   712  			if strings.HasPrefix(strings.ToLower(option.text), d.prefix) {
   713  				d.list.SetCurrentItem(index)
   714  				return
   715  			}
   716  		}
   717  
   718  		// Prefix does not match any item. Remove last rune.
   719  		r := []rune(d.prefix)
   720  		d.prefix = string(r[:len(r)-1])
   721  	}
   722  }
   723  
   724  // openList hands control over to the embedded List primitive.
   725  func (d *DropDown) openList(setFocus func(Primitive)) {
   726  	d.open = true
   727  	optionBefore := d.currentOption
   728  
   729  	d.list.SetSelectedFunc(func(index int, item *ListItem) {
   730  		if d.dragging {
   731  			return // If we're dragging the mouse, we don't want to trigger any events.
   732  		}
   733  
   734  		// An option was selected. Close the list again.
   735  		d.currentOption = index
   736  		d.closeList(setFocus)
   737  
   738  		// Trigger "selected" event.
   739  		if d.selected != nil {
   740  			d.selected(d.currentOption, d.options[d.currentOption])
   741  		}
   742  		if d.options[d.currentOption].selected != nil {
   743  			d.options[d.currentOption].selected(d.currentOption, d.options[d.currentOption])
   744  		}
   745  	})
   746  	d.list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
   747  		if event.Key() == tcell.KeyRune {
   748  			d.prefix += string(event.Rune())
   749  			d.evalPrefix()
   750  		} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
   751  			if len(d.prefix) > 0 {
   752  				r := []rune(d.prefix)
   753  				d.prefix = string(r[:len(r)-1])
   754  			}
   755  			d.evalPrefix()
   756  		} else if event.Key() == tcell.KeyEscape {
   757  			d.currentOption = optionBefore
   758  			d.list.SetCurrentItem(d.currentOption)
   759  			d.closeList(setFocus)
   760  			if d.selected != nil {
   761  				if d.currentOption > -1 {
   762  					d.selected(d.currentOption, d.options[d.currentOption])
   763  				}
   764  			}
   765  		} else {
   766  			d.prefix = ""
   767  		}
   768  
   769  		return event
   770  	})
   771  
   772  	setFocus(d.list)
   773  }
   774  
   775  // closeList closes the embedded List element by hiding it and removing focus
   776  // from it.
   777  func (d *DropDown) closeList(setFocus func(Primitive)) {
   778  	d.open = false
   779  	if d.list.HasFocus() {
   780  		setFocus(d)
   781  	}
   782  }
   783  
   784  // Focus is called by the application when the primitive receives focus.
   785  func (d *DropDown) Focus(delegate func(p Primitive)) {
   786  	d.Box.Focus(delegate)
   787  	if d.open {
   788  		delegate(d.list)
   789  	}
   790  }
   791  
   792  // HasFocus returns whether or not this primitive has focus.
   793  func (d *DropDown) HasFocus() bool {
   794  	d.RLock()
   795  	defer d.RUnlock()
   796  
   797  	return d._hasFocus()
   798  }
   799  
   800  func (d *DropDown) _hasFocus() bool {
   801  	if d.open {
   802  		return d.list.HasFocus()
   803  	}
   804  	return d.hasFocus
   805  }
   806  
   807  // MouseHandler returns the mouse handler for this primitive.
   808  func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   809  	return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   810  		// Was the mouse event in the drop-down box itself (or on its label)?
   811  		x, y := event.Position()
   812  		_, rectY, _, _ := d.GetInnerRect()
   813  		inRect := y == rectY
   814  		if !d.open && !inRect {
   815  			return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
   816  		}
   817  
   818  		// Handle dragging. Clicks are implicitly handled by this logic.
   819  		switch action {
   820  		case MouseLeftDown:
   821  			consumed = d.open || inRect
   822  			capture = d
   823  			if !d.open {
   824  				d.openList(setFocus)
   825  				d.dragging = true
   826  			} else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
   827  				d.closeList(setFocus) // Close drop-down if clicked outside of it.
   828  			}
   829  		case MouseMove:
   830  			if d.dragging {
   831  				// We pretend it's a left click so we can see the selection during
   832  				// dragging. Because we don't act upon it, it's not a problem.
   833  				d.list.MouseHandler()(MouseLeftClick, event, setFocus)
   834  				consumed = true
   835  				capture = d
   836  			}
   837  		case MouseLeftUp:
   838  			if d.dragging {
   839  				d.dragging = false
   840  				d.list.MouseHandler()(MouseLeftClick, event, setFocus)
   841  				consumed = true
   842  			}
   843  		}
   844  
   845  		return
   846  	})
   847  }
   848  

View as plain text