...

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

Documentation: github.com/rivo/tview

     1  package tview
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/gdamore/tcell/v2"
     7  	"github.com/rivo/uniseg"
     8  )
     9  
    10  // dropDownOption is one option that can be selected in a drop-down primitive.
    11  type dropDownOption struct {
    12  	Text     string // The text to be displayed in the drop-down.
    13  	Selected func() // The (optional) callback for when this option was selected.
    14  }
    15  
    16  // DropDown implements a selection widget whose options become visible in a
    17  // drop-down list when activated.
    18  //
    19  // See https://github.com/rivo/tview/wiki/DropDown for an example.
    20  type DropDown struct {
    21  	*Box
    22  
    23  	// Whether or not this drop-down is disabled/read-only.
    24  	disabled bool
    25  
    26  	// The options from which the user can choose.
    27  	options []*dropDownOption
    28  
    29  	// Strings to be placed before and after each drop-down option.
    30  	optionPrefix, optionSuffix string
    31  
    32  	// The index of the currently selected option. Negative if no option is
    33  	// currently selected.
    34  	currentOption int
    35  
    36  	// Strings to be placed before and after the current option.
    37  	currentOptionPrefix, currentOptionSuffix string
    38  
    39  	// The text to be displayed when no option has yet been selected.
    40  	noSelection string
    41  
    42  	// Set to true if the options are visible and selectable.
    43  	open bool
    44  
    45  	// The runes typed so far to directly access one of the list items.
    46  	prefix string
    47  
    48  	// The list element for the options.
    49  	list *List
    50  
    51  	// The text to be displayed before the input area.
    52  	label string
    53  
    54  	// The label color.
    55  	labelColor tcell.Color
    56  
    57  	// The background color of the input area.
    58  	fieldBackgroundColor tcell.Color
    59  
    60  	// The text color of the input area.
    61  	fieldTextColor tcell.Color
    62  
    63  	// The color for prefixes.
    64  	prefixTextColor tcell.Color
    65  
    66  	// The screen width of the label area. A value of 0 means use the width of
    67  	// the label text.
    68  	labelWidth int
    69  
    70  	// The screen width of the input area. A value of 0 means extend as much as
    71  	// possible.
    72  	fieldWidth int
    73  
    74  	// An optional function which is called when the user indicated that they
    75  	// are done selecting options. The key which was pressed is provided (tab,
    76  	// shift-tab, or escape).
    77  	done func(tcell.Key)
    78  
    79  	// A callback function set by the Form class and called when the user leaves
    80  	// this form item.
    81  	finished func(tcell.Key)
    82  
    83  	// A callback function which is called when the user changes the drop-down's
    84  	// selection.
    85  	selected func(text string, index int)
    86  
    87  	dragging bool // Set to true when mouse dragging is in progress.
    88  }
    89  
    90  // NewDropDown returns a new drop-down.
    91  func NewDropDown() *DropDown {
    92  	list := NewList()
    93  	list.ShowSecondaryText(false).
    94  		SetMainTextColor(Styles.PrimitiveBackgroundColor).
    95  		SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
    96  		SetSelectedBackgroundColor(Styles.PrimaryTextColor).
    97  		SetHighlightFullLine(true).
    98  		SetBackgroundColor(Styles.MoreContrastBackgroundColor)
    99  
   100  	d := &DropDown{
   101  		Box:                  NewBox(),
   102  		currentOption:        -1,
   103  		list:                 list,
   104  		labelColor:           Styles.SecondaryTextColor,
   105  		fieldBackgroundColor: Styles.ContrastBackgroundColor,
   106  		fieldTextColor:       Styles.PrimaryTextColor,
   107  		prefixTextColor:      Styles.ContrastSecondaryTextColor,
   108  	}
   109  
   110  	return d
   111  }
   112  
   113  // SetCurrentOption sets the index of the currently selected option. This may
   114  // be a negative value to indicate that no option is currently selected. Calling
   115  // this function will also trigger the "selected" callback (if there is one).
   116  func (d *DropDown) SetCurrentOption(index int) *DropDown {
   117  	if index >= 0 && index < len(d.options) {
   118  		d.currentOption = index
   119  		d.list.SetCurrentItem(index)
   120  		if d.selected != nil {
   121  			d.selected(d.options[index].Text, index)
   122  		}
   123  		if d.options[index].Selected != nil {
   124  			d.options[index].Selected()
   125  		}
   126  	} else {
   127  		d.currentOption = -1
   128  		d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
   129  		if d.selected != nil {
   130  			d.selected("", -1)
   131  		}
   132  	}
   133  	return d
   134  }
   135  
   136  // GetCurrentOption returns the index of the currently selected option as well
   137  // as its text. If no option was selected, -1 and an empty string is returned.
   138  func (d *DropDown) GetCurrentOption() (int, string) {
   139  	var text string
   140  	if d.currentOption >= 0 && d.currentOption < len(d.options) {
   141  		text = d.options[d.currentOption].Text
   142  	}
   143  	return d.currentOption, text
   144  }
   145  
   146  // SetTextOptions sets the text to be placed before and after each drop-down
   147  // option (prefix/suffix), the text placed before and after the currently
   148  // selected option (currentPrefix/currentSuffix) as well as the text to be
   149  // displayed when no option is currently selected. Per default, all of these
   150  // strings are empty.
   151  func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) *DropDown {
   152  	d.currentOptionPrefix = currentPrefix
   153  	d.currentOptionSuffix = currentSuffix
   154  	d.noSelection = noSelection
   155  	d.optionPrefix = prefix
   156  	d.optionSuffix = suffix
   157  	for index := 0; index < d.list.GetItemCount(); index++ {
   158  		d.list.SetItemText(index, prefix+d.options[index].Text+suffix, "")
   159  	}
   160  	return d
   161  }
   162  
   163  // SetLabel sets the text to be displayed before the input area.
   164  func (d *DropDown) SetLabel(label string) *DropDown {
   165  	d.label = label
   166  	return d
   167  }
   168  
   169  // GetLabel returns the text to be displayed before the input area.
   170  func (d *DropDown) GetLabel() string {
   171  	return d.label
   172  }
   173  
   174  // SetLabelWidth sets the screen width of the label. A value of 0 will cause the
   175  // primitive to use the width of the label string.
   176  func (d *DropDown) SetLabelWidth(width int) *DropDown {
   177  	d.labelWidth = width
   178  	return d
   179  }
   180  
   181  // SetLabelColor sets the color of the label.
   182  func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown {
   183  	d.labelColor = color
   184  	return d
   185  }
   186  
   187  // SetFieldBackgroundColor sets the background color of the options area.
   188  func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
   189  	d.fieldBackgroundColor = color
   190  	return d
   191  }
   192  
   193  // SetFieldTextColor sets the text color of the options area.
   194  func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
   195  	d.fieldTextColor = color
   196  	return d
   197  }
   198  
   199  // SetPrefixTextColor sets the color of the prefix string. The prefix string is
   200  // shown when the user starts typing text, which directly selects the first
   201  // option that starts with the typed string.
   202  func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
   203  	d.prefixTextColor = color
   204  	return d
   205  }
   206  
   207  // SetListStyles sets the styles of the items in the drop-down list (unselected
   208  // as well as selected items). Style attributes are currently ignored but may be
   209  // used in the future.
   210  func (d *DropDown) SetListStyles(unselected, selected tcell.Style) *DropDown {
   211  	fg, bg, _ := unselected.Decompose()
   212  	d.list.SetMainTextColor(fg).SetBackgroundColor(bg)
   213  	fg, bg, _ = selected.Decompose()
   214  	d.list.SetSelectedTextColor(fg).SetSelectedBackgroundColor(bg)
   215  	return d
   216  }
   217  
   218  // SetFormAttributes sets attributes shared by all form items.
   219  func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
   220  	d.labelWidth = labelWidth
   221  	d.labelColor = labelColor
   222  	d.backgroundColor = bgColor
   223  	d.fieldTextColor = fieldTextColor
   224  	d.fieldBackgroundColor = fieldBgColor
   225  	return d
   226  }
   227  
   228  // SetFieldWidth sets the screen width of the options area. A value of 0 means
   229  // extend to as long as the longest option text.
   230  func (d *DropDown) SetFieldWidth(width int) *DropDown {
   231  	d.fieldWidth = width
   232  	return d
   233  }
   234  
   235  // GetFieldWidth returns this primitive's field screen width.
   236  func (d *DropDown) GetFieldWidth() int {
   237  	if d.fieldWidth > 0 {
   238  		return d.fieldWidth
   239  	}
   240  	fieldWidth := 0
   241  	for _, option := range d.options {
   242  		width := TaggedStringWidth(option.Text)
   243  		if width > fieldWidth {
   244  			fieldWidth = width
   245  		}
   246  	}
   247  	return fieldWidth
   248  }
   249  
   250  // GetFieldHeight returns this primitive's field height.
   251  func (d *DropDown) GetFieldHeight() int {
   252  	return 1
   253  }
   254  
   255  // SetDisabled sets whether or not the item is disabled / read-only.
   256  func (d *DropDown) SetDisabled(disabled bool) FormItem {
   257  	d.disabled = disabled
   258  	if d.finished != nil {
   259  		d.finished(-1)
   260  	}
   261  	return d
   262  }
   263  
   264  // AddOption adds a new selectable option to this drop-down. The "selected"
   265  // callback is called when this option was selected. It may be nil.
   266  func (d *DropDown) AddOption(text string, selected func()) *DropDown {
   267  	d.options = append(d.options, &dropDownOption{Text: text, Selected: selected})
   268  	d.list.AddItem(d.optionPrefix+text+d.optionSuffix, "", 0, nil)
   269  	return d
   270  }
   271  
   272  // SetOptions replaces all current options with the ones provided and installs
   273  // one callback function which is called when one of the options is selected.
   274  // It will be called with the option's text and its index into the options
   275  // slice. The "selected" parameter may be nil.
   276  func (d *DropDown) SetOptions(texts []string, selected func(text string, index int)) *DropDown {
   277  	d.list.Clear()
   278  	d.options = nil
   279  	for index, text := range texts {
   280  		func(t string, i int) {
   281  			d.AddOption(text, nil)
   282  		}(text, index)
   283  	}
   284  	d.selected = selected
   285  	return d
   286  }
   287  
   288  // GetOptionCount returns the number of options in the drop-down.
   289  func (d *DropDown) GetOptionCount() int {
   290  	return len(d.options)
   291  }
   292  
   293  // RemoveOption removes the specified option from the drop-down. Panics if the
   294  // index is out of range.
   295  func (d *DropDown) RemoveOption(index int) *DropDown {
   296  	d.options = append(d.options[:index], d.options[index+1:]...)
   297  	d.list.RemoveItem(index)
   298  	return d
   299  }
   300  
   301  // SetSelectedFunc sets a handler which is called when the user changes the
   302  // drop-down's option. This handler will be called in addition and prior to
   303  // an option's optional individual handler. The handler is provided with the
   304  // selected option's text and index. If "no option" was selected, these values
   305  // are an empty string and -1.
   306  func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown {
   307  	d.selected = handler
   308  	return d
   309  }
   310  
   311  // SetDoneFunc sets a handler which is called when the user is done selecting
   312  // options. The callback function is provided with the key that was pressed,
   313  // which is one of the following:
   314  //
   315  //   - KeyEscape: Abort selection.
   316  //   - KeyTab: Move to the next field.
   317  //   - KeyBacktab: Move to the previous field.
   318  func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown {
   319  	d.done = handler
   320  	return d
   321  }
   322  
   323  // SetFinishedFunc sets a callback invoked when the user leaves this form item.
   324  func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
   325  	d.finished = handler
   326  	return d
   327  }
   328  
   329  // Draw draws this primitive onto the screen.
   330  func (d *DropDown) Draw(screen tcell.Screen) {
   331  	d.Box.DrawForSubclass(screen, d)
   332  
   333  	// Prepare.
   334  	x, y, width, height := d.GetInnerRect()
   335  	rightLimit := x + width
   336  	if height < 1 || rightLimit <= x {
   337  		return
   338  	}
   339  
   340  	// Draw label.
   341  	if d.labelWidth > 0 {
   342  		labelWidth := d.labelWidth
   343  		if labelWidth > rightLimit-x {
   344  			labelWidth = rightLimit - x
   345  		}
   346  		Print(screen, d.label, x, y, labelWidth, AlignLeft, d.labelColor)
   347  		x += labelWidth
   348  	} else {
   349  		_, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor)
   350  		x += drawnWidth
   351  	}
   352  
   353  	// What's the longest option text?
   354  	maxWidth := 0
   355  	optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
   356  	for _, option := range d.options {
   357  		strWidth := TaggedStringWidth(option.Text) + optionWrapWidth
   358  		if strWidth > maxWidth {
   359  			maxWidth = strWidth
   360  		}
   361  	}
   362  
   363  	// Draw selection area.
   364  	fieldWidth := d.fieldWidth
   365  	if fieldWidth == 0 {
   366  		fieldWidth = maxWidth
   367  		if d.currentOption < 0 {
   368  			noSelectionWidth := TaggedStringWidth(d.noSelection)
   369  			if noSelectionWidth > fieldWidth {
   370  				fieldWidth = noSelectionWidth
   371  			}
   372  		} else if d.currentOption < len(d.options) {
   373  			currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix)
   374  			if currentOptionWidth > fieldWidth {
   375  				fieldWidth = currentOptionWidth
   376  			}
   377  		}
   378  	}
   379  	if rightLimit-x < fieldWidth {
   380  		fieldWidth = rightLimit - x
   381  	}
   382  	fieldStyle := tcell.StyleDefault.Background(d.fieldBackgroundColor)
   383  	if d.HasFocus() && !d.open {
   384  		fieldStyle = fieldStyle.Background(d.fieldTextColor)
   385  	}
   386  	if d.disabled {
   387  		fieldStyle = fieldStyle.Background(d.backgroundColor)
   388  	}
   389  	for index := 0; index < fieldWidth; index++ {
   390  		screen.SetContent(x+index, y, ' ', nil, fieldStyle)
   391  	}
   392  
   393  	// Draw selected text.
   394  	if d.open && len(d.prefix) > 0 {
   395  		// Show the prefix.
   396  		currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix)
   397  		prefixWidth := uniseg.StringWidth(d.prefix)
   398  		listItemText := d.options[d.list.GetCurrentItem()].Text
   399  		Print(screen, d.currentOptionPrefix, x, y, fieldWidth, AlignLeft, d.fieldTextColor)
   400  		Print(screen, d.prefix, x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor)
   401  		if len(d.prefix) < len(listItemText) {
   402  			Print(screen, listItemText[len(d.prefix):]+d.currentOptionSuffix, x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, d.fieldTextColor)
   403  		}
   404  	} else {
   405  		color := d.fieldTextColor
   406  		text := d.noSelection
   407  		if d.currentOption >= 0 && d.currentOption < len(d.options) {
   408  			text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix
   409  		}
   410  		// Just show the current selection.
   411  		if d.HasFocus() && !d.open && !d.disabled {
   412  			color = d.fieldBackgroundColor
   413  		}
   414  		Print(screen, text, x, y, fieldWidth, AlignLeft, color)
   415  	}
   416  
   417  	// Draw options list.
   418  	if d.HasFocus() && d.open {
   419  		lx := x
   420  		ly := y + 1
   421  		lwidth := maxWidth
   422  		lheight := len(d.options)
   423  		swidth, sheight := screen.Size()
   424  		// We prefer to align the left sides of the list and the main widget, but
   425  		// if there is no space to the right, then shift the list to the left.
   426  		if lx+lwidth >= swidth {
   427  			lx = swidth - lwidth
   428  			if lx < 0 {
   429  				lx = 0
   430  			}
   431  		}
   432  		// We prefer to drop down but if there is no space, maybe drop up?
   433  		if ly+lheight >= sheight && ly-2 > lheight-ly {
   434  			ly = y - lheight
   435  			if ly < 0 {
   436  				ly = 0
   437  			}
   438  		}
   439  		if ly+lheight >= sheight {
   440  			lheight = sheight - ly
   441  		}
   442  		d.list.SetRect(lx, ly, lwidth, lheight)
   443  		d.list.Draw(screen)
   444  	}
   445  }
   446  
   447  // InputHandler returns the handler for this primitive.
   448  func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   449  	return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
   450  		if d.disabled {
   451  			return
   452  		}
   453  
   454  		// If the list has focus, let it process its own key events.
   455  		if d.list.HasFocus() {
   456  			if handler := d.list.InputHandler(); handler != nil {
   457  				handler(event, setFocus)
   458  			}
   459  			return
   460  		}
   461  
   462  		// Process key event.
   463  		switch key := event.Key(); key {
   464  		case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
   465  			d.prefix = ""
   466  
   467  			// If the first key was a letter already, it becomes part of the prefix.
   468  			if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
   469  				d.prefix += string(r)
   470  				d.evalPrefix()
   471  			}
   472  
   473  			d.openList(setFocus)
   474  		case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
   475  			if d.done != nil {
   476  				d.done(key)
   477  			}
   478  			if d.finished != nil {
   479  				d.finished(key)
   480  			}
   481  		}
   482  	})
   483  }
   484  
   485  // evalPrefix selects an item in the drop-down list based on the current prefix.
   486  func (d *DropDown) evalPrefix() {
   487  	if len(d.prefix) > 0 {
   488  		for index, option := range d.options {
   489  			if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) {
   490  				d.list.SetCurrentItem(index)
   491  				return
   492  			}
   493  		}
   494  
   495  		// Prefix does not match any item. Remove last rune.
   496  		r := []rune(d.prefix)
   497  		d.prefix = string(r[:len(r)-1])
   498  	}
   499  }
   500  
   501  // openList hands control over to the embedded List primitive.
   502  func (d *DropDown) openList(setFocus func(Primitive)) {
   503  	d.open = true
   504  	optionBefore := d.currentOption
   505  
   506  	d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
   507  		if d.dragging {
   508  			return // If we're dragging the mouse, we don't want to trigger any events.
   509  		}
   510  
   511  		// An option was selected. Close the list again.
   512  		d.currentOption = index
   513  		d.closeList(setFocus)
   514  
   515  		// Trigger "selected" event.
   516  		if d.selected != nil {
   517  			d.selected(d.options[d.currentOption].Text, d.currentOption)
   518  		}
   519  		if d.options[d.currentOption].Selected != nil {
   520  			d.options[d.currentOption].Selected()
   521  		}
   522  	}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
   523  		if event.Key() == tcell.KeyRune {
   524  			d.prefix += string(event.Rune())
   525  			d.evalPrefix()
   526  		} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
   527  			if len(d.prefix) > 0 {
   528  				r := []rune(d.prefix)
   529  				d.prefix = string(r[:len(r)-1])
   530  			}
   531  			d.evalPrefix()
   532  		} else if event.Key() == tcell.KeyEscape {
   533  			d.currentOption = optionBefore
   534  			d.closeList(setFocus)
   535  		} else {
   536  			d.prefix = ""
   537  		}
   538  
   539  		return event
   540  	})
   541  
   542  	setFocus(d.list)
   543  }
   544  
   545  // closeList closes the embedded List element by hiding it and removing focus
   546  // from it.
   547  func (d *DropDown) closeList(setFocus func(Primitive)) {
   548  	d.open = false
   549  	if d.list.HasFocus() {
   550  		setFocus(d)
   551  	}
   552  }
   553  
   554  // IsOpen returns true if the drop-down list is currently open.
   555  func (d *DropDown) IsOpen() bool {
   556  	return d.open
   557  }
   558  
   559  // Focus is called by the application when the primitive receives focus.
   560  func (d *DropDown) Focus(delegate func(p Primitive)) {
   561  	// If we're part of a form and this item is disabled, there's nothing the
   562  	// user can do here so we're finished.
   563  	if d.finished != nil && d.disabled {
   564  		d.finished(-1)
   565  		return
   566  	}
   567  
   568  	if d.open {
   569  		delegate(d.list)
   570  	} else {
   571  		d.Box.Focus(delegate)
   572  	}
   573  }
   574  
   575  // HasFocus returns whether or not this primitive has focus.
   576  func (d *DropDown) HasFocus() bool {
   577  	if d.open {
   578  		return d.list.HasFocus()
   579  	}
   580  	return d.Box.HasFocus()
   581  }
   582  
   583  // MouseHandler returns the mouse handler for this primitive.
   584  func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   585  	return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   586  		if d.disabled {
   587  			return false, nil
   588  		}
   589  
   590  		// Was the mouse event in the drop-down box itself (or on its label)?
   591  		x, y := event.Position()
   592  		rectX, rectY, rectWidth, _ := d.GetInnerRect()
   593  		inRect := y == rectY && x >= rectX && x < rectX+rectWidth
   594  		if !d.open && !inRect {
   595  			return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
   596  		}
   597  
   598  		// As long as the drop-down is open, we capture all mouse events.
   599  		if d.open {
   600  			capture = d
   601  		}
   602  
   603  		switch action {
   604  		case MouseLeftDown:
   605  			consumed = d.open || inRect
   606  			capture = d
   607  			if !d.open {
   608  				d.openList(setFocus)
   609  				d.dragging = true
   610  			} else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
   611  				d.closeList(setFocus) // Close drop-down if clicked outside of it.
   612  			}
   613  		case MouseMove:
   614  			if d.dragging {
   615  				// We pretend it's a left click so we can see the selection during
   616  				// dragging. Because we don't act upon it, it's not a problem.
   617  				d.list.MouseHandler()(MouseLeftClick, event, setFocus)
   618  				consumed = true
   619  			}
   620  		case MouseLeftUp:
   621  			if d.dragging {
   622  				d.dragging = false
   623  				d.list.MouseHandler()(MouseLeftClick, event, setFocus)
   624  				consumed = true
   625  			}
   626  		}
   627  
   628  		return
   629  	})
   630  }
   631  

View as plain text