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