1 package tview
2
3 import (
4 "fmt"
5 "strings"
6
7 "github.com/gdamore/tcell/v2"
8 )
9
10
11 type listItem struct {
12 MainText string
13 SecondaryText string
14 Shortcut rune
15 Selected func()
16 }
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 type List struct {
39 *Box
40
41
42 items []*listItem
43
44
45 currentItem int
46
47
48 showSecondaryText bool
49
50
51 mainTextStyle tcell.Style
52
53
54 secondaryTextStyle tcell.Style
55
56
57 shortcutStyle tcell.Style
58
59
60 selectedStyle tcell.Style
61
62
63 selectedFocusOnly bool
64
65
66 highlightFullLine bool
67
68
69 wrapAround bool
70
71
72
73 itemOffset int
74
75
76
77 horizontalOffset int
78
79
80
81
82 overflowing bool
83
84
85
86 changed func(index int, mainText, secondaryText string, shortcut rune)
87
88
89
90 selected func(index int, mainText, secondaryText string, shortcut rune)
91
92
93 done func()
94 }
95
96
97 func NewList() *List {
98 return &List{
99 Box: NewBox(),
100 showSecondaryText: true,
101 wrapAround: true,
102 mainTextStyle: tcell.StyleDefault.Foreground(Styles.PrimaryTextColor),
103 secondaryTextStyle: tcell.StyleDefault.Foreground(Styles.TertiaryTextColor),
104 shortcutStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
105 selectedStyle: tcell.StyleDefault.Foreground(Styles.PrimitiveBackgroundColor).Background(Styles.PrimaryTextColor),
106 }
107 }
108
109
110
111
112
113
114
115 func (l *List) SetCurrentItem(index int) *List {
116 if index < 0 {
117 index = len(l.items) + index
118 }
119 if index >= len(l.items) {
120 index = len(l.items) - 1
121 }
122 if index < 0 {
123 index = 0
124 }
125
126 if index != l.currentItem && l.changed != nil {
127 item := l.items[index]
128 l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
129 }
130
131 l.currentItem = index
132
133 l.adjustOffset()
134
135 return l
136 }
137
138
139
140 func (l *List) GetCurrentItem() int {
141 return l.currentItem
142 }
143
144
145
146
147
148
149
150
151
152 func (l *List) SetOffset(items, horizontal int) *List {
153 l.itemOffset = items
154 l.horizontalOffset = horizontal
155 return l
156 }
157
158
159
160
161 func (l *List) GetOffset() (int, int) {
162 return l.itemOffset, l.horizontalOffset
163 }
164
165
166
167
168
169
170
171
172
173 func (l *List) RemoveItem(index int) *List {
174 if len(l.items) == 0 {
175 return l
176 }
177
178
179 if index < 0 {
180 index = len(l.items) + index
181 }
182 if index >= len(l.items) {
183 index = len(l.items) - 1
184 }
185 if index < 0 {
186 index = 0
187 }
188
189
190 l.items = append(l.items[:index], l.items[index+1:]...)
191
192
193 if len(l.items) == 0 {
194 return l
195 }
196
197
198 previousCurrentItem := l.currentItem
199 if l.currentItem > index || l.currentItem == len(l.items) {
200 l.currentItem--
201 }
202
203
204 if previousCurrentItem == index && l.changed != nil {
205 item := l.items[l.currentItem]
206 l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
207 }
208
209 return l
210 }
211
212
213 func (l *List) SetMainTextColor(color tcell.Color) *List {
214 l.mainTextStyle = l.mainTextStyle.Foreground(color)
215 return l
216 }
217
218
219
220
221 func (l *List) SetMainTextStyle(style tcell.Style) *List {
222 l.mainTextStyle = style
223 return l
224 }
225
226
227 func (l *List) SetSecondaryTextColor(color tcell.Color) *List {
228 l.secondaryTextStyle = l.secondaryTextStyle.Foreground(color)
229 return l
230 }
231
232
233
234
235 func (l *List) SetSecondaryTextStyle(style tcell.Style) *List {
236 l.secondaryTextStyle = style
237 return l
238 }
239
240
241 func (l *List) SetShortcutColor(color tcell.Color) *List {
242 l.shortcutStyle = l.shortcutStyle.Foreground(color)
243 return l
244 }
245
246
247
248
249 func (l *List) SetShortcutStyle(style tcell.Style) *List {
250 l.shortcutStyle = style
251 return l
252 }
253
254
255
256
257 func (l *List) SetSelectedTextColor(color tcell.Color) *List {
258 l.selectedStyle = l.selectedStyle.Foreground(color)
259 return l
260 }
261
262
263 func (l *List) SetSelectedBackgroundColor(color tcell.Color) *List {
264 l.selectedStyle = l.selectedStyle.Background(color)
265 return l
266 }
267
268
269
270
271 func (l *List) SetSelectedStyle(style tcell.Style) *List {
272 l.selectedStyle = style
273 return l
274 }
275
276
277
278
279 func (l *List) SetSelectedFocusOnly(focusOnly bool) *List {
280 l.selectedFocusOnly = focusOnly
281 return l
282 }
283
284
285
286
287
288 func (l *List) SetHighlightFullLine(highlight bool) *List {
289 l.highlightFullLine = highlight
290 return l
291 }
292
293
294 func (l *List) ShowSecondaryText(show bool) *List {
295 l.showSecondaryText = show
296 return l
297 }
298
299
300
301
302
303
304 func (l *List) SetWrapAround(wrapAround bool) *List {
305 l.wrapAround = wrapAround
306 return l
307 }
308
309
310
311
312
313
314
315 func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List {
316 l.changed = handler
317 return l
318 }
319
320
321
322
323
324 func (l *List) SetSelectedFunc(handler func(int, string, string, rune)) *List {
325 l.selected = handler
326 return l
327 }
328
329
330
331 func (l *List) SetDoneFunc(handler func()) *List {
332 l.done = handler
333 return l
334 }
335
336
337 func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List {
338 l.InsertItem(-1, mainText, secondaryText, shortcut, selected)
339 return l
340 }
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364 func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut rune, selected func()) *List {
365 item := &listItem{
366 MainText: mainText,
367 SecondaryText: secondaryText,
368 Shortcut: shortcut,
369 Selected: selected,
370 }
371
372
373 if index < 0 {
374 index = len(l.items) + index + 1
375 }
376 if index < 0 {
377 index = 0
378 } else if index > len(l.items) {
379 index = len(l.items)
380 }
381
382
383 if l.currentItem < len(l.items) && l.currentItem >= index {
384 l.currentItem++
385 }
386
387
388 l.items = append(l.items, nil)
389 if index < len(l.items)-1 {
390 copy(l.items[index+1:], l.items[index:])
391 }
392 l.items[index] = item
393
394
395 if len(l.items) == 1 && l.changed != nil {
396 item := l.items[0]
397 l.changed(0, item.MainText, item.SecondaryText, item.Shortcut)
398 }
399
400 return l
401 }
402
403
404 func (l *List) GetItemCount() int {
405 return len(l.items)
406 }
407
408
409
410 func (l *List) GetItemText(index int) (main, secondary string) {
411 return l.items[index].MainText, l.items[index].SecondaryText
412 }
413
414
415
416 func (l *List) SetItemText(index int, main, secondary string) *List {
417 item := l.items[index]
418 item.MainText = main
419 item.SecondaryText = secondary
420 return l
421 }
422
423
424
425
426
427
428
429
430
431
432
433 func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
434 if mainSearch == "" && secondarySearch == "" {
435 return
436 }
437
438 if ignoreCase {
439 mainSearch = strings.ToLower(mainSearch)
440 secondarySearch = strings.ToLower(secondarySearch)
441 }
442
443 for index, item := range l.items {
444 mainText := item.MainText
445 secondaryText := item.SecondaryText
446 if ignoreCase {
447 mainText = strings.ToLower(mainText)
448 secondaryText = strings.ToLower(secondaryText)
449 }
450
451
452 mainContained := strings.Contains(mainText, mainSearch)
453 secondaryContained := strings.Contains(secondaryText, secondarySearch)
454 if mustContainBoth && mainContained && secondaryContained ||
455 !mustContainBoth && (mainText != "" && mainContained || secondaryText != "" && secondaryContained) {
456 indices = append(indices, index)
457 }
458 }
459
460 return
461 }
462
463
464 func (l *List) Clear() *List {
465 l.items = nil
466 l.currentItem = 0
467 return l
468 }
469
470
471 func (l *List) Draw(screen tcell.Screen) {
472 l.Box.DrawForSubclass(screen, l)
473
474
475 x, y, width, height := l.GetInnerRect()
476 bottomLimit := y + height
477 _, totalHeight := screen.Size()
478 if bottomLimit > totalHeight {
479 bottomLimit = totalHeight
480 }
481
482
483 var showShortcuts bool
484 for _, item := range l.items {
485 if item.Shortcut != 0 {
486 showShortcuts = true
487 x += 4
488 width -= 4
489 break
490 }
491 }
492
493 if l.horizontalOffset < 0 {
494 l.horizontalOffset = 0
495 }
496
497
498 var (
499 maxWidth int
500 overflowing bool
501 )
502 for index, item := range l.items {
503 if index < l.itemOffset {
504 continue
505 }
506
507 if y >= bottomLimit {
508 break
509 }
510
511
512 if showShortcuts && item.Shortcut != 0 {
513 printWithStyle(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 0, 4, AlignRight, l.shortcutStyle, true)
514 }
515
516
517 _, printedWidth, _, end := printWithStyle(screen, item.MainText, x, y, l.horizontalOffset, width, AlignLeft, l.mainTextStyle, true)
518 if printedWidth > maxWidth {
519 maxWidth = printedWidth
520 }
521 if end < len(item.MainText) {
522 overflowing = true
523 }
524
525
526 if index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus()) {
527 textWidth := width
528 if !l.highlightFullLine {
529 if w := TaggedStringWidth(item.MainText); w < textWidth {
530 textWidth = w
531 }
532 }
533
534 mainTextColor, _, _ := l.mainTextStyle.Decompose()
535 for bx := 0; bx < textWidth; bx++ {
536 m, c, style, _ := screen.GetContent(x+bx, y)
537 fg, _, _ := style.Decompose()
538 style = l.selectedStyle
539 if fg != mainTextColor {
540 style = style.Foreground(fg)
541 }
542 screen.SetContent(x+bx, y, m, c, style)
543 }
544 }
545
546 y++
547
548 if y >= bottomLimit {
549 break
550 }
551
552
553 if l.showSecondaryText {
554 _, printedWidth, _, end := printWithStyle(screen, item.SecondaryText, x, y, l.horizontalOffset, width, AlignLeft, l.secondaryTextStyle, true)
555 if printedWidth > maxWidth {
556 maxWidth = printedWidth
557 }
558 if end < len(item.SecondaryText) {
559 overflowing = true
560 }
561 y++
562 }
563 }
564
565
566
567
568 if l.horizontalOffset > 0 && maxWidth < width {
569 l.horizontalOffset -= width - maxWidth
570 l.Draw(screen)
571 }
572 l.overflowing = overflowing
573 }
574
575
576
577 func (l *List) adjustOffset() {
578 _, _, _, height := l.GetInnerRect()
579 if height == 0 {
580 return
581 }
582 if l.currentItem < l.itemOffset {
583 l.itemOffset = l.currentItem
584 } else if l.showSecondaryText {
585 if 2*(l.currentItem-l.itemOffset) >= height-1 {
586 l.itemOffset = (2*l.currentItem + 3 - height) / 2
587 }
588 } else {
589 if l.currentItem-l.itemOffset >= height {
590 l.itemOffset = l.currentItem + 1 - height
591 }
592 }
593 }
594
595
596 func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
597 return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
598 if event.Key() == tcell.KeyEscape {
599 if l.done != nil {
600 l.done()
601 }
602 return
603 } else if len(l.items) == 0 {
604 return
605 }
606
607 previousItem := l.currentItem
608
609 switch key := event.Key(); key {
610 case tcell.KeyTab, tcell.KeyDown:
611 l.currentItem++
612 case tcell.KeyBacktab, tcell.KeyUp:
613 l.currentItem--
614 case tcell.KeyRight:
615 if l.overflowing {
616 l.horizontalOffset += 2
617 } else {
618 l.currentItem++
619 }
620 case tcell.KeyLeft:
621 if l.horizontalOffset > 0 {
622 l.horizontalOffset -= 2
623 } else {
624 l.currentItem--
625 }
626 case tcell.KeyHome:
627 l.currentItem = 0
628 case tcell.KeyEnd:
629 l.currentItem = len(l.items) - 1
630 case tcell.KeyPgDn:
631 _, _, _, height := l.GetInnerRect()
632 l.currentItem += height
633 if l.currentItem >= len(l.items) {
634 l.currentItem = len(l.items) - 1
635 }
636 case tcell.KeyPgUp:
637 _, _, _, height := l.GetInnerRect()
638 l.currentItem -= height
639 if l.currentItem < 0 {
640 l.currentItem = 0
641 }
642 case tcell.KeyEnter:
643 if l.currentItem >= 0 && l.currentItem < len(l.items) {
644 item := l.items[l.currentItem]
645 if item.Selected != nil {
646 item.Selected()
647 }
648 if l.selected != nil {
649 l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
650 }
651 }
652 case tcell.KeyRune:
653 ch := event.Rune()
654 if ch != ' ' {
655
656 var found bool
657 for index, item := range l.items {
658 if item.Shortcut == ch {
659
660 found = true
661 l.currentItem = index
662 break
663 }
664 }
665 if !found {
666 break
667 }
668 }
669 item := l.items[l.currentItem]
670 if item.Selected != nil {
671 item.Selected()
672 }
673 if l.selected != nil {
674 l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
675 }
676 }
677
678 if l.currentItem < 0 {
679 if l.wrapAround {
680 l.currentItem = len(l.items) - 1
681 } else {
682 l.currentItem = 0
683 }
684 } else if l.currentItem >= len(l.items) {
685 if l.wrapAround {
686 l.currentItem = 0
687 } else {
688 l.currentItem = len(l.items) - 1
689 }
690 }
691
692 if l.currentItem != previousItem && l.currentItem < len(l.items) {
693 if l.changed != nil {
694 item := l.items[l.currentItem]
695 l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
696 }
697 l.adjustOffset()
698 }
699 })
700 }
701
702
703
704 func (l *List) indexAtPoint(x, y int) int {
705 rectX, rectY, width, height := l.GetInnerRect()
706 if rectX < 0 || rectX >= rectX+width || y < rectY || y >= rectY+height {
707 return -1
708 }
709
710 index := y - rectY
711 if l.showSecondaryText {
712 index /= 2
713 }
714 index += l.itemOffset
715
716 if index >= len(l.items) {
717 return -1
718 }
719 return index
720 }
721
722
723 func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
724 return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
725 if !l.InRect(event.Position()) {
726 return false, nil
727 }
728
729
730 switch action {
731 case MouseLeftClick:
732 setFocus(l)
733 index := l.indexAtPoint(event.Position())
734 if index != -1 {
735 item := l.items[index]
736 if item.Selected != nil {
737 item.Selected()
738 }
739 if l.selected != nil {
740 l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
741 }
742 if index != l.currentItem {
743 if l.changed != nil {
744 l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
745 }
746 l.adjustOffset()
747 }
748 l.currentItem = index
749 }
750 consumed = true
751 case MouseScrollUp:
752 if l.itemOffset > 0 {
753 l.itemOffset--
754 }
755 consumed = true
756 case MouseScrollDown:
757 lines := len(l.items) - l.itemOffset
758 if l.showSecondaryText {
759 lines *= 2
760 }
761 if _, _, _, height := l.GetInnerRect(); lines > height {
762 l.itemOffset++
763 }
764 consumed = true
765 }
766
767 return
768 })
769 }
770
View as plain text