1 package cview
2
3 import (
4 "bytes"
5 "math"
6 "regexp"
7 "sync"
8 "unicode/utf8"
9
10 "github.com/gdamore/tcell/v2"
11 "github.com/mattn/go-runewidth"
12 )
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 type InputField struct {
33 *Box
34
35
36 text []byte
37
38
39 label []byte
40
41
42 placeholder []byte
43
44
45 labelColor tcell.Color
46
47
48 labelColorFocused tcell.Color
49
50
51 fieldBackgroundColor tcell.Color
52
53
54 fieldBackgroundColorFocused tcell.Color
55
56
57 fieldTextColor tcell.Color
58
59
60 fieldTextColorFocused tcell.Color
61
62
63 placeholderTextColor tcell.Color
64
65
66 placeholderTextColorFocused tcell.Color
67
68
69 autocompleteListTextColor tcell.Color
70
71
72 autocompleteListBackgroundColor tcell.Color
73
74
75 autocompleteListSelectedTextColor tcell.Color
76
77
78 autocompleteListSelectedBackgroundColor tcell.Color
79
80
81 autocompleteSuggestionTextColor tcell.Color
82
83
84 fieldNoteTextColor tcell.Color
85
86
87 fieldNote []byte
88
89
90
91 labelWidth int
92
93
94
95 fieldWidth int
96
97
98
99 maskCharacter rune
100
101
102 cursorPos int
103
104
105
106
107
108
109 autocomplete func(text string) []*ListItem
110
111
112
113 autocompleteList *List
114
115
116 autocompleteListSuggestion []byte
117
118
119 accept func(text string, ch rune) bool
120
121
122 changed func(text string)
123
124
125
126
127 done func(tcell.Key)
128
129
130
131 finished func(tcell.Key)
132
133
134 fieldX int
135
136
137 offset int
138
139 sync.RWMutex
140 }
141
142
143 func NewInputField() *InputField {
144 return &InputField{
145 Box: NewBox(),
146 labelColor: Styles.SecondaryTextColor,
147 fieldBackgroundColor: Styles.MoreContrastBackgroundColor,
148 fieldBackgroundColorFocused: Styles.ContrastBackgroundColor,
149 fieldTextColor: Styles.PrimaryTextColor,
150 fieldTextColorFocused: Styles.PrimaryTextColor,
151 placeholderTextColor: Styles.ContrastSecondaryTextColor,
152 autocompleteListTextColor: Styles.PrimitiveBackgroundColor,
153 autocompleteListBackgroundColor: Styles.MoreContrastBackgroundColor,
154 autocompleteListSelectedTextColor: Styles.PrimitiveBackgroundColor,
155 autocompleteListSelectedBackgroundColor: Styles.PrimaryTextColor,
156 autocompleteSuggestionTextColor: Styles.ContrastSecondaryTextColor,
157 fieldNoteTextColor: Styles.SecondaryTextColor,
158 labelColorFocused: ColorUnset,
159 placeholderTextColorFocused: ColorUnset,
160 }
161 }
162
163
164 func (i *InputField) SetText(text string) {
165 i.Lock()
166
167 i.text = []byte(text)
168 i.cursorPos = len(text)
169 if i.changed != nil {
170 i.Unlock()
171 i.changed(text)
172 } else {
173 i.Unlock()
174 }
175 }
176
177
178 func (i *InputField) GetText() string {
179 i.RLock()
180 defer i.RUnlock()
181
182 return string(i.text)
183 }
184
185
186 func (i *InputField) SetLabel(label string) {
187 i.Lock()
188 defer i.Unlock()
189
190 i.label = []byte(label)
191 }
192
193
194 func (i *InputField) GetLabel() string {
195 i.RLock()
196 defer i.RUnlock()
197
198 return string(i.label)
199 }
200
201
202
203 func (i *InputField) SetLabelWidth(width int) {
204 i.Lock()
205 defer i.Unlock()
206
207 i.labelWidth = width
208 }
209
210
211 func (i *InputField) SetPlaceholder(text string) {
212 i.Lock()
213 defer i.Unlock()
214
215 i.placeholder = []byte(text)
216 }
217
218
219 func (i *InputField) SetLabelColor(color tcell.Color) {
220 i.Lock()
221 defer i.Unlock()
222
223 i.labelColor = color
224 }
225
226
227 func (i *InputField) SetLabelColorFocused(color tcell.Color) {
228 i.Lock()
229 defer i.Unlock()
230
231 i.labelColorFocused = color
232 }
233
234
235 func (i *InputField) SetFieldBackgroundColor(color tcell.Color) {
236 i.Lock()
237 defer i.Unlock()
238
239 i.fieldBackgroundColor = color
240 }
241
242
243
244 func (i *InputField) SetFieldBackgroundColorFocused(color tcell.Color) {
245 i.Lock()
246 defer i.Unlock()
247
248 i.fieldBackgroundColorFocused = color
249 }
250
251
252 func (i *InputField) SetFieldTextColor(color tcell.Color) {
253 i.Lock()
254 defer i.Unlock()
255
256 i.fieldTextColor = color
257 }
258
259
260 func (i *InputField) SetFieldTextColorFocused(color tcell.Color) {
261 i.Lock()
262 defer i.Unlock()
263
264 i.fieldTextColorFocused = color
265 }
266
267
268 func (i *InputField) SetPlaceholderTextColor(color tcell.Color) {
269 i.Lock()
270 defer i.Unlock()
271
272 i.placeholderTextColor = color
273 }
274
275
276
277 func (i *InputField) SetPlaceholderTextColorFocused(color tcell.Color) {
278 i.Lock()
279 defer i.Unlock()
280
281 i.placeholderTextColorFocused = color
282 }
283
284
285 func (i *InputField) SetAutocompleteListTextColor(color tcell.Color) {
286 i.Lock()
287 defer i.Unlock()
288
289 i.autocompleteListTextColor = color
290 }
291
292
293
294 func (i *InputField) SetAutocompleteListBackgroundColor(color tcell.Color) {
295 i.Lock()
296 defer i.Unlock()
297
298 i.autocompleteListBackgroundColor = color
299 }
300
301
302
303 func (i *InputField) SetAutocompleteListSelectedTextColor(color tcell.Color) {
304 i.Lock()
305 defer i.Unlock()
306
307 i.autocompleteListSelectedTextColor = color
308 }
309
310
311
312 func (i *InputField) SetAutocompleteListSelectedBackgroundColor(color tcell.Color) {
313 i.Lock()
314 defer i.Unlock()
315
316 i.autocompleteListSelectedBackgroundColor = color
317 }
318
319
320
321 func (i *InputField) SetAutocompleteSuggestionTextColor(color tcell.Color) {
322 i.Lock()
323 defer i.Unlock()
324
325 i.autocompleteSuggestionTextColor = color
326 }
327
328
329 func (i *InputField) SetFieldNoteTextColor(color tcell.Color) {
330 i.Lock()
331 defer i.Unlock()
332
333 i.fieldNoteTextColor = color
334 }
335
336
337
338 func (i *InputField) SetFieldNote(note string) {
339 i.Lock()
340 defer i.Unlock()
341
342 i.fieldNote = []byte(note)
343 }
344
345
346 func (i *InputField) ResetFieldNote() {
347 i.Lock()
348 defer i.Unlock()
349
350 i.fieldNote = nil
351 }
352
353
354
355 func (i *InputField) SetFieldWidth(width int) {
356 i.Lock()
357 defer i.Unlock()
358
359 i.fieldWidth = width
360 }
361
362
363 func (i *InputField) GetFieldWidth() int {
364 i.RLock()
365 defer i.RUnlock()
366
367 return i.fieldWidth
368 }
369
370
371 func (i *InputField) GetFieldHeight() int {
372 i.RLock()
373 defer i.RUnlock()
374 if len(i.fieldNote) == 0 {
375 return 1
376 }
377 return 2
378 }
379
380
381 func (i *InputField) GetCursorPosition() int {
382 i.RLock()
383 defer i.RUnlock()
384
385 return i.cursorPos
386 }
387
388
389 func (i *InputField) SetCursorPosition(cursorPos int) {
390 i.Lock()
391 defer i.Unlock()
392
393 i.cursorPos = cursorPos
394 }
395
396
397
398 func (i *InputField) SetMaskCharacter(mask rune) {
399 i.Lock()
400 defer i.Unlock()
401
402 i.maskCharacter = mask
403 }
404
405
406
407
408
409
410
411 func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []*ListItem)) {
412 i.Lock()
413 i.autocomplete = callback
414 i.Unlock()
415
416 i.Autocomplete()
417 }
418
419
420
421
422
423
424
425
426
427 func (i *InputField) Autocomplete() {
428 i.Lock()
429 if i.autocomplete == nil {
430 i.Unlock()
431 return
432 }
433 i.Unlock()
434
435
436 entries := i.autocomplete(string(i.text))
437 if len(entries) == 0 {
438
439 i.Lock()
440 i.autocompleteList = nil
441 i.autocompleteListSuggestion = nil
442 i.Unlock()
443 return
444 }
445
446 i.Lock()
447
448
449 if i.autocompleteList == nil {
450 l := NewList()
451 l.SetChangedFunc(i.autocompleteChanged)
452 l.ShowSecondaryText(false)
453 l.SetMainTextColor(i.autocompleteListTextColor)
454 l.SetSelectedTextColor(i.autocompleteListSelectedTextColor)
455 l.SetSelectedBackgroundColor(i.autocompleteListSelectedBackgroundColor)
456 l.SetHighlightFullLine(true)
457 l.SetBackgroundColor(i.autocompleteListBackgroundColor)
458
459 i.autocompleteList = l
460 }
461
462
463 currentEntry := -1
464 i.autocompleteList.Clear()
465 for index, entry := range entries {
466 i.autocompleteList.AddItem(entry)
467 if currentEntry < 0 && entry.GetMainText() == string(i.text) {
468 currentEntry = index
469 }
470 }
471
472
473 if currentEntry >= 0 {
474 i.autocompleteList.SetCurrentItem(currentEntry)
475 }
476
477 i.Unlock()
478 }
479
480
481
482 func (i *InputField) autocompleteChanged(_ int, item *ListItem) {
483 mainText := item.GetMainBytes()
484 secondaryText := item.GetSecondaryBytes()
485 if len(i.text) < len(secondaryText) {
486 i.autocompleteListSuggestion = secondaryText[len(i.text):]
487 } else if len(i.text) < len(mainText) {
488 i.autocompleteListSuggestion = mainText[len(i.text):]
489 } else {
490 i.autocompleteListSuggestion = nil
491 }
492 }
493
494
495
496
497
498
499 func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) {
500 i.Lock()
501 defer i.Unlock()
502
503 i.accept = handler
504 }
505
506
507
508 func (i *InputField) SetChangedFunc(handler func(text string)) {
509 i.Lock()
510 defer i.Unlock()
511
512 i.changed = handler
513 }
514
515
516
517
518
519
520
521
522
523 func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) {
524 i.Lock()
525 defer i.Unlock()
526
527 i.done = handler
528 }
529
530
531 func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) {
532 i.Lock()
533 defer i.Unlock()
534
535 i.finished = handler
536 }
537
538
539 func (i *InputField) Draw(screen tcell.Screen) {
540 if !i.GetVisible() {
541 return
542 }
543
544 i.Box.Draw(screen)
545
546 i.Lock()
547 defer i.Unlock()
548
549
550 labelColor := i.labelColor
551 fieldBackgroundColor := i.fieldBackgroundColor
552 fieldTextColor := i.fieldTextColor
553 if i.GetFocusable().HasFocus() {
554 if i.labelColorFocused != ColorUnset {
555 labelColor = i.labelColorFocused
556 }
557 if i.fieldBackgroundColorFocused != ColorUnset {
558 fieldBackgroundColor = i.fieldBackgroundColorFocused
559 }
560 if i.fieldTextColorFocused != ColorUnset {
561 fieldTextColor = i.fieldTextColorFocused
562 }
563 }
564
565
566 x, y, width, height := i.GetInnerRect()
567 rightLimit := x + width
568 if height < 1 || rightLimit <= x {
569 return
570 }
571
572
573 if i.labelWidth > 0 {
574 labelWidth := i.labelWidth
575 if labelWidth > rightLimit-x {
576 labelWidth = rightLimit - x
577 }
578 Print(screen, i.label, x, y, labelWidth, AlignLeft, labelColor)
579 x += labelWidth
580 } else {
581 _, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, labelColor)
582 x += drawnWidth
583 }
584
585
586 i.fieldX = x
587 fieldWidth := i.fieldWidth
588 if fieldWidth == 0 {
589 fieldWidth = math.MaxInt32
590 }
591 if rightLimit-x < fieldWidth {
592 fieldWidth = rightLimit - x
593 }
594 fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
595 for index := 0; index < fieldWidth; index++ {
596 screen.SetContent(x+index, y, ' ', nil, fieldStyle)
597 }
598
599
600 var cursorScreenPos int
601 text := i.text
602 if len(text) == 0 && len(i.placeholder) > 0 {
603
604 placeholderTextColor := i.placeholderTextColor
605 if i.GetFocusable().HasFocus() && i.placeholderTextColorFocused != ColorUnset {
606 placeholderTextColor = i.placeholderTextColorFocused
607 }
608 Print(screen, EscapeBytes(i.placeholder), x, y, fieldWidth, AlignLeft, placeholderTextColor)
609 i.offset = 0
610 } else {
611
612 if i.maskCharacter > 0 {
613 text = bytes.Repeat([]byte(string(i.maskCharacter)), utf8.RuneCount(i.text))
614 }
615 var drawnText []byte
616 if fieldWidth > runewidth.StringWidth(string(text)) {
617
618 drawnText = EscapeBytes(text)
619 Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor)
620 i.offset = 0
621 iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
622 if textPos >= i.cursorPos {
623 return true
624 }
625 cursorScreenPos += screenWidth
626 return false
627 })
628 } else {
629
630 if i.cursorPos < 0 {
631 i.cursorPos = 0
632 } else if i.cursorPos > len(text) {
633 i.cursorPos = len(text)
634 }
635
636 var shiftLeft int
637 if i.offset > i.cursorPos {
638 i.offset = i.cursorPos
639 } else if subWidth := runewidth.StringWidth(string(text[i.offset:i.cursorPos])); subWidth > fieldWidth-1 {
640 shiftLeft = subWidth - fieldWidth + 1
641 }
642 currentOffset := i.offset
643 iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
644 if textPos >= currentOffset {
645 if shiftLeft > 0 {
646 i.offset = textPos + textWidth
647 shiftLeft -= screenWidth
648 } else {
649 if textPos+textWidth > i.cursorPos {
650 return true
651 }
652 cursorScreenPos += screenWidth
653 }
654 }
655 return false
656 })
657 drawnText = EscapeBytes(text[i.offset:])
658 Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor)
659 }
660
661 if i.maskCharacter == 0 && len(i.autocompleteListSuggestion) > 0 {
662 Print(screen, i.autocompleteListSuggestion, x+runewidth.StringWidth(string(drawnText)), y, fieldWidth-runewidth.StringWidth(string(drawnText)), AlignLeft, i.autocompleteSuggestionTextColor)
663 }
664 }
665
666
667 if len(i.fieldNote) > 0 {
668 Print(screen, i.fieldNote, x, y+1, fieldWidth, AlignLeft, i.fieldNoteTextColor)
669 }
670
671
672 if i.autocompleteList != nil {
673
674 lheight := i.autocompleteList.GetItemCount()
675 lwidth := 0
676 for index := 0; index < lheight; index++ {
677 entry, _ := i.autocompleteList.GetItemText(index)
678 width := TaggedStringWidth(entry)
679 if width > lwidth {
680 lwidth = width
681 }
682 }
683
684
685 lx := x
686 ly := y + 1
687 _, sheight := screen.Size()
688 if ly+lheight >= sheight && ly-2 > lheight-ly {
689 ly = y - lheight
690 if ly < 0 {
691 ly = 0
692 }
693 }
694 if ly+lheight >= sheight {
695 lheight = sheight - ly
696 }
697 if i.autocompleteList.scrollBarVisibility == ScrollBarAlways || (i.autocompleteList.scrollBarVisibility == ScrollBarAuto && i.autocompleteList.GetItemCount() > lheight) {
698 lwidth++
699 }
700 i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
701 i.autocompleteList.Draw(screen)
702 }
703
704
705 if i.focus.HasFocus() {
706 screen.ShowCursor(x+cursorScreenPos, y)
707 }
708 }
709
710
711 func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
712 return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
713 i.Lock()
714
715
716 currentText := i.text
717 defer func() {
718 i.Lock()
719 newText := i.text
720 i.Unlock()
721
722 if !bytes.Equal(newText, currentText) {
723 i.Autocomplete()
724 if i.changed != nil {
725 i.changed(string(i.text))
726 }
727 }
728 }()
729
730
731 home := func() { i.cursorPos = 0 }
732 end := func() { i.cursorPos = len(i.text) }
733 moveLeft := func() {
734 iterateStringReverse(string(i.text[:i.cursorPos]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
735 i.cursorPos -= textWidth
736 return true
737 })
738 }
739 moveRight := func() {
740 iterateString(string(i.text[i.cursorPos:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
741 i.cursorPos += textWidth
742 return true
743 })
744 }
745 moveWordLeft := func() {
746 i.cursorPos = len(regexRightWord.ReplaceAll(i.text[:i.cursorPos], nil))
747 }
748 moveWordRight := func() {
749 i.cursorPos = len(i.text) - len(regexLeftWord.ReplaceAll(i.text[i.cursorPos:], nil))
750 }
751
752
753
754 add := func(r rune) bool {
755 newText := append(append(i.text[:i.cursorPos], []byte(string(r))...), i.text[i.cursorPos:]...)
756 if i.accept != nil && !i.accept(string(newText), r) {
757 return false
758 }
759 i.text = newText
760 i.cursorPos += len(string(r))
761 return true
762 }
763
764
765 finish := func(key tcell.Key) {
766 if i.done != nil {
767 i.done(key)
768 }
769 if i.finished != nil {
770 i.finished(key)
771 }
772 }
773
774
775 switch key := event.Key(); key {
776 case tcell.KeyRune:
777 if event.Modifiers()&tcell.ModAlt > 0 {
778
779 switch event.Rune() {
780 case 'a':
781 home()
782 case 'e':
783 end()
784 case 'b':
785 moveWordLeft()
786 case 'f':
787 moveWordRight()
788 default:
789 if !add(event.Rune()) {
790 i.Unlock()
791 return
792 }
793 }
794 } else {
795
796 if !add(event.Rune()) {
797 i.Unlock()
798 return
799 }
800 }
801 case tcell.KeyCtrlU:
802 i.text = nil
803 i.cursorPos = 0
804 case tcell.KeyCtrlK:
805 i.text = i.text[:i.cursorPos]
806 case tcell.KeyCtrlW:
807 newText := append(regexRightWord.ReplaceAll(i.text[:i.cursorPos], nil), i.text[i.cursorPos:]...)
808 i.cursorPos -= len(i.text) - len(newText)
809 i.text = newText
810 case tcell.KeyBackspace, tcell.KeyBackspace2:
811 iterateStringReverse(string(i.text[:i.cursorPos]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
812 i.text = append(i.text[:textPos], i.text[textPos+textWidth:]...)
813 i.cursorPos -= textWidth
814 return true
815 })
816 if i.offset >= i.cursorPos {
817 i.offset = 0
818 }
819 case tcell.KeyDelete:
820 iterateString(string(i.text[i.cursorPos:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
821 i.text = append(i.text[:i.cursorPos], i.text[i.cursorPos+textWidth:]...)
822 return true
823 })
824 case tcell.KeyLeft:
825 if event.Modifiers()&tcell.ModAlt > 0 {
826 moveWordLeft()
827 } else {
828 moveLeft()
829 }
830 case tcell.KeyRight:
831 if event.Modifiers()&tcell.ModAlt > 0 {
832 moveWordRight()
833 } else {
834 moveRight()
835 }
836 case tcell.KeyHome, tcell.KeyCtrlA:
837 home()
838 case tcell.KeyEnd, tcell.KeyCtrlE:
839 end()
840 case tcell.KeyEnter:
841 if i.autocompleteList != nil {
842 currentItem := i.autocompleteList.GetCurrentItem()
843 selectionText := currentItem.GetMainText()
844 if currentItem.GetSecondaryText() != "" {
845 selectionText = currentItem.GetSecondaryText()
846 }
847 i.Unlock()
848 i.SetText(selectionText)
849 i.Lock()
850 i.autocompleteList = nil
851 i.autocompleteListSuggestion = nil
852 i.Unlock()
853 } else {
854 i.Unlock()
855 finish(key)
856 }
857 return
858 case tcell.KeyEscape:
859 if i.autocompleteList != nil {
860 i.autocompleteList = nil
861 i.autocompleteListSuggestion = nil
862 i.Unlock()
863 } else {
864 i.Unlock()
865 finish(key)
866 }
867 return
868 case tcell.KeyDown, tcell.KeyTab:
869 if i.autocompleteList != nil {
870 count := i.autocompleteList.GetItemCount()
871 newEntry := i.autocompleteList.GetCurrentItemIndex() + 1
872 if newEntry >= count {
873 newEntry = 0
874 }
875 i.autocompleteList.SetCurrentItem(newEntry)
876 i.Unlock()
877 } else {
878 i.Unlock()
879 finish(key)
880 }
881 return
882 case tcell.KeyUp, tcell.KeyBacktab:
883 if i.autocompleteList != nil {
884 newEntry := i.autocompleteList.GetCurrentItemIndex() - 1
885 if newEntry < 0 {
886 newEntry = i.autocompleteList.GetItemCount() - 1
887 }
888 i.autocompleteList.SetCurrentItem(newEntry)
889 i.Unlock()
890 } else {
891 i.Unlock()
892 finish(key)
893 }
894 return
895 }
896
897 i.Unlock()
898 })
899 }
900
901
902 func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
903 return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
904 x, y := event.Position()
905 _, rectY, _, _ := i.GetInnerRect()
906 if !i.InRect(x, y) {
907 return false, nil
908 }
909
910
911 if action == MouseLeftClick && y == rectY {
912
913 if x >= i.fieldX {
914 if !iterateString(string(i.text), func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
915 if x-i.fieldX < screenPos+screenWidth {
916 i.cursorPos = textPos
917 return true
918 }
919 return false
920 }) {
921 i.cursorPos = len(i.text)
922 }
923 }
924 setFocus(i)
925 consumed = true
926 }
927
928 return
929 })
930 }
931
932 var (
933 regexRightWord = regexp.MustCompile(`(\w*|\W)$`)
934 regexLeftWord = regexp.MustCompile(`^(\W|\w*)`)
935 )
936
View as plain text