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