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