1 package tview
2
3 import (
4 "strings"
5
6 "github.com/gdamore/tcell/v2"
7 "github.com/rivo/uniseg"
8 )
9
10
11 type dropDownOption struct {
12 Text string
13 Selected func()
14 }
15
16
17
18
19
20 type DropDown struct {
21 *Box
22
23
24 disabled bool
25
26
27 options []*dropDownOption
28
29
30 optionPrefix, optionSuffix string
31
32
33
34 currentOption int
35
36
37 currentOptionPrefix, currentOptionSuffix string
38
39
40 noSelection string
41
42
43 open bool
44
45
46 prefix string
47
48
49 list *List
50
51
52 label string
53
54
55 labelColor tcell.Color
56
57
58 fieldBackgroundColor tcell.Color
59
60
61 fieldTextColor tcell.Color
62
63
64 prefixTextColor tcell.Color
65
66
67
68 labelWidth int
69
70
71
72 fieldWidth int
73
74
75
76
77 done func(tcell.Key)
78
79
80
81 finished func(tcell.Key)
82
83
84
85 selected func(text string, index int)
86
87 dragging bool
88 }
89
90
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
114
115
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)
129 if d.selected != nil {
130 d.selected("", -1)
131 }
132 }
133 return d
134 }
135
136
137
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
147
148
149
150
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
164 func (d *DropDown) SetLabel(label string) *DropDown {
165 d.label = label
166 return d
167 }
168
169
170 func (d *DropDown) GetLabel() string {
171 return d.label
172 }
173
174
175
176 func (d *DropDown) SetLabelWidth(width int) *DropDown {
177 d.labelWidth = width
178 return d
179 }
180
181
182 func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown {
183 d.labelColor = color
184 return d
185 }
186
187
188 func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
189 d.fieldBackgroundColor = color
190 return d
191 }
192
193
194 func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
195 d.fieldTextColor = color
196 return d
197 }
198
199
200
201
202 func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
203 d.prefixTextColor = color
204 return d
205 }
206
207
208
209
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
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
229
230 func (d *DropDown) SetFieldWidth(width int) *DropDown {
231 d.fieldWidth = width
232 return d
233 }
234
235
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
251 func (d *DropDown) GetFieldHeight() int {
252 return 1
253 }
254
255
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
265
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
273
274
275
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
289 func (d *DropDown) GetOptionCount() int {
290 return len(d.options)
291 }
292
293
294
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
302
303
304
305
306 func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown {
307 d.selected = handler
308 return d
309 }
310
311
312
313
314
315
316
317
318 func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown {
319 d.done = handler
320 return d
321 }
322
323
324 func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
325 d.finished = handler
326 return d
327 }
328
329
330 func (d *DropDown) Draw(screen tcell.Screen) {
331 d.Box.DrawForSubclass(screen, d)
332
333
334 x, y, width, height := d.GetInnerRect()
335 rightLimit := x + width
336 if height < 1 || rightLimit <= x {
337 return
338 }
339
340
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
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
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
394 if d.open && len(d.prefix) > 0 {
395
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
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
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
425
426 if lx+lwidth >= swidth {
427 lx = swidth - lwidth
428 if lx < 0 {
429 lx = 0
430 }
431 }
432
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
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
455 if d.list.HasFocus() {
456 if handler := d.list.InputHandler(); handler != nil {
457 handler(event, setFocus)
458 }
459 return
460 }
461
462
463 switch key := event.Key(); key {
464 case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
465 d.prefix = ""
466
467
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
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
496 r := []rune(d.prefix)
497 d.prefix = string(r[:len(r)-1])
498 }
499 }
500
501
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
509 }
510
511
512 d.currentOption = index
513 d.closeList(setFocus)
514
515
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
546
547 func (d *DropDown) closeList(setFocus func(Primitive)) {
548 d.open = false
549 if d.list.HasFocus() {
550 setFocus(d)
551 }
552 }
553
554
555 func (d *DropDown) IsOpen() bool {
556 return d.open
557 }
558
559
560 func (d *DropDown) Focus(delegate func(p Primitive)) {
561
562
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
576 func (d *DropDown) HasFocus() bool {
577 if d.open {
578 return d.list.HasFocus()
579 }
580 return d.Box.HasFocus()
581 }
582
583
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
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
596 }
597
598
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)
612 }
613 case MouseMove:
614 if d.dragging {
615
616
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