1 package cview
2
3 import (
4 "strings"
5 "sync"
6
7 "github.com/gdamore/tcell/v2"
8 "github.com/mattn/go-runewidth"
9 )
10
11
12 type DropDownOption struct {
13 text string
14 selected func(index int, option *DropDownOption)
15 reference interface{}
16
17 sync.RWMutex
18 }
19
20
21 func NewDropDownOption(text string) *DropDownOption {
22 return &DropDownOption{text: text}
23 }
24
25
26 func (d *DropDownOption) GetText() string {
27 d.RLock()
28 defer d.RUnlock()
29
30 return d.text
31 }
32
33
34 func (d *DropDownOption) SetText(text string) {
35 d.text = text
36 }
37
38
39 func (d *DropDownOption) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
40 d.selected = handler
41 }
42
43
44 func (d *DropDownOption) GetReference() interface{} {
45 d.RLock()
46 defer d.RUnlock()
47
48 return d.reference
49 }
50
51
52 func (d *DropDownOption) SetReference(reference interface{}) {
53 d.reference = reference
54 }
55
56
57
58 type DropDown struct {
59 *Box
60
61
62 options []*DropDownOption
63
64
65 optionPrefix, optionSuffix string
66
67
68
69 currentOption int
70
71
72 currentOptionPrefix, currentOptionSuffix string
73
74
75 noSelection string
76
77
78 open bool
79
80
81 prefix string
82
83
84 list *List
85
86
87 label string
88
89
90 labelColor tcell.Color
91
92
93 labelColorFocused tcell.Color
94
95
96 fieldBackgroundColor tcell.Color
97
98
99 fieldBackgroundColorFocused tcell.Color
100
101
102 fieldTextColor tcell.Color
103
104
105 fieldTextColorFocused tcell.Color
106
107
108 prefixTextColor tcell.Color
109
110
111
112 labelWidth int
113
114
115
116 fieldWidth int
117
118
119
120
121 done func(tcell.Key)
122
123
124
125 finished func(tcell.Key)
126
127
128
129 selected func(index int, option *DropDownOption)
130
131
132 dragging bool
133
134
135 abbreviationChars string
136
137
138 dropDownSymbol rune
139
140
141 dropDownOpenSymbol rune
142
143
144 dropDownSelectedSymbol rune
145
146
147 alwaysDrawDropDownSymbol bool
148
149 sync.RWMutex
150 }
151
152
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
188
189 func (d *DropDown) SetDropDownSymbolRune(symbol rune) {
190 d.Lock()
191 defer d.Unlock()
192 d.dropDownSymbol = symbol
193 }
194
195
196
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
211
212 func (d *DropDown) SetDropDownSelectedSymbolRune(symbol rune) {
213 d.Lock()
214 defer d.Unlock()
215 d.dropDownSelectedSymbol = symbol
216 }
217
218
219
220 func (d *DropDown) SetAlwaysDrawDropDownSymbol(alwaysDraw bool) {
221 d.Lock()
222 defer d.Unlock()
223 d.alwaysDrawDropDownSymbol = alwaysDraw
224 }
225
226
227
228
229 func (d *DropDown) SetCurrentOption(index int) {
230 d.Lock()
231 defer d.Unlock()
232
233 if index >= 0 && index < len(d.options) {
234 d.currentOption = index
235 d.list.SetCurrentItem(index)
236 if d.selected != nil {
237 d.Unlock()
238 d.selected(index, d.options[index])
239 d.Lock()
240 }
241 if d.options[index].selected != nil {
242 d.Unlock()
243 d.options[index].selected(index, d.options[index])
244 d.Lock()
245 }
246 } else {
247 d.currentOption = -1
248 d.list.SetCurrentItem(0)
249 if d.selected != nil {
250 d.Unlock()
251 d.selected(-1, nil)
252 d.Lock()
253 }
254 }
255 }
256
257
258
259 func (d *DropDown) GetCurrentOption() (int, *DropDownOption) {
260 d.RLock()
261 defer d.RUnlock()
262
263 var option *DropDownOption
264 if d.currentOption >= 0 && d.currentOption < len(d.options) {
265 option = d.options[d.currentOption]
266 }
267 return d.currentOption, option
268 }
269
270
271
272
273
274
275 func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) {
276 d.Lock()
277 defer d.Unlock()
278
279 d.currentOptionPrefix = currentPrefix
280 d.currentOptionSuffix = currentSuffix
281 d.noSelection = noSelection
282 d.optionPrefix = prefix
283 d.optionSuffix = suffix
284 for index := 0; index < d.list.GetItemCount(); index++ {
285 d.list.SetItemText(index, prefix+d.options[index].text+suffix, "")
286 }
287 }
288
289
290 func (d *DropDown) SetLabel(label string) {
291 d.Lock()
292 defer d.Unlock()
293
294 d.label = label
295 }
296
297
298 func (d *DropDown) GetLabel() string {
299 d.RLock()
300 defer d.RUnlock()
301
302 return d.label
303 }
304
305
306
307 func (d *DropDown) SetLabelWidth(width int) {
308 d.Lock()
309 defer d.Unlock()
310
311 d.labelWidth = width
312 }
313
314
315 func (d *DropDown) SetLabelColor(color tcell.Color) {
316 d.Lock()
317 defer d.Unlock()
318
319 d.labelColor = color
320 }
321
322
323 func (d *DropDown) SetLabelColorFocused(color tcell.Color) {
324 d.Lock()
325 defer d.Unlock()
326
327 d.labelColorFocused = color
328 }
329
330
331 func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) {
332 d.Lock()
333 defer d.Unlock()
334
335 d.fieldBackgroundColor = color
336 }
337
338
339 func (d *DropDown) SetFieldBackgroundColorFocused(color tcell.Color) {
340 d.Lock()
341 defer d.Unlock()
342
343 d.fieldBackgroundColorFocused = color
344 }
345
346
347 func (d *DropDown) SetFieldTextColor(color tcell.Color) {
348 d.Lock()
349 defer d.Unlock()
350
351 d.fieldTextColor = color
352 }
353
354
355 func (d *DropDown) SetFieldTextColorFocused(color tcell.Color) {
356 d.Lock()
357 defer d.Unlock()
358
359 d.fieldTextColorFocused = color
360 }
361
362
363 func (d *DropDown) SetDropDownTextColor(color tcell.Color) {
364 d.Lock()
365 defer d.Unlock()
366
367 d.list.SetMainTextColor(color)
368 }
369
370
371 func (d *DropDown) SetDropDownBackgroundColor(color tcell.Color) {
372 d.Lock()
373 defer d.Unlock()
374
375 d.list.SetBackgroundColor(color)
376 }
377
378
379
380 func (d *DropDown) SetDropDownSelectedTextColor(color tcell.Color) {
381 d.Lock()
382 defer d.Unlock()
383
384 d.list.SetSelectedTextColor(color)
385 }
386
387
388
389 func (d *DropDown) SetDropDownSelectedBackgroundColor(color tcell.Color) {
390 d.Lock()
391 defer d.Unlock()
392
393 d.list.SetSelectedBackgroundColor(color)
394 }
395
396
397
398
399 func (d *DropDown) SetPrefixTextColor(color tcell.Color) {
400 d.Lock()
401 defer d.Unlock()
402
403 d.prefixTextColor = color
404 }
405
406
407
408 func (d *DropDown) SetFieldWidth(width int) {
409 d.Lock()
410 defer d.Unlock()
411
412 d.fieldWidth = width
413 }
414
415
416 func (d *DropDown) GetFieldHeight() int {
417 return 1
418 }
419
420
421 func (d *DropDown) GetFieldWidth() int {
422 d.RLock()
423 defer d.RUnlock()
424 return d.getFieldWidth()
425 }
426
427 func (d *DropDown) getFieldWidth() int {
428 if d.fieldWidth > 0 {
429 return d.fieldWidth
430 }
431 fieldWidth := 0
432 for _, option := range d.options {
433 width := TaggedStringWidth(option.text)
434 if width > fieldWidth {
435 fieldWidth = width
436 }
437 }
438 fieldWidth += len(d.optionPrefix) + len(d.optionSuffix)
439 fieldWidth += len(d.currentOptionPrefix) + len(d.currentOptionSuffix)
440
441 fieldWidth += 4
442 return fieldWidth
443 }
444
445
446 func (d *DropDown) AddOptionsSimple(options ...string) {
447 optionsToAdd := make([]*DropDownOption, len(options))
448 for i, option := range options {
449 optionsToAdd[i] = NewDropDownOption(option)
450 }
451 d.AddOptions(optionsToAdd...)
452 }
453
454
455 func (d *DropDown) AddOptions(options ...*DropDownOption) {
456 d.Lock()
457 defer d.Unlock()
458 d.addOptions(options...)
459 }
460
461 func (d *DropDown) addOptions(options ...*DropDownOption) {
462 d.options = append(d.options, options...)
463 for _, option := range options {
464 d.list.AddItem(NewListItem(d.optionPrefix + option.text + d.optionSuffix))
465 }
466 }
467
468
469
470
471
472 func (d *DropDown) SetOptionsSimple(selected func(index int, option *DropDownOption), options ...string) {
473 optionsToSet := make([]*DropDownOption, len(options))
474 for i, option := range options {
475 optionsToSet[i] = NewDropDownOption(option)
476 }
477 d.SetOptions(selected, optionsToSet...)
478 }
479
480
481
482
483
484 func (d *DropDown) SetOptions(selected func(index int, option *DropDownOption), options ...*DropDownOption) {
485 d.Lock()
486 defer d.Unlock()
487
488 d.list.Clear()
489 d.options = nil
490 d.addOptions(options...)
491 d.selected = selected
492 }
493
494
495
496
497
498 func (d *DropDown) SetChangedFunc(handler func(index int, option *DropDownOption)) {
499 d.list.SetChangedFunc(func(index int, item *ListItem) {
500 handler(index, d.options[index])
501 })
502 }
503
504
505
506
507
508
509 func (d *DropDown) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
510 d.Lock()
511 defer d.Unlock()
512
513 d.selected = handler
514 }
515
516
517
518
519
520
521
522
523 func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) {
524 d.Lock()
525 defer d.Unlock()
526
527 d.done = handler
528 }
529
530
531 func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) {
532 d.Lock()
533 defer d.Unlock()
534
535 d.finished = handler
536 }
537
538
539 func (d *DropDown) Draw(screen tcell.Screen) {
540 d.Box.Draw(screen)
541 hasFocus := d.GetFocusable().HasFocus()
542
543 d.Lock()
544 defer d.Unlock()
545
546
547 labelColor := d.labelColor
548 fieldBackgroundColor := d.fieldBackgroundColor
549 fieldTextColor := d.fieldTextColor
550 if hasFocus {
551 if d.labelColorFocused != ColorUnset {
552 labelColor = d.labelColorFocused
553 }
554 if d.fieldBackgroundColorFocused != ColorUnset {
555 fieldBackgroundColor = d.fieldBackgroundColorFocused
556 }
557 if d.fieldTextColorFocused != ColorUnset {
558 fieldTextColor = d.fieldTextColorFocused
559 }
560 }
561
562
563 x, y, width, height := d.GetInnerRect()
564 rightLimit := x + width
565 if height < 1 || rightLimit <= x {
566 return
567 }
568
569
570 if d.labelWidth > 0 {
571 labelWidth := d.labelWidth
572 if labelWidth > rightLimit-x {
573 labelWidth = rightLimit - x
574 }
575 Print(screen, []byte(d.label), x, y, labelWidth, AlignLeft, labelColor)
576 x += labelWidth
577 } else {
578 _, drawnWidth := Print(screen, []byte(d.label), x, y, rightLimit-x, AlignLeft, labelColor)
579 x += drawnWidth
580 }
581
582
583 maxWidth := 0
584 optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
585 for _, option := range d.options {
586 strWidth := TaggedStringWidth(option.text) + optionWrapWidth
587 if strWidth > maxWidth {
588 maxWidth = strWidth
589 }
590 }
591
592
593 fieldWidth := d.getFieldWidth()
594 if fieldWidth == 0 {
595 fieldWidth = maxWidth
596 if d.currentOption < 0 {
597 noSelectionWidth := TaggedStringWidth(d.noSelection)
598 if noSelectionWidth > fieldWidth {
599 fieldWidth = noSelectionWidth
600 }
601 } else if d.currentOption < len(d.options) {
602 currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix)
603 if currentOptionWidth > fieldWidth {
604 fieldWidth = currentOptionWidth
605 }
606 }
607 }
608 if rightLimit-x < fieldWidth {
609 fieldWidth = rightLimit - x
610 }
611 fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
612 for index := 0; index < fieldWidth; index++ {
613 screen.SetContent(x+index, y, ' ', nil, fieldStyle)
614 }
615
616
617 if d.open && len(d.prefix) > 0 {
618
619 currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix)
620 prefixWidth := runewidth.StringWidth(d.prefix)
621 listItemText := d.options[d.list.GetCurrentItemIndex()].text
622 Print(screen, []byte(d.currentOptionPrefix), x, y, fieldWidth, AlignLeft, fieldTextColor)
623 Print(screen, []byte(d.prefix), x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor)
624 if len(d.prefix) < len(listItemText) {
625 Print(screen, []byte(listItemText[len(d.prefix):]+d.currentOptionSuffix), x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor)
626 }
627 } else {
628 color := fieldTextColor
629 text := d.noSelection
630 if d.currentOption >= 0 && d.currentOption < len(d.options) {
631 text = d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix
632 }
633
634 if fieldWidth > len(d.abbreviationChars)+3 && len(text) > fieldWidth {
635 text = text[0:fieldWidth-3-len(d.abbreviationChars)] + d.abbreviationChars
636 }
637
638
639 Print(screen, []byte(text), x, y, fieldWidth, AlignLeft, color)
640 }
641
642
643 if d.alwaysDrawDropDownSymbol || d._hasFocus() {
644 symbol := d.dropDownSymbol
645 if d.open {
646 symbol = d.dropDownOpenSymbol
647 }
648 screen.SetContent(x+fieldWidth-2, y, symbol, nil, new(tcell.Style).Foreground(fieldTextColor).Background(fieldBackgroundColor))
649 }
650
651
652 if hasFocus && d.open {
653
654 lx := x
655 ly := y + 1
656 lheight := len(d.options)
657 _, sheight := screen.Size()
658 if ly+lheight >= sheight && ly-2 > lheight-ly {
659 ly = y - lheight
660 if ly < 0 {
661 ly = 0
662 }
663 }
664 if ly+lheight >= sheight {
665 lheight = sheight - ly
666 }
667 lwidth := maxWidth
668 if d.list.scrollBarVisibility == ScrollBarAlways || (d.list.scrollBarVisibility == ScrollBarAuto && len(d.options) > lheight) {
669 lwidth++
670 }
671 if lwidth < fieldWidth {
672 lwidth = fieldWidth
673 }
674 d.list.SetRect(lx, ly, lwidth, lheight)
675 d.list.Draw(screen)
676 }
677 }
678
679
680 func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
681 return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
682
683 switch key := event.Key(); key {
684 case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
685 d.Lock()
686 defer d.Unlock()
687
688 d.prefix = ""
689
690
691 if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
692 d.prefix += string(r)
693 d.evalPrefix()
694 }
695
696 d.openList(setFocus)
697 case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
698 if d.done != nil {
699 d.done(key)
700 }
701 if d.finished != nil {
702 d.finished(key)
703 }
704 }
705 })
706 }
707
708
709 func (d *DropDown) evalPrefix() {
710 if len(d.prefix) > 0 {
711 for index, option := range d.options {
712 if strings.HasPrefix(strings.ToLower(option.text), d.prefix) {
713 d.list.SetCurrentItem(index)
714 return
715 }
716 }
717
718
719 r := []rune(d.prefix)
720 d.prefix = string(r[:len(r)-1])
721 }
722 }
723
724
725 func (d *DropDown) openList(setFocus func(Primitive)) {
726 d.open = true
727 optionBefore := d.currentOption
728
729 d.list.SetSelectedFunc(func(index int, item *ListItem) {
730 if d.dragging {
731 return
732 }
733
734
735 d.currentOption = index
736 d.closeList(setFocus)
737
738
739 if d.selected != nil {
740 d.selected(d.currentOption, d.options[d.currentOption])
741 }
742 if d.options[d.currentOption].selected != nil {
743 d.options[d.currentOption].selected(d.currentOption, d.options[d.currentOption])
744 }
745 })
746 d.list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
747 if event.Key() == tcell.KeyRune {
748 d.prefix += string(event.Rune())
749 d.evalPrefix()
750 } else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
751 if len(d.prefix) > 0 {
752 r := []rune(d.prefix)
753 d.prefix = string(r[:len(r)-1])
754 }
755 d.evalPrefix()
756 } else if event.Key() == tcell.KeyEscape {
757 d.currentOption = optionBefore
758 d.list.SetCurrentItem(d.currentOption)
759 d.closeList(setFocus)
760 if d.selected != nil {
761 if d.currentOption > -1 {
762 d.selected(d.currentOption, d.options[d.currentOption])
763 }
764 }
765 } else {
766 d.prefix = ""
767 }
768
769 return event
770 })
771
772 setFocus(d.list)
773 }
774
775
776
777 func (d *DropDown) closeList(setFocus func(Primitive)) {
778 d.open = false
779 if d.list.HasFocus() {
780 setFocus(d)
781 }
782 }
783
784
785 func (d *DropDown) Focus(delegate func(p Primitive)) {
786 d.Box.Focus(delegate)
787 if d.open {
788 delegate(d.list)
789 }
790 }
791
792
793 func (d *DropDown) HasFocus() bool {
794 d.RLock()
795 defer d.RUnlock()
796
797 return d._hasFocus()
798 }
799
800 func (d *DropDown) _hasFocus() bool {
801 if d.open {
802 return d.list.HasFocus()
803 }
804 return d.hasFocus
805 }
806
807
808 func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
809 return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
810
811 x, y := event.Position()
812 _, rectY, _, _ := d.GetInnerRect()
813 inRect := y == rectY
814 if !d.open && !inRect {
815 return d.InRect(x, y), nil
816 }
817
818
819 switch action {
820 case MouseLeftDown:
821 consumed = d.open || inRect
822 capture = d
823 if !d.open {
824 d.openList(setFocus)
825 d.dragging = true
826 } else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
827 d.closeList(setFocus)
828 }
829 case MouseMove:
830 if d.dragging {
831
832
833 d.list.MouseHandler()(MouseLeftClick, event, setFocus)
834 consumed = true
835 capture = d
836 }
837 case MouseLeftUp:
838 if d.dragging {
839 d.dragging = false
840 d.list.MouseHandler()(MouseLeftClick, event, setFocus)
841 consumed = true
842 }
843 }
844
845 return
846 })
847 }
848
View as plain text