...

Source file src/codeberg.org/tslocum/cview/dropdown.go

Documentation: codeberg.org/tslocum/cview

     1  package cview
     2  
     3  import (
     4  	"strings"
     5  	"sync"
     6  
     7  	"github.com/gdamore/tcell/v3"
     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  	if index >= 0 && index < len(d.options) {
   232  		d.currentOption = index
   233  		d.list.SetCurrentItem(index)
   234  		if d.selected != nil {
   235  			d.Unlock()
   236  			d.selected(index, d.options[index])
   237  			d.Lock()
   238  		}
   239  		if d.options[index].selected != nil {
   240  			d.Unlock()
   241  			d.options[index].selected(index, d.options[index])
   242  			d.Lock()
   243  		}
   244  	} else {
   245  		d.currentOption = -1
   246  		d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
   247  		if d.selected != nil {
   248  			d.Unlock()
   249  			d.selected(-1, nil)
   250  			d.Lock()
   251  		}
   252  	}
   253  	d.Unlock()
   254  }
   255  
   256  // GetCurrentOption returns the index of the currently selected option as well
   257  // as the option itself. If no option was selected, -1 and nil is returned.
   258  func (d *DropDown) GetCurrentOption() (int, *DropDownOption) {
   259  	d.RLock()
   260  	defer d.RUnlock()
   261  
   262  	var option *DropDownOption
   263  	if d.currentOption >= 0 && d.currentOption < len(d.options) {
   264  		option = d.options[d.currentOption]
   265  	}
   266  	return d.currentOption, option
   267  }
   268  
   269  // SetTextOptions sets the text to be placed before and after each drop-down
   270  // option (prefix/suffix), the text placed before and after the currently
   271  // selected option (currentPrefix/currentSuffix) as well as the text to be
   272  // displayed when no option is currently selected. Per default, all of these
   273  // strings are empty.
   274  func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) {
   275  	d.Lock()
   276  	defer d.Unlock()
   277  
   278  	d.currentOptionPrefix = currentPrefix
   279  	d.currentOptionSuffix = currentSuffix
   280  	d.noSelection = noSelection
   281  	d.optionPrefix = prefix
   282  	d.optionSuffix = suffix
   283  	for index := 0; index < d.list.GetItemCount(); index++ {
   284  		d.list.SetItemText(index, prefix+d.options[index].text+suffix, "")
   285  	}
   286  }
   287  
   288  // SetLabel sets the text to be displayed before the input area.
   289  func (d *DropDown) SetLabel(label string) {
   290  	d.Lock()
   291  	defer d.Unlock()
   292  
   293  	d.label = label
   294  }
   295  
   296  // GetLabel returns the text to be displayed before the input area.
   297  func (d *DropDown) GetLabel() string {
   298  	d.RLock()
   299  	defer d.RUnlock()
   300  
   301  	return d.label
   302  }
   303  
   304  // SetLabelWidth sets the screen width of the label. A value of 0 will cause the
   305  // primitive to use the width of the label string.
   306  func (d *DropDown) SetLabelWidth(width int) {
   307  	d.Lock()
   308  	defer d.Unlock()
   309  
   310  	d.labelWidth = width
   311  }
   312  
   313  // SetLabelColor sets the color of the label.
   314  func (d *DropDown) SetLabelColor(color tcell.Color) {
   315  	d.Lock()
   316  	defer d.Unlock()
   317  
   318  	d.labelColor = color
   319  }
   320  
   321  // SetLabelColorFocused sets the color of the label when focused.
   322  func (d *DropDown) SetLabelColorFocused(color tcell.Color) {
   323  	d.Lock()
   324  	defer d.Unlock()
   325  
   326  	d.labelColorFocused = color
   327  }
   328  
   329  // SetFieldBackgroundColor sets the background color of the options area.
   330  func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) {
   331  	d.Lock()
   332  	defer d.Unlock()
   333  
   334  	d.fieldBackgroundColor = color
   335  }
   336  
   337  // SetFieldBackgroundColorFocused sets the background color of the options area when focused.
   338  func (d *DropDown) SetFieldBackgroundColorFocused(color tcell.Color) {
   339  	d.Lock()
   340  	defer d.Unlock()
   341  
   342  	d.fieldBackgroundColorFocused = color
   343  }
   344  
   345  // SetFieldTextColor sets the text color of the options area.
   346  func (d *DropDown) SetFieldTextColor(color tcell.Color) {
   347  	d.Lock()
   348  	defer d.Unlock()
   349  
   350  	d.fieldTextColor = color
   351  }
   352  
   353  // SetFieldTextColorFocused sets the text color of the options area when focused.
   354  func (d *DropDown) SetFieldTextColorFocused(color tcell.Color) {
   355  	d.Lock()
   356  	defer d.Unlock()
   357  
   358  	d.fieldTextColorFocused = color
   359  }
   360  
   361  // SetDropDownTextColor sets text color of the drop-down list.
   362  func (d *DropDown) SetDropDownTextColor(color tcell.Color) {
   363  	d.Lock()
   364  	defer d.Unlock()
   365  
   366  	d.list.SetMainTextColor(color)
   367  }
   368  
   369  // SetDropDownBackgroundColor sets the background color of the drop-down list.
   370  func (d *DropDown) SetDropDownBackgroundColor(color tcell.Color) {
   371  	d.Lock()
   372  	defer d.Unlock()
   373  
   374  	d.list.SetBackgroundColor(color)
   375  }
   376  
   377  // SetDropDownSelectedTextColor sets the text color of the selected option in
   378  // the drop-down list.
   379  func (d *DropDown) SetDropDownSelectedTextColor(color tcell.Color) {
   380  	d.Lock()
   381  	defer d.Unlock()
   382  
   383  	d.list.SetSelectedTextColor(color)
   384  }
   385  
   386  // SetDropDownSelectedBackgroundColor sets the background color of the selected
   387  // option in the drop-down list.
   388  func (d *DropDown) SetDropDownSelectedBackgroundColor(color tcell.Color) {
   389  	d.Lock()
   390  	defer d.Unlock()
   391  
   392  	d.list.SetSelectedBackgroundColor(color)
   393  }
   394  
   395  // SetPrefixTextColor sets the color of the prefix string. The prefix string is
   396  // shown when the user starts typing text, which directly selects the first
   397  // option that starts with the typed string.
   398  func (d *DropDown) SetPrefixTextColor(color tcell.Color) {
   399  	d.Lock()
   400  	defer d.Unlock()
   401  
   402  	d.prefixTextColor = color
   403  }
   404  
   405  // SetFieldWidth sets the screen width of the options area. A value of 0 means
   406  // extend to as long as the longest option text.
   407  func (d *DropDown) SetFieldWidth(width int) {
   408  	d.Lock()
   409  	defer d.Unlock()
   410  
   411  	d.fieldWidth = width
   412  }
   413  
   414  // GetFieldHeight returns the height of the field.
   415  func (d *DropDown) GetFieldHeight() int {
   416  	return 1
   417  }
   418  
   419  // GetFieldWidth returns this primitive's field screen width.
   420  func (d *DropDown) GetFieldWidth() int {
   421  	d.RLock()
   422  	defer d.RUnlock()
   423  	return d.getFieldWidth()
   424  }
   425  
   426  func (d *DropDown) getFieldWidth() int {
   427  	if d.fieldWidth > 0 {
   428  		return d.fieldWidth
   429  	}
   430  	fieldWidth := 0
   431  	for _, option := range d.options {
   432  		width := TaggedStringWidth(option.text)
   433  		if width > fieldWidth {
   434  			fieldWidth = width
   435  		}
   436  	}
   437  	fieldWidth += len(d.optionPrefix) + len(d.optionSuffix)
   438  	fieldWidth += len(d.currentOptionPrefix) + len(d.currentOptionSuffix)
   439  	// space <text> space + dropDownSymbol + space
   440  	fieldWidth += 4
   441  	return fieldWidth
   442  }
   443  
   444  // AddOptionsSimple adds new selectable options to this drop-down.
   445  func (d *DropDown) AddOptionsSimple(options ...string) {
   446  	optionsToAdd := make([]*DropDownOption, len(options))
   447  	for i, option := range options {
   448  		optionsToAdd[i] = NewDropDownOption(option)
   449  	}
   450  	d.AddOptions(optionsToAdd...)
   451  }
   452  
   453  // AddOptions adds new selectable options to this drop-down.
   454  func (d *DropDown) AddOptions(options ...*DropDownOption) {
   455  	d.Lock()
   456  	defer d.Unlock()
   457  	d.addOptions(options...)
   458  }
   459  
   460  func (d *DropDown) addOptions(options ...*DropDownOption) {
   461  	d.options = append(d.options, options...)
   462  	for _, option := range options {
   463  		d.list.AddItem(NewListItem(d.optionPrefix + option.text + d.optionSuffix))
   464  	}
   465  }
   466  
   467  // SetOptionsSimple replaces all current options with the ones provided and installs
   468  // one callback function which is called when one of the options is selected.
   469  // It will be called with the option's index and the option itself
   470  // The "selected" parameter may be nil.
   471  func (d *DropDown) SetOptionsSimple(selected func(index int, option *DropDownOption), options ...string) {
   472  	optionsToSet := make([]*DropDownOption, len(options))
   473  	for i, option := range options {
   474  		optionsToSet[i] = NewDropDownOption(option)
   475  	}
   476  	d.SetOptions(selected, optionsToSet...)
   477  }
   478  
   479  // SetOptions replaces all current options with the ones provided and installs
   480  // one callback function which is called when one of the options is selected.
   481  // It will be called with the option's index and the option itself.
   482  // The "selected" parameter may be nil.
   483  func (d *DropDown) SetOptions(selected func(index int, option *DropDownOption), options ...*DropDownOption) {
   484  	d.Lock()
   485  	defer d.Unlock()
   486  
   487  	d.list.Clear()
   488  	d.options = nil
   489  	d.addOptions(options...)
   490  	d.selected = selected
   491  }
   492  
   493  // SetChangedFunc sets a handler which is called when the user changes the
   494  // focused drop-down option. The handler is provided with the selected option's
   495  // index and the option itself. If "no option" was selected, these values are
   496  // -1 and nil.
   497  func (d *DropDown) SetChangedFunc(handler func(index int, option *DropDownOption)) {
   498  	d.list.SetChangedFunc(func(index int, item *ListItem) {
   499  		handler(index, d.options[index])
   500  	})
   501  }
   502  
   503  // SetSelectedFunc sets a handler which is called when the user selects a
   504  // drop-down's option. This handler will be called in addition and prior to
   505  // an option's optional individual handler. The handler is provided with the
   506  // selected option's index and the option itself. If "no option" was selected, these values
   507  // are -1 and nil.
   508  func (d *DropDown) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
   509  	d.Lock()
   510  	defer d.Unlock()
   511  
   512  	d.selected = handler
   513  }
   514  
   515  // SetDoneFunc sets a handler which is called when the user is done selecting
   516  // options. The callback function is provided with the key that was pressed,
   517  // which is one of the following:
   518  //
   519  //   - KeyEscape: Abort selection.
   520  //   - KeyTab: Move to the next field.
   521  //   - KeyBacktab: Move to the previous field.
   522  func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) {
   523  	d.Lock()
   524  	defer d.Unlock()
   525  
   526  	d.done = handler
   527  }
   528  
   529  // SetFinishedFunc sets a callback invoked when the user leaves this form item.
   530  func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) {
   531  	d.Lock()
   532  	defer d.Unlock()
   533  
   534  	d.finished = handler
   535  }
   536  
   537  // Draw draws this primitive onto the screen.
   538  func (d *DropDown) Draw(screen tcell.Screen) {
   539  	d.Box.Draw(screen)
   540  	hasFocus := d.GetFocusable().HasFocus()
   541  
   542  	d.Lock()
   543  	defer d.Unlock()
   544  
   545  	// Select colors
   546  	labelColor := d.labelColor
   547  	fieldBackgroundColor := d.fieldBackgroundColor
   548  	fieldTextColor := d.fieldTextColor
   549  	if hasFocus {
   550  		if d.labelColorFocused != ColorUnset {
   551  			labelColor = d.labelColorFocused
   552  		}
   553  		if d.fieldBackgroundColorFocused != ColorUnset {
   554  			fieldBackgroundColor = d.fieldBackgroundColorFocused
   555  		}
   556  		if d.fieldTextColorFocused != ColorUnset {
   557  			fieldTextColor = d.fieldTextColorFocused
   558  		}
   559  	}
   560  
   561  	// Prepare.
   562  	x, y, width, height := d.GetInnerRect()
   563  	rightLimit := x + width
   564  	if height < 1 || rightLimit <= x {
   565  		return
   566  	}
   567  
   568  	// Draw label.
   569  	if d.labelWidth > 0 {
   570  		labelWidth := d.labelWidth
   571  		if labelWidth > rightLimit-x {
   572  			labelWidth = rightLimit - x
   573  		}
   574  		Print(screen, []byte(d.label), x, y, labelWidth, AlignLeft, labelColor)
   575  		x += labelWidth
   576  	} else {
   577  		_, drawnWidth := Print(screen, []byte(d.label), x, y, rightLimit-x, AlignLeft, labelColor)
   578  		x += drawnWidth
   579  	}
   580  
   581  	// What's the longest option text?
   582  	maxWidth := 0
   583  	optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
   584  	for _, option := range d.options {
   585  		strWidth := TaggedStringWidth(option.text) + optionWrapWidth
   586  		if strWidth > maxWidth {
   587  			maxWidth = strWidth
   588  		}
   589  	}
   590  
   591  	// Draw selection area.
   592  	fieldWidth := d.getFieldWidth()
   593  	if fieldWidth == 0 {
   594  		fieldWidth = maxWidth
   595  		if d.currentOption < 0 {
   596  			noSelectionWidth := TaggedStringWidth(d.noSelection)
   597  			if noSelectionWidth > fieldWidth {
   598  				fieldWidth = noSelectionWidth
   599  			}
   600  		} else if d.currentOption < len(d.options) {
   601  			currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix)
   602  			if currentOptionWidth > fieldWidth {
   603  				fieldWidth = currentOptionWidth
   604  			}
   605  		}
   606  	}
   607  	if rightLimit-x < fieldWidth {
   608  		fieldWidth = rightLimit - x
   609  	}
   610  	fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
   611  	for index := 0; index < fieldWidth; index++ {
   612  		screen.Put(x+index, y, " ", fieldStyle)
   613  	}
   614  
   615  	// Draw selected text.
   616  	if d.open && len(d.prefix) > 0 {
   617  		// Show the prefix.
   618  		currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix)
   619  		prefixWidth := runewidth.StringWidth(d.prefix)
   620  		listItemText := d.options[d.list.GetCurrentItemIndex()].text
   621  		Print(screen, []byte(d.currentOptionPrefix), x, y, fieldWidth, AlignLeft, fieldTextColor)
   622  		Print(screen, []byte(d.prefix), x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor)
   623  		if len(d.prefix) < len(listItemText) {
   624  			Print(screen, []byte(listItemText[len(d.prefix):]+d.currentOptionSuffix), x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor)
   625  		}
   626  	} else {
   627  		color := fieldTextColor
   628  		text := d.noSelection
   629  		if d.currentOption >= 0 && d.currentOption < len(d.options) {
   630  			text = d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix
   631  		}
   632  		// Abbreviate text when not fitting
   633  		if fieldWidth > len(d.abbreviationChars)+3 && len(text) > fieldWidth {
   634  			text = text[0:fieldWidth-3-len(d.abbreviationChars)] + d.abbreviationChars
   635  		}
   636  
   637  		// Just show the current selection.
   638  		Print(screen, []byte(text), x, y, fieldWidth, AlignLeft, color)
   639  	}
   640  
   641  	// Draw drop-down symbol
   642  	if d.alwaysDrawDropDownSymbol || d._hasFocus() {
   643  		symbol := d.dropDownSymbol
   644  		if d.open {
   645  			symbol = d.dropDownOpenSymbol
   646  		}
   647  		screen.Put(x+fieldWidth-2, y, string(symbol), new(tcell.Style).Foreground(fieldTextColor).Background(fieldBackgroundColor))
   648  	}
   649  
   650  	// Draw options list.
   651  	if hasFocus && d.open {
   652  		// We prefer to drop-down but if there is no space, maybe drop up?
   653  		lx := x
   654  		ly := y + 1
   655  		lheight := len(d.options)
   656  		_, sheight := screen.Size()
   657  		if ly+lheight >= sheight && ly-2 > lheight-ly {
   658  			ly = y - lheight
   659  			if ly < 0 {
   660  				ly = 0
   661  			}
   662  		}
   663  		if ly+lheight >= sheight {
   664  			lheight = sheight - ly
   665  		}
   666  		lwidth := maxWidth
   667  		if d.list.scrollBarVisibility == ScrollBarAlways || (d.list.scrollBarVisibility == ScrollBarAuto && len(d.options) > lheight) {
   668  			lwidth++ // Add space for scroll bar
   669  		}
   670  		if lwidth < fieldWidth {
   671  			lwidth = fieldWidth
   672  		}
   673  		d.list.SetRect(lx, ly, lwidth, lheight)
   674  		d.list.Draw(screen)
   675  	}
   676  }
   677  
   678  // InputHandler returns the handler for this primitive.
   679  func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   680  	return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
   681  		// Process key event.
   682  		switch key := event.Key(); key {
   683  		case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
   684  			d.Lock()
   685  			defer d.Unlock()
   686  
   687  			d.prefix = ""
   688  
   689  			// If the first key was a letter already, it becomes part of the prefix.
   690  			if str := event.Str(); key == tcell.KeyRune && str != " " {
   691  				d.prefix += str
   692  				d.evalPrefix()
   693  			}
   694  
   695  			d.openList(setFocus)
   696  		case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
   697  			if d.done != nil {
   698  				d.done(key)
   699  			}
   700  			if d.finished != nil {
   701  				d.finished(key)
   702  			}
   703  		}
   704  	})
   705  }
   706  
   707  // evalPrefix selects an item in the drop-down list based on the current prefix.
   708  func (d *DropDown) evalPrefix() {
   709  	if len(d.prefix) > 0 {
   710  		for index, option := range d.options {
   711  			if strings.HasPrefix(strings.ToLower(option.text), d.prefix) {
   712  				d.list.SetCurrentItem(index)
   713  				return
   714  			}
   715  		}
   716  
   717  		// Prefix does not match any item. Remove last rune.
   718  		r := []rune(d.prefix)
   719  		d.prefix = string(r[:len(r)-1])
   720  	}
   721  }
   722  
   723  // openList hands control over to the embedded List primitive.
   724  func (d *DropDown) openList(setFocus func(Primitive)) {
   725  	d.open = true
   726  	optionBefore := d.currentOption
   727  
   728  	d.list.SetSelectedFunc(func(index int, item *ListItem) {
   729  		if d.dragging {
   730  			return // If we're dragging the mouse, we don't want to trigger any events.
   731  		}
   732  
   733  		// An option was selected. Close the list again.
   734  		d.currentOption = index
   735  		d.closeList(setFocus)
   736  
   737  		// Trigger "selected" event.
   738  		if d.selected != nil {
   739  			d.selected(d.currentOption, d.options[d.currentOption])
   740  		}
   741  		if d.options[d.currentOption].selected != nil {
   742  			d.options[d.currentOption].selected(d.currentOption, d.options[d.currentOption])
   743  		}
   744  	})
   745  	d.list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
   746  		if event.Key() == tcell.KeyRune {
   747  			d.prefix += event.Str()
   748  			d.evalPrefix()
   749  		} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
   750  			if len(d.prefix) > 0 {
   751  				r := []rune(d.prefix)
   752  				d.prefix = string(r[:len(r)-1])
   753  			}
   754  			d.evalPrefix()
   755  		} else if event.Key() == tcell.KeyEscape {
   756  			d.currentOption = optionBefore
   757  			d.list.SetCurrentItem(d.currentOption)
   758  			d.closeList(setFocus)
   759  			if d.selected != nil {
   760  				if d.currentOption > -1 {
   761  					d.selected(d.currentOption, d.options[d.currentOption])
   762  				}
   763  			}
   764  		} else {
   765  			d.prefix = ""
   766  		}
   767  
   768  		return event
   769  	})
   770  
   771  	setFocus(d.list)
   772  }
   773  
   774  // closeList closes the embedded List element by hiding it and removing focus
   775  // from it.
   776  func (d *DropDown) closeList(setFocus func(Primitive)) {
   777  	d.open = false
   778  	if d.list.HasFocus() {
   779  		setFocus(d)
   780  	}
   781  }
   782  
   783  // Focus is called by the application when the primitive receives focus.
   784  func (d *DropDown) Focus(delegate func(p Primitive)) {
   785  	d.Box.Focus(delegate)
   786  	if d.open {
   787  		delegate(d.list)
   788  	}
   789  }
   790  
   791  // HasFocus returns whether or not this primitive has focus.
   792  func (d *DropDown) HasFocus() bool {
   793  	d.RLock()
   794  	defer d.RUnlock()
   795  
   796  	return d._hasFocus()
   797  }
   798  
   799  func (d *DropDown) _hasFocus() bool {
   800  	if d.open {
   801  		return d.list.HasFocus()
   802  	}
   803  	return d.hasFocus
   804  }
   805  
   806  // MouseHandler returns the mouse handler for this primitive.
   807  func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   808  	return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   809  		// Was the mouse event in the drop-down box itself (or on its label)?
   810  		x, y := event.Position()
   811  		_, rectY, _, _ := d.GetInnerRect()
   812  		inRect := y == rectY
   813  		if !d.open && !inRect {
   814  			return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
   815  		}
   816  
   817  		// Handle dragging. Clicks are implicitly handled by this logic.
   818  		switch action {
   819  		case MouseLeftDown:
   820  			consumed = d.open || inRect
   821  			capture = d
   822  			if !d.open {
   823  				d.openList(setFocus)
   824  				d.dragging = true
   825  			} else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
   826  				d.closeList(setFocus) // Close drop-down if clicked outside of it.
   827  			}
   828  		case MouseMove:
   829  			if d.dragging {
   830  				// We pretend it's a left click so we can see the selection during
   831  				// dragging. Because we don't act upon it, it's not a problem.
   832  				d.list.MouseHandler()(MouseLeftClick, event, setFocus)
   833  				consumed = true
   834  				capture = d
   835  			}
   836  		case MouseLeftUp:
   837  			if d.dragging {
   838  				d.dragging = false
   839  				d.list.MouseHandler()(MouseLeftClick, event, setFocus)
   840  				consumed = true
   841  			}
   842  		}
   843  
   844  		return
   845  	})
   846  }
   847  

View as plain text