1package tview 2 3import ( 4 "strings" 5 6 "github.com/gdamore/tcell" 7) 8 9// dropDownOption is one option that can be selected in a drop-down primitive. 10type dropDownOption struct { 11 Text string // The text to be displayed in the drop-down. 12 Selected func() // The (optional) callback for when this option was selected. 13} 14 15// DropDown implements a selection widget whose options become visible in a 16// drop-down list when activated. 17// 18// See https://github.com/rivo/tview/wiki/DropDown for an example. 19type DropDown struct { 20 *Box 21 22 // The options from which the user can choose. 23 options []*dropDownOption 24 25 // Strings to be placed before and after each drop-down option. 26 optionPrefix, optionSuffix string 27 28 // The index of the currently selected option. Negative if no option is 29 // currently selected. 30 currentOption int 31 32 // Strings to be placed beefore and after the current option. 33 currentOptionPrefix, currentOptionSuffix string 34 35 // The text to be displayed when no option has yet been selected. 36 noSelection string 37 38 // Set to true if the options are visible and selectable. 39 open bool 40 41 // The runes typed so far to directly access one of the list items. 42 prefix string 43 44 // The list element for the options. 45 list *List 46 47 // The text to be displayed before the input area. 48 label string 49 50 // The label color. 51 labelColor tcell.Color 52 53 // The background color of the input area. 54 fieldBackgroundColor tcell.Color 55 56 // The text color of the input area. 57 fieldTextColor tcell.Color 58 59 // The color for prefixes. 60 prefixTextColor tcell.Color 61 62 // The screen width of the label area. A value of 0 means use the width of 63 // the label text. 64 labelWidth int 65 66 // The screen width of the input area. A value of 0 means extend as much as 67 // possible. 68 fieldWidth int 69 70 // An optional function which is called when the user indicated that they 71 // are done selecting options. The key which was pressed is provided (tab, 72 // shift-tab, or escape). 73 done func(tcell.Key) 74 75 // A callback function set by the Form class and called when the user leaves 76 // this form item. 77 finished func(tcell.Key) 78 79 // A callback function which is called when the user changes the drop-down's 80 // selection. 81 selected func(text string, index int) 82} 83 84// NewDropDown returns a new drop-down. 85func NewDropDown() *DropDown { 86 list := NewList() 87 list.ShowSecondaryText(false). 88 SetMainTextColor(Styles.PrimitiveBackgroundColor). 89 SetSelectedTextColor(Styles.PrimitiveBackgroundColor). 90 SetSelectedBackgroundColor(Styles.PrimaryTextColor). 91 SetHighlightFullLine(true). 92 SetBackgroundColor(Styles.MoreContrastBackgroundColor) 93 94 d := &DropDown{ 95 Box: NewBox(), 96 currentOption: -1, 97 list: list, 98 labelColor: Styles.SecondaryTextColor, 99 fieldBackgroundColor: Styles.ContrastBackgroundColor, 100 fieldTextColor: Styles.PrimaryTextColor, 101 prefixTextColor: Styles.ContrastSecondaryTextColor, 102 } 103 104 d.focus = d 105 106 return d 107} 108 109// SetCurrentOption sets the index of the currently selected option. This may 110// be a negative value to indicate that no option is currently selected. Calling 111// this function will also trigger the "selected" callback (if there is one). 112func (d *DropDown) SetCurrentOption(index int) *DropDown { 113 if index >= 0 && index < len(d.options) { 114 d.currentOption = index 115 d.list.SetCurrentItem(index) 116 if d.selected != nil { 117 d.selected(d.options[index].Text, index) 118 } 119 if d.options[index].Selected != nil { 120 d.options[index].Selected() 121 } 122 } else { 123 d.currentOption = -1 124 d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item". 125 if d.selected != nil { 126 d.selected("", -1) 127 } 128 } 129 return d 130} 131 132// GetCurrentOption returns the index of the currently selected option as well 133// as its text. If no option was selected, -1 and an empty string is returned. 134func (d *DropDown) GetCurrentOption() (int, string) { 135 var text string 136 if d.currentOption >= 0 && d.currentOption < len(d.options) { 137 text = d.options[d.currentOption].Text 138 } 139 return d.currentOption, text 140} 141 142// SetTextOptions sets the text to be placed before and after each drop-down 143// option (prefix/suffix), the text placed before and after the currently 144// selected option (currentPrefix/currentSuffix) as well as the text to be 145// displayed when no option is currently selected. Per default, all of these 146// strings are empty. 147func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) *DropDown { 148 d.currentOptionPrefix = currentPrefix 149 d.currentOptionSuffix = currentSuffix 150 d.noSelection = noSelection 151 d.optionPrefix = prefix 152 d.optionSuffix = suffix 153 for index := 0; index < d.list.GetItemCount(); index++ { 154 d.list.SetItemText(index, prefix+d.options[index].Text+suffix, "") 155 } 156 return d 157} 158 159// SetLabel sets the text to be displayed before the input area. 160func (d *DropDown) SetLabel(label string) *DropDown { 161 d.label = label 162 return d 163} 164 165// GetLabel returns the text to be displayed before the input area. 166func (d *DropDown) GetLabel() string { 167 return d.label 168} 169 170// SetLabelWidth sets the screen width of the label. A value of 0 will cause the 171// primitive to use the width of the label string. 172func (d *DropDown) SetLabelWidth(width int) *DropDown { 173 d.labelWidth = width 174 return d 175} 176 177// SetLabelColor sets the color of the label. 178func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown { 179 d.labelColor = color 180 return d 181} 182 183// SetFieldBackgroundColor sets the background color of the options area. 184func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown { 185 d.fieldBackgroundColor = color 186 return d 187} 188 189// SetFieldTextColor sets the text color of the options area. 190func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown { 191 d.fieldTextColor = color 192 return d 193} 194 195// SetPrefixTextColor sets the color of the prefix string. The prefix string is 196// shown when the user starts typing text, which directly selects the first 197// option that starts with the typed string. 198func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown { 199 d.prefixTextColor = color 200 return d 201} 202 203// SetFormAttributes sets attributes shared by all form items. 204func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { 205 d.labelWidth = labelWidth 206 d.labelColor = labelColor 207 d.backgroundColor = bgColor 208 d.fieldTextColor = fieldTextColor 209 d.fieldBackgroundColor = fieldBgColor 210 return d 211} 212 213// SetFieldWidth sets the screen width of the options area. A value of 0 means 214// extend to as long as the longest option text. 215func (d *DropDown) SetFieldWidth(width int) *DropDown { 216 d.fieldWidth = width 217 return d 218} 219 220// GetFieldWidth returns this primitive's field screen width. 221func (d *DropDown) GetFieldWidth() int { 222 if d.fieldWidth > 0 { 223 return d.fieldWidth 224 } 225 fieldWidth := 0 226 for _, option := range d.options { 227 width := TaggedStringWidth(option.Text) 228 if width > fieldWidth { 229 fieldWidth = width 230 } 231 } 232 return fieldWidth 233} 234 235// AddOption adds a new selectable option to this drop-down. The "selected" 236// callback is called when this option was selected. It may be nil. 237func (d *DropDown) AddOption(text string, selected func()) *DropDown { 238 d.options = append(d.options, &dropDownOption{Text: text, Selected: selected}) 239 d.list.AddItem(d.optionPrefix+text+d.optionSuffix, "", 0, nil) 240 return d 241} 242 243// SetOptions replaces all current options with the ones provided and installs 244// one callback function which is called when one of the options is selected. 245// It will be called with the option's text and its index into the options 246// slice. The "selected" parameter may be nil. 247func (d *DropDown) SetOptions(texts []string, selected func(text string, index int)) *DropDown { 248 d.list.Clear() 249 d.options = nil 250 for index, text := range texts { 251 func(t string, i int) { 252 d.AddOption(text, nil) 253 }(text, index) 254 } 255 d.selected = selected 256 return d 257} 258 259// SetSelectedFunc sets a handler which is called when the user changes the 260// drop-down's option. This handler will be called in addition and prior to 261// an option's optional individual handler. The handler is provided with the 262// selected option's text and index. If "no option" was selected, these values 263// are an empty string and -1. 264func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown { 265 d.selected = handler 266 return d 267} 268 269// SetDoneFunc sets a handler which is called when the user is done selecting 270// options. The callback function is provided with the key that was pressed, 271// which is one of the following: 272// 273// - KeyEscape: Abort selection. 274// - KeyTab: Move to the next field. 275// - KeyBacktab: Move to the previous field. 276func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown { 277 d.done = handler 278 return d 279} 280 281// SetFinishedFunc sets a callback invoked when the user leaves this form item. 282func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem { 283 d.finished = handler 284 return d 285} 286 287// Draw draws this primitive onto the screen. 288func (d *DropDown) Draw(screen tcell.Screen) { 289 d.Box.Draw(screen) 290 291 // Prepare. 292 x, y, width, height := d.GetInnerRect() 293 rightLimit := x + width 294 if height < 1 || rightLimit <= x { 295 return 296 } 297 298 // Draw label. 299 if d.labelWidth > 0 { 300 labelWidth := d.labelWidth 301 if labelWidth > rightLimit-x { 302 labelWidth = rightLimit - x 303 } 304 Print(screen, d.label, x, y, labelWidth, AlignLeft, d.labelColor) 305 x += labelWidth 306 } else { 307 _, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor) 308 x += drawnWidth 309 } 310 311 // What's the longest option text? 312 maxWidth := 0 313 optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix) 314 for _, option := range d.options { 315 strWidth := TaggedStringWidth(option.Text) + optionWrapWidth 316 if strWidth > maxWidth { 317 maxWidth = strWidth 318 } 319 } 320 321 // Draw selection area. 322 fieldWidth := d.fieldWidth 323 if fieldWidth == 0 { 324 fieldWidth = maxWidth 325 if d.currentOption < 0 { 326 noSelectionWidth := TaggedStringWidth(d.noSelection) 327 if noSelectionWidth > fieldWidth { 328 fieldWidth = noSelectionWidth 329 } 330 } else if d.currentOption < len(d.options) { 331 currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix) 332 if currentOptionWidth > fieldWidth { 333 fieldWidth = currentOptionWidth 334 } 335 } 336 } 337 if rightLimit-x < fieldWidth { 338 fieldWidth = rightLimit - x 339 } 340 fieldStyle := tcell.StyleDefault.Background(d.fieldBackgroundColor) 341 if d.GetFocusable().HasFocus() && !d.open { 342 fieldStyle = fieldStyle.Background(d.fieldTextColor) 343 } 344 for index := 0; index < fieldWidth; index++ { 345 screen.SetContent(x+index, y, ' ', nil, fieldStyle) 346 } 347 348 // Draw selected text. 349 if d.open && len(d.prefix) > 0 { 350 // Show the prefix. 351 currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix) 352 prefixWidth := stringWidth(d.prefix) 353 listItemText := d.options[d.list.GetCurrentItem()].Text 354 Print(screen, d.currentOptionPrefix, x, y, fieldWidth, AlignLeft, d.fieldTextColor) 355 Print(screen, d.prefix, x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor) 356 if len(d.prefix) < len(listItemText) { 357 Print(screen, listItemText[len(d.prefix):]+d.currentOptionSuffix, x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, d.fieldTextColor) 358 } 359 } else { 360 color := d.fieldTextColor 361 text := d.noSelection 362 if d.currentOption >= 0 && d.currentOption < len(d.options) { 363 text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix 364 } 365 // Just show the current selection. 366 if d.GetFocusable().HasFocus() && !d.open { 367 color = d.fieldBackgroundColor 368 } 369 Print(screen, text, x, y, fieldWidth, AlignLeft, color) 370 } 371 372 // Draw options list. 373 if d.HasFocus() && d.open { 374 // We prefer to drop down but if there is no space, maybe drop up? 375 lx := x 376 ly := y + 1 377 lwidth := maxWidth 378 lheight := len(d.options) 379 _, sheight := screen.Size() 380 if ly+lheight >= sheight && ly-2 > lheight-ly { 381 ly = y - lheight 382 if ly < 0 { 383 ly = 0 384 } 385 } 386 if ly+lheight >= sheight { 387 lheight = sheight - ly 388 } 389 d.list.SetRect(lx, ly, lwidth, lheight) 390 d.list.Draw(screen) 391 } 392} 393 394// InputHandler returns the handler for this primitive. 395func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { 396 return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { 397 // A helper function which selects an item in the drop-down list based on 398 // the current prefix. 399 evalPrefix := func() { 400 if len(d.prefix) > 0 { 401 for index, option := range d.options { 402 if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) { 403 d.list.SetCurrentItem(index) 404 return 405 } 406 } 407 // Prefix does not match any item. Remove last rune. 408 r := []rune(d.prefix) 409 d.prefix = string(r[:len(r)-1]) 410 } 411 } 412 413 // Process key event. 414 switch key := event.Key(); key { 415 case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown: 416 d.prefix = "" 417 418 // If the first key was a letter already, it becomes part of the prefix. 419 if r := event.Rune(); key == tcell.KeyRune && r != ' ' { 420 d.prefix += string(r) 421 evalPrefix() 422 } 423 424 // Hand control over to the list. 425 d.open = true 426 optionBefore := d.currentOption 427 d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { 428 // An option was selected. Close the list again. 429 d.open = false 430 setFocus(d) 431 d.currentOption = index 432 433 // Trigger "selected" event. 434 if d.selected != nil { 435 d.selected(d.options[d.currentOption].Text, d.currentOption) 436 } 437 if d.options[d.currentOption].Selected != nil { 438 d.options[d.currentOption].Selected() 439 } 440 }).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 441 if event.Key() == tcell.KeyRune { 442 d.prefix += string(event.Rune()) 443 evalPrefix() 444 } else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 { 445 if len(d.prefix) > 0 { 446 r := []rune(d.prefix) 447 d.prefix = string(r[:len(r)-1]) 448 } 449 evalPrefix() 450 } else if event.Key() == tcell.KeyEscape { 451 d.open = false 452 d.currentOption = optionBefore 453 setFocus(d) 454 } else { 455 d.prefix = "" 456 } 457 return event 458 }) 459 setFocus(d.list) 460 case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: 461 if d.done != nil { 462 d.done(key) 463 } 464 if d.finished != nil { 465 d.finished(key) 466 } 467 } 468 }) 469} 470 471// Focus is called by the application when the primitive receives focus. 472func (d *DropDown) Focus(delegate func(p Primitive)) { 473 d.Box.Focus(delegate) 474 if d.open { 475 delegate(d.list) 476 } 477} 478 479// HasFocus returns whether or not this primitive has focus. 480func (d *DropDown) HasFocus() bool { 481 if d.open { 482 return d.list.HasFocus() 483 } 484 return d.hasFocus 485} 486