1 package tview
2
3 import (
4 "bytes"
5 "fmt"
6 "regexp"
7 "strings"
8 "sync"
9 "unicode/utf8"
10
11 "github.com/gdamore/tcell/v2"
12 colorful "github.com/lucasb-eyer/go-colorful"
13 "github.com/rivo/uniseg"
14 )
15
16 var (
17 openColorRegex = regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`)
18 openRegionRegex = regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`)
19 newLineRegex = regexp.MustCompile(`\r?\n`)
20
21
22 TabSize = 4
23 )
24
25
26 type textViewIndex struct {
27 Line int
28 Pos int
29 NextPos int
30 Width int
31 ForegroundColor string
32 BackgroundColor string
33 Attributes string
34 Region string
35 }
36
37
38 type textViewRegion struct {
39
40 ID string
41
42
43
44 FromX, FromY, ToX, ToY int
45 }
46
47
48
49
50
51 type TextViewWriter struct {
52 t *TextView
53 }
54
55
56 func (w TextViewWriter) Close() error {
57 w.t.Unlock()
58 return nil
59 }
60
61
62 func (w TextViewWriter) Clear() {
63 w.t.clear()
64 }
65
66
67
68 func (w TextViewWriter) Write(p []byte) (n int, err error) {
69 return w.t.write(p)
70 }
71
72
73 func (w TextViewWriter) HasFocus() bool {
74 return w.t.hasFocus
75 }
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141 type TextView struct {
142 sync.Mutex
143 *Box
144
145
146
147 width, height int
148
149
150 buffer []string
151
152
153 recentBytes []byte
154
155
156
157 index []*textViewIndex
158
159
160 label string
161
162
163 labelWidth int
164
165
166 labelStyle tcell.Style
167
168
169 align int
170
171
172 regionInfos []*textViewRegion
173
174
175
176
177 fromHighlight, toHighlight int
178
179
180
181 posHighlight int
182
183
184 highlights map[string]struct{}
185
186
187 lastWidth int
188
189
190 longestLine int
191
192
193 lineOffset int
194
195
196 trackEnd bool
197
198
199
200 columnOffset int
201
202
203
204 maxLines int
205
206
207 pageSize int
208
209
210
211 scrollable bool
212
213
214
215
216 wrap bool
217
218
219
220 wordWrap bool
221
222
223
224 textStyle tcell.Style
225
226
227
228 dynamicColors bool
229
230
231 regions bool
232
233
234
235 scrollToHighlights bool
236
237
238
239 toggleHighlights bool
240
241
242
243 changed func()
244
245
246
247 done func(tcell.Key)
248
249
250
251 highlighted func(added, removed, remaining []string)
252
253
254
255 finished func(tcell.Key)
256 }
257
258
259 func NewTextView() *TextView {
260 return &TextView{
261 Box: NewBox(),
262 labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
263 highlights: make(map[string]struct{}),
264 lineOffset: -1,
265 scrollable: true,
266 align: AlignLeft,
267 wrap: true,
268 textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor),
269 regions: false,
270 dynamicColors: false,
271 }
272 }
273
274
275 func (t *TextView) SetLabel(label string) *TextView {
276 t.label = label
277 return t
278 }
279
280
281 func (t *TextView) GetLabel() string {
282 return t.label
283 }
284
285
286
287 func (t *TextView) SetLabelWidth(width int) *TextView {
288 t.labelWidth = width
289 return t
290 }
291
292
293
294
295
296 func (t *TextView) SetSize(rows, columns int) *TextView {
297 t.width = columns
298 t.height = rows
299 return t
300 }
301
302
303 func (t *TextView) GetFieldWidth() int {
304 return t.width
305 }
306
307
308 func (t *TextView) GetFieldHeight() int {
309 return t.height
310 }
311
312
313 func (t *TextView) SetDisabled(disabled bool) FormItem {
314 return t
315 }
316
317
318
319
320 func (t *TextView) SetScrollable(scrollable bool) *TextView {
321 t.scrollable = scrollable
322 if !scrollable {
323 t.trackEnd = true
324 }
325 return t
326 }
327
328
329
330
331 func (t *TextView) SetWrap(wrap bool) *TextView {
332 if t.wrap != wrap {
333 t.index = nil
334 }
335 t.wrap = wrap
336 return t
337 }
338
339
340
341
342
343
344 func (t *TextView) SetWordWrap(wrapOnWords bool) *TextView {
345 if t.wordWrap != wrapOnWords {
346 t.index = nil
347 }
348 t.wordWrap = wrapOnWords
349 return t
350 }
351
352
353
354
355
356
357
358
359
360
361 func (t *TextView) SetMaxLines(maxLines int) *TextView {
362 t.maxLines = maxLines
363 return t
364 }
365
366
367
368 func (t *TextView) SetTextAlign(align int) *TextView {
369 if t.align != align {
370 t.index = nil
371 }
372 t.align = align
373 return t
374 }
375
376
377
378
379 func (t *TextView) SetTextColor(color tcell.Color) *TextView {
380 t.textStyle = t.textStyle.Foreground(color)
381 return t
382 }
383
384
385
386
387 func (t *TextView) SetBackgroundColor(color tcell.Color) *Box {
388 t.Box.SetBackgroundColor(color)
389 t.textStyle = t.textStyle.Background(color)
390 return t.Box
391 }
392
393
394
395
396
397 func (t *TextView) SetTextStyle(style tcell.Style) *TextView {
398 t.textStyle = style
399 return t
400 }
401
402
403
404
405
406 func (t *TextView) SetText(text string) *TextView {
407 batch := t.BatchWriter()
408 defer batch.Close()
409
410 batch.Clear()
411 fmt.Fprint(batch, text)
412 return t
413 }
414
415
416
417 func (t *TextView) GetText(stripAllTags bool) string {
418
419 buffer := t.buffer
420 if !stripAllTags {
421 buffer = make([]string, len(t.buffer), len(t.buffer)+1)
422 copy(buffer, t.buffer)
423 buffer = append(buffer, string(t.recentBytes))
424 }
425
426
427 text := strings.Join(buffer, "\n")
428
429
430 if stripAllTags {
431 if t.regions {
432 text = regionPattern.ReplaceAllString(text, "")
433 }
434 if t.dynamicColors {
435 text = stripTags(text)
436 }
437 if t.regions && !t.dynamicColors {
438 text = escapePattern.ReplaceAllString(text, `[$1$2]`)
439 }
440 }
441
442 return text
443 }
444
445
446
447 func (t *TextView) GetOriginalLineCount() int {
448 return len(t.buffer)
449 }
450
451
452
453 func (t *TextView) SetDynamicColors(dynamic bool) *TextView {
454 if t.dynamicColors != dynamic {
455 t.index = nil
456 }
457 t.dynamicColors = dynamic
458 return t
459 }
460
461
462
463 func (t *TextView) SetRegions(regions bool) *TextView {
464 if t.regions != regions {
465 t.index = nil
466 }
467 t.regions = regions
468 return t
469 }
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487 func (t *TextView) SetChangedFunc(handler func()) *TextView {
488 t.changed = handler
489 return t
490 }
491
492
493
494
495 func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
496 t.done = handler
497 return t
498 }
499
500
501
502
503
504
505
506
507 func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []string)) *TextView {
508 t.highlighted = handler
509 return t
510 }
511
512
513 func (t *TextView) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
514 t.finished = handler
515 return t
516 }
517
518
519 func (t *TextView) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
520 t.labelWidth = labelWidth
521 t.backgroundColor = bgColor
522 t.labelStyle = t.labelStyle.Foreground(labelColor)
523
524 t.textStyle = tcell.StyleDefault.Foreground(fieldTextColor).Background(bgColor)
525 return t
526 }
527
528
529 func (t *TextView) ScrollTo(row, column int) *TextView {
530 if !t.scrollable {
531 return t
532 }
533 t.lineOffset = row
534 t.columnOffset = column
535 t.trackEnd = false
536 return t
537 }
538
539
540
541 func (t *TextView) ScrollToBeginning() *TextView {
542 if !t.scrollable {
543 return t
544 }
545 t.trackEnd = false
546 t.lineOffset = 0
547 t.columnOffset = 0
548 return t
549 }
550
551
552
553
554 func (t *TextView) ScrollToEnd() *TextView {
555 if !t.scrollable {
556 return t
557 }
558 t.trackEnd = true
559 t.columnOffset = 0
560 return t
561 }
562
563
564
565 func (t *TextView) GetScrollOffset() (row, column int) {
566 return t.lineOffset, t.columnOffset
567 }
568
569
570 func (t *TextView) Clear() *TextView {
571 t.Lock()
572 defer t.Unlock()
573
574 t.clear()
575 return t
576 }
577
578
579
580 func (t *TextView) clear() {
581 t.buffer = nil
582 t.recentBytes = nil
583 t.index = nil
584 }
585
586
587
588
589
590
591
592
593
594
595
596
597
598 func (t *TextView) Highlight(regionIDs ...string) *TextView {
599
600 if t.toggleHighlights {
601 var newIDs []string
602 HighlightLoop:
603 for regionID := range t.highlights {
604 for _, id := range regionIDs {
605 if regionID == id {
606 continue HighlightLoop
607 }
608 }
609 newIDs = append(newIDs, regionID)
610 }
611 for _, regionID := range regionIDs {
612 if _, ok := t.highlights[regionID]; !ok {
613 newIDs = append(newIDs, regionID)
614 }
615 }
616 regionIDs = newIDs
617 }
618
619
620 var added, removed, remaining []string
621 if t.highlighted != nil {
622 for _, regionID := range regionIDs {
623 if _, ok := t.highlights[regionID]; ok {
624 remaining = append(remaining, regionID)
625 delete(t.highlights, regionID)
626 } else {
627 added = append(added, regionID)
628 }
629 }
630 for regionID := range t.highlights {
631 removed = append(removed, regionID)
632 }
633 }
634
635
636 t.highlights = make(map[string]struct{})
637 for _, id := range regionIDs {
638 if id == "" {
639 continue
640 }
641 t.highlights[id] = struct{}{}
642 }
643 t.index = nil
644
645
646 if t.highlighted != nil && len(added) > 0 || len(removed) > 0 {
647 t.highlighted(added, removed, remaining)
648 }
649
650 return t
651 }
652
653
654 func (t *TextView) GetHighlights() (regionIDs []string) {
655 for id := range t.highlights {
656 regionIDs = append(regionIDs, id)
657 }
658 return
659 }
660
661
662
663
664
665 func (t *TextView) SetToggleHighlights(toggle bool) *TextView {
666 t.toggleHighlights = toggle
667 return t
668 }
669
670
671
672
673
674
675
676
677
678 func (t *TextView) ScrollToHighlight() *TextView {
679 if len(t.highlights) == 0 || !t.scrollable || !t.regions {
680 return t
681 }
682 t.index = nil
683 t.scrollToHighlights = true
684 t.trackEnd = false
685 return t
686 }
687
688
689
690
691
692
693
694 func (t *TextView) GetRegionText(regionID string) string {
695 if !t.regions || regionID == "" {
696 return ""
697 }
698
699 var (
700 buffer bytes.Buffer
701 currentRegionID string
702 )
703
704 for _, str := range t.buffer {
705
706 var colorTagIndices [][]int
707 if t.dynamicColors {
708 colorTagIndices = colorPattern.FindAllStringIndex(str, -1)
709 }
710
711
712 var (
713 regionIndices [][]int
714 regions [][]string
715 )
716 if t.regions {
717 regionIndices = regionPattern.FindAllStringIndex(str, -1)
718 regions = regionPattern.FindAllStringSubmatch(str, -1)
719 }
720
721
722 var currentTag, currentRegion int
723 for pos, ch := range str {
724
725 if currentTag < len(colorTagIndices) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
726 tag := currentTag
727 if pos == colorTagIndices[tag][1]-1 {
728 currentTag++
729 }
730 if colorTagIndices[tag][1]-colorTagIndices[tag][0] > 2 {
731 continue
732 }
733 }
734
735
736 if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
737 if pos == regionIndices[currentRegion][1]-1 {
738 if currentRegionID == regionID {
739
740 return buffer.String()
741 }
742 currentRegionID = regions[currentRegion][1]
743 currentRegion++
744 }
745 continue
746 }
747
748
749 if currentRegionID == regionID {
750 buffer.WriteRune(ch)
751 }
752 }
753
754
755 if currentRegionID == regionID {
756 buffer.WriteRune('\n')
757 }
758 }
759
760 return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`)
761 }
762
763
764 func (t *TextView) Focus(delegate func(p Primitive)) {
765
766 t.Lock()
767 defer t.Unlock()
768
769
770
771 if t.finished != nil && !t.scrollable {
772 t.finished(-1)
773 return
774 }
775
776 t.Box.Focus(delegate)
777 }
778
779
780 func (t *TextView) HasFocus() bool {
781
782
783 t.Lock()
784 defer t.Unlock()
785 return t.Box.HasFocus()
786 }
787
788
789
790
791 func (t *TextView) Write(p []byte) (n int, err error) {
792 t.Lock()
793 defer t.Unlock()
794
795 return t.write(p)
796 }
797
798
799
800 func (t *TextView) write(p []byte) (n int, err error) {
801
802 changed := t.changed
803 if changed != nil {
804 defer func() {
805
806
807 go changed()
808 }()
809 }
810
811
812 newBytes := append(t.recentBytes, p...)
813 t.recentBytes = nil
814
815
816 if r, _ := utf8.DecodeLastRune(p); r == utf8.RuneError {
817 t.recentBytes = newBytes
818 return len(p), nil
819 }
820
821
822 if t.dynamicColors {
823 location := openColorRegex.FindIndex(newBytes)
824 if location != nil {
825 t.recentBytes = newBytes[location[0]:]
826 newBytes = newBytes[:location[0]]
827 }
828 }
829
830
831 if t.regions {
832 location := openRegionRegex.FindIndex(newBytes)
833 if location != nil {
834 t.recentBytes = newBytes[location[0]:]
835 newBytes = newBytes[:location[0]]
836 }
837 }
838
839
840 newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)
841 for index, line := range newLineRegex.Split(string(newBytes), -1) {
842 if index == 0 {
843 if len(t.buffer) == 0 {
844 t.buffer = []string{line}
845 } else {
846 t.buffer[len(t.buffer)-1] += line
847 }
848 } else {
849 t.buffer = append(t.buffer, line)
850 }
851 }
852
853
854 t.index = nil
855
856 return len(p), nil
857 }
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876 func (t *TextView) BatchWriter() TextViewWriter {
877 t.Lock()
878 return TextViewWriter{
879 t: t,
880 }
881 }
882
883
884
885
886
887
888
889
890 func (t *TextView) reindexBuffer(width int) {
891 if t.index != nil {
892 return
893 }
894 t.index = nil
895 t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1
896
897
898 if width < 1 {
899 return
900 }
901
902
903 regionID := ""
904 var (
905 highlighted bool
906 foregroundColor, backgroundColor, attributes string
907 )
908
909
910 for bufferIndex, str := range t.buffer {
911 colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeString(str, t.dynamicColors, t.regions)
912
913
914 var splitLines []string
915 str = strippedStr
916 if t.wrap && len(str) > 0 {
917 for len(str) > 0 {
918
919 var splitPos, clusterWidth, lineWidth int
920 state := -1
921 remaining := str
922 for splitPos == 0 || len(remaining) > 0 {
923 var cluster string
924 cluster, remaining, clusterWidth, state = uniseg.FirstGraphemeClusterInString(remaining, state)
925 lineWidth += clusterWidth
926 if splitPos > 0 && lineWidth > width {
927 break
928 }
929 splitPos += len(cluster)
930 }
931 extract := str[:splitPos]
932
933 if t.wordWrap && len(extract) < len(str) {
934
935 if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 {
936 extract = str[:len(extract)+spaces[1]]
937 }
938
939
940 matches := boundaryPattern.FindAllStringIndex(extract, -1)
941 if len(matches) > 0 {
942
943 extract = extract[:matches[len(matches)-1][1]]
944 }
945 }
946 splitLines = append(splitLines, extract)
947 str = str[len(extract):]
948 }
949 } else {
950
951 splitLines = []string{str}
952 }
953
954
955 var originalPos, colorPos, regionPos, escapePos int
956 for _, splitLine := range splitLines {
957 line := &textViewIndex{
958 Line: bufferIndex,
959 Pos: originalPos,
960 ForegroundColor: foregroundColor,
961 BackgroundColor: backgroundColor,
962 Attributes: attributes,
963 Region: regionID,
964 }
965
966
967 lineLength := len(splitLine)
968 remainingLength := lineLength
969 tagEnd := originalPos
970 totalTagLength := 0
971 for {
972
973 nextTag := make([][3]int, 0, 3)
974 if colorPos < len(colorTagIndices) {
975 nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0})
976 }
977 if regionPos < len(regionIndices) {
978 nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1})
979 }
980 if escapePos < len(escapeIndices) {
981 nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2})
982 }
983 minPos := -1
984 tagIndex := -1
985 for index, pair := range nextTag {
986 if minPos < 0 || pair[0] < minPos {
987 minPos = pair[0]
988 tagIndex = index
989 }
990 }
991
992
993 if tagIndex < 0 || minPos > tagEnd+remainingLength {
994 break
995 }
996
997
998 strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength
999 tagEnd = nextTag[tagIndex][1]
1000 tagLength := tagEnd - nextTag[tagIndex][0]
1001 if nextTag[tagIndex][2] == 2 {
1002 tagLength = 1
1003 }
1004 totalTagLength += tagLength
1005 remainingLength = lineLength - (tagEnd - originalPos - totalTagLength)
1006
1007
1008 switch nextTag[tagIndex][2] {
1009 case 0:
1010
1011 foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
1012 colorPos++
1013 case 1:
1014
1015 regionID = regions[regionPos][1]
1016 _, highlighted = t.highlights[regionID]
1017
1018
1019 if highlighted {
1020 line := len(t.index)
1021 if t.fromHighlight < 0 {
1022 t.fromHighlight, t.toHighlight = line, line
1023 t.posHighlight = uniseg.StringWidth(splitLine[:strippedTagStart])
1024 } else if line > t.toHighlight {
1025 t.toHighlight = line
1026 }
1027 }
1028
1029 regionPos++
1030 case 2:
1031
1032 escapePos++
1033 }
1034 }
1035
1036
1037 originalPos += lineLength + totalTagLength
1038
1039
1040 line.NextPos = originalPos
1041 line.Width = uniseg.StringWidth(splitLine)
1042 t.index = append(t.index, line)
1043 }
1044
1045
1046 if t.wrap && t.wordWrap {
1047 for _, line := range t.index {
1048 str := t.buffer[line.Line][line.Pos:line.NextPos]
1049 spaces := spacePattern.FindAllStringIndex(str, -1)
1050 if spaces != nil && spaces[len(spaces)-1][1] == len(str) {
1051 oldNextPos := line.NextPos
1052 line.NextPos -= spaces[len(spaces)-1][1] - spaces[len(spaces)-1][0]
1053 line.Width -= uniseg.StringWidth(t.buffer[line.Line][line.NextPos:oldNextPos])
1054 }
1055 }
1056 }
1057 }
1058
1059
1060 if t.maxLines > 0 && len(t.index) > t.maxLines {
1061 removedLines := len(t.index) - t.maxLines
1062
1063
1064 t.index = t.index[removedLines:]
1065 if t.fromHighlight >= 0 {
1066 t.fromHighlight -= removedLines
1067 if t.fromHighlight < 0 {
1068 t.fromHighlight = 0
1069 }
1070 }
1071 if t.toHighlight >= 0 {
1072 t.toHighlight -= removedLines
1073 if t.toHighlight < 0 {
1074 t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1
1075 }
1076 }
1077 bufferShift := t.index[0].Line
1078 for _, line := range t.index {
1079 line.Line -= bufferShift
1080 }
1081
1082
1083 t.buffer = t.buffer[bufferShift:]
1084 var prefix string
1085 if t.index[0].ForegroundColor != "" || t.index[0].BackgroundColor != "" || t.index[0].Attributes != "" {
1086 prefix = fmt.Sprintf("[%s:%s:%s]", t.index[0].ForegroundColor, t.index[0].BackgroundColor, t.index[0].Attributes)
1087 }
1088 if t.index[0].Region != "" {
1089 prefix += fmt.Sprintf(`["%s"]`, t.index[0].Region)
1090 }
1091 posShift := t.index[0].Pos
1092 t.buffer[0] = prefix + t.buffer[0][posShift:]
1093 t.lineOffset -= removedLines
1094 if t.lineOffset < 0 {
1095 t.lineOffset = 0
1096 }
1097
1098
1099 posShift -= len(prefix)
1100 for _, line := range t.index {
1101 if line.Line != 0 {
1102 break
1103 }
1104 line.Pos -= posShift
1105 line.NextPos -= posShift
1106 }
1107 }
1108
1109
1110 t.longestLine = 0
1111 for _, line := range t.index {
1112 if line.Width > t.longestLine {
1113 t.longestLine = line.Width
1114 }
1115 }
1116 }
1117
1118
1119 func (t *TextView) Draw(screen tcell.Screen) {
1120 t.Box.DrawForSubclass(screen, t)
1121 t.Lock()
1122 defer t.Unlock()
1123
1124
1125 x, y, width, height := t.GetInnerRect()
1126 t.pageSize = height
1127
1128
1129 _, labelBg, _ := t.labelStyle.Decompose()
1130 if t.labelWidth > 0 {
1131 labelWidth := t.labelWidth
1132 if labelWidth > width {
1133 labelWidth = width
1134 }
1135 printWithStyle(screen, t.label, x, y, 0, labelWidth, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault)
1136 x += labelWidth
1137 width -= labelWidth
1138 } else {
1139 _, drawnWidth, _, _ := printWithStyle(screen, t.label, x, y, 0, width, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault)
1140 x += drawnWidth
1141 width -= drawnWidth
1142 }
1143
1144
1145 if t.width > 0 && t.width < width {
1146 width = t.width
1147 }
1148 if t.height > 0 && t.height < height {
1149 height = t.height
1150 }
1151 if width <= 0 {
1152 return
1153 }
1154
1155
1156 _, bg, _ := t.textStyle.Decompose()
1157 if bg != t.backgroundColor {
1158 for row := 0; row < height; row++ {
1159 for column := 0; column < width; column++ {
1160 screen.SetContent(x+column, y+row, ' ', nil, t.textStyle)
1161 }
1162 }
1163 }
1164
1165
1166 if width != t.lastWidth && t.wrap {
1167 t.index = nil
1168 }
1169 t.lastWidth = width
1170
1171
1172 t.reindexBuffer(width)
1173 if t.regions {
1174 t.regionInfos = nil
1175 }
1176
1177
1178 if t.index == nil {
1179 return
1180 }
1181
1182
1183 if t.regions && t.scrollToHighlights && t.fromHighlight >= 0 {
1184
1185 if t.toHighlight-t.fromHighlight+1 < height {
1186
1187 t.lineOffset = (t.fromHighlight + t.toHighlight - height) / 2
1188 } else {
1189
1190 t.lineOffset = t.fromHighlight
1191 }
1192
1193
1194 if t.posHighlight-t.columnOffset > 3*width/4 {
1195 t.columnOffset = t.posHighlight - width/2
1196 }
1197
1198
1199 if t.posHighlight-t.columnOffset < 0 {
1200 t.columnOffset = t.posHighlight - width/4
1201 }
1202 }
1203 t.scrollToHighlights = false
1204
1205
1206 if t.lineOffset+height > len(t.index) {
1207 t.trackEnd = true
1208 }
1209 if t.trackEnd {
1210 t.lineOffset = len(t.index) - height
1211 }
1212 if t.lineOffset < 0 {
1213 t.lineOffset = 0
1214 }
1215
1216
1217 if t.align == AlignLeft {
1218 if t.columnOffset+width > t.longestLine {
1219 t.columnOffset = t.longestLine - width
1220 }
1221 if t.columnOffset < 0 {
1222 t.columnOffset = 0
1223 }
1224 } else if t.align == AlignRight {
1225 if t.columnOffset-width < -t.longestLine {
1226 t.columnOffset = width - t.longestLine
1227 }
1228 if t.columnOffset > 0 {
1229 t.columnOffset = 0
1230 }
1231 } else {
1232 half := (t.longestLine - width) / 2
1233 if half > 0 {
1234 if t.columnOffset > half {
1235 t.columnOffset = half
1236 }
1237 if t.columnOffset < -half {
1238 t.columnOffset = -half
1239 }
1240 } else {
1241 t.columnOffset = 0
1242 }
1243 }
1244
1245
1246 for line := t.lineOffset; line < len(t.index); line++ {
1247
1248 if line-t.lineOffset >= height {
1249 break
1250 }
1251
1252
1253 index := t.index[line]
1254 text := t.buffer[index.Line][index.Pos:index.NextPos]
1255 foregroundColor := index.ForegroundColor
1256 backgroundColor := index.BackgroundColor
1257 attributes := index.Attributes
1258 regionID := index.Region
1259 if t.regions {
1260 if len(t.regionInfos) > 0 && t.regionInfos[len(t.regionInfos)-1].ID != regionID {
1261
1262 t.regionInfos[len(t.regionInfos)-1].ToX = x
1263 t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset
1264 }
1265 if regionID != "" && (len(t.regionInfos) == 0 || t.regionInfos[len(t.regionInfos)-1].ID != regionID) {
1266
1267 t.regionInfos = append(t.regionInfos, &textViewRegion{
1268 ID: regionID,
1269 FromX: x,
1270 FromY: y + line - t.lineOffset,
1271 ToX: -1,
1272 ToY: -1,
1273 })
1274 }
1275 }
1276
1277
1278 colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions)
1279
1280
1281 var skip, posX int
1282 if t.align == AlignLeft {
1283 posX = -t.columnOffset
1284 } else if t.align == AlignRight {
1285 posX = width - index.Width - t.columnOffset
1286 } else {
1287 posX = (width-index.Width)/2 - t.columnOffset
1288 }
1289 if posX < 0 {
1290 skip = -posX
1291 posX = 0
1292 }
1293
1294
1295 if y+line-t.lineOffset >= 0 {
1296 var colorPos, regionPos, escapePos, tagOffset, skipped int
1297 iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
1298
1299 for {
1300 if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
1301
1302 foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
1303 tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
1304 colorPos++
1305 } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
1306
1307 if regionID != "" && len(t.regionInfos) > 0 && t.regionInfos[len(t.regionInfos)-1].ID == regionID {
1308
1309 t.regionInfos[len(t.regionInfos)-1].ToX = x + posX
1310 t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset
1311 }
1312 regionID = regions[regionPos][1]
1313 if regionID != "" {
1314
1315 t.regionInfos = append(t.regionInfos, &textViewRegion{
1316 ID: regionID,
1317 FromX: x + posX,
1318 FromY: y + line - t.lineOffset,
1319 ToX: -1,
1320 ToY: -1,
1321 })
1322 }
1323 tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
1324 regionPos++
1325 } else {
1326 break
1327 }
1328 }
1329
1330
1331 if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
1332 tagOffset++
1333 escapePos++
1334 }
1335
1336
1337 style := overlayStyle(t.textStyle, foregroundColor, backgroundColor, attributes)
1338
1339
1340 var highlighted bool
1341 if regionID != "" {
1342 if _, ok := t.highlights[regionID]; ok {
1343 highlighted = true
1344 }
1345 }
1346 if highlighted {
1347 fg, bg, _ := style.Decompose()
1348 if bg == t.backgroundColor {
1349 r, g, b := fg.RGB()
1350 c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
1351 _, _, li := c.Hcl()
1352 if li < .5 {
1353 bg = tcell.ColorWhite
1354 } else {
1355 bg = tcell.ColorBlack
1356 }
1357 }
1358 style = style.Background(fg).Foreground(bg)
1359 }
1360
1361
1362 if !t.wrap && skipped < skip {
1363 skipped += screenWidth
1364 return false
1365 }
1366
1367
1368 if posX+screenWidth > width {
1369 return true
1370 }
1371
1372
1373 for offset := screenWidth - 1; offset >= 0; offset-- {
1374 if offset == 0 {
1375 screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
1376 } else {
1377 screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
1378 }
1379 }
1380
1381
1382 posX += screenWidth
1383 return false
1384 })
1385 }
1386 }
1387
1388
1389
1390 if !t.scrollable && t.lineOffset > 0 {
1391 if t.lineOffset >= len(t.index) {
1392 t.buffer = nil
1393 } else {
1394 t.buffer = t.buffer[t.index[t.lineOffset].Line:]
1395 }
1396 t.index = nil
1397 t.lineOffset = 0
1398 }
1399 }
1400
1401
1402 func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
1403 return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
1404 key := event.Key()
1405
1406 if key == tcell.KeyEscape || key == tcell.KeyEnter || key == tcell.KeyTab || key == tcell.KeyBacktab {
1407 if t.done != nil {
1408 t.done(key)
1409 }
1410 if t.finished != nil {
1411 t.finished(key)
1412 }
1413 return
1414 }
1415
1416 if !t.scrollable {
1417 return
1418 }
1419
1420 switch key {
1421 case tcell.KeyRune:
1422 switch event.Rune() {
1423 case 'g':
1424 t.trackEnd = false
1425 t.lineOffset = 0
1426 t.columnOffset = 0
1427 case 'G':
1428 t.trackEnd = true
1429 t.columnOffset = 0
1430 case 'j':
1431 t.lineOffset++
1432 case 'k':
1433 t.trackEnd = false
1434 t.lineOffset--
1435 case 'h':
1436 t.columnOffset--
1437 case 'l':
1438 t.columnOffset++
1439 }
1440 case tcell.KeyHome:
1441 t.trackEnd = false
1442 t.lineOffset = 0
1443 t.columnOffset = 0
1444 case tcell.KeyEnd:
1445 t.trackEnd = true
1446 t.columnOffset = 0
1447 case tcell.KeyUp:
1448 t.trackEnd = false
1449 t.lineOffset--
1450 case tcell.KeyDown:
1451 t.lineOffset++
1452 case tcell.KeyLeft:
1453 t.columnOffset--
1454 case tcell.KeyRight:
1455 t.columnOffset++
1456 case tcell.KeyPgDn, tcell.KeyCtrlF:
1457 t.lineOffset += t.pageSize
1458 case tcell.KeyPgUp, tcell.KeyCtrlB:
1459 t.trackEnd = false
1460 t.lineOffset -= t.pageSize
1461 }
1462 })
1463 }
1464
1465
1466 func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
1467 return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
1468 x, y := event.Position()
1469 if !t.InRect(x, y) {
1470 return false, nil
1471 }
1472
1473 switch action {
1474 case MouseLeftDown:
1475 setFocus(t)
1476 consumed = true
1477 case MouseLeftClick:
1478 if t.regions {
1479
1480 for _, region := range t.regionInfos {
1481 if y == region.FromY && x < region.FromX ||
1482 y == region.ToY && x >= region.ToX ||
1483 region.FromY >= 0 && y < region.FromY ||
1484 region.ToY >= 0 && y > region.ToY {
1485 continue
1486 }
1487 t.Highlight(region.ID)
1488 break
1489 }
1490 }
1491 consumed = true
1492 case MouseScrollUp:
1493 t.trackEnd = false
1494 t.lineOffset--
1495 consumed = true
1496 case MouseScrollDown:
1497 t.lineOffset++
1498 consumed = true
1499 }
1500
1501 return
1502 })
1503 }
1504
View as plain text