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