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