diff --git a/CHANGELOG b/CHANGELOG index db739e0..6741ec5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ v1.5.1 (WIP) - Store TextView buffer as [][]byte instead of []string - Add TextView.SetBytes and TextView.GetBytes +- Allow modification of scroll bar render text v1.5.0 (2020-10-03) - Add scroll bar to TextView diff --git a/application.go b/application.go index 8a11cd7..be024c0 100644 --- a/application.go +++ b/application.go @@ -9,9 +9,6 @@ import ( ) const ( - // StandardDoubleClick is the standard double click interval. - StandardDoubleClick = 500 * time.Millisecond - // The size of the event/update/redraw channels. queueSize = 100 @@ -19,30 +16,6 @@ const ( resizeEventThrottle = 200 * time.Millisecond ) -// MouseAction indicates one of the actions the mouse is logically doing. -type MouseAction int16 - -// Available mouse actions. -const ( - MouseMove MouseAction = iota - MouseLeftDown - MouseLeftUp - MouseLeftClick - MouseLeftDoubleClick - MouseMiddleDown - MouseMiddleUp - MouseMiddleClick - MouseMiddleDoubleClick - MouseRightDown - MouseRightUp - MouseRightClick - MouseRightDoubleClick - MouseScrollUp - MouseScrollDown - MouseScrollLeft - MouseScrollRight -) - // Application represents the top node of an application. // // It is not strictly required to use this class as none of the other classes diff --git a/box.go b/box.go index 5bb2016..fa051bc 100644 --- a/box.go +++ b/box.go @@ -39,7 +39,7 @@ type Box struct { borderAttributes tcell.AttrMask // The title. Only visible if there is a border, too. - title string + title []byte // The color of the title. titleColor tcell.Color @@ -376,7 +376,7 @@ func (b *Box) SetTitle(title string) *Box { b.l.Lock() defer b.l.Unlock() - b.title = title + b.title = []byte(title) return b } @@ -385,7 +385,7 @@ func (b *Box) GetTitle() string { b.l.RLock() defer b.l.RUnlock() - return b.title + return string(b.title) } // SetTitleColor sets the box's title color. @@ -474,12 +474,12 @@ func (b *Box) Draw(screen tcell.Screen) { screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, border) // Draw title. - if b.title != "" && b.width >= 4 { + if len(b.title) > 0 && b.width >= 4 { printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor) if len(b.title)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(b.x+b.width-2, b.y) fg, _, _ := style.Decompose() - Print(screen, string(SemigraphicsHorizontalEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg) + Print(screen, []byte(string(SemigraphicsHorizontalEllipsis)), b.x+b.width-2, b.y, 1, AlignLeft, fg) } } } diff --git a/button.go b/button.go index 63b2079..24dbda9 100644 --- a/button.go +++ b/button.go @@ -11,7 +11,7 @@ type Button struct { *Box // The text to be displayed before the input area. - label string + label []byte // The label color. labelColor tcell.Color @@ -38,7 +38,7 @@ func NewButton(label string) *Button { box.SetRect(0, 0, TaggedStringWidth(label)+4, 1) return &Button{ Box: box, - label: label, + label: []byte(label), labelColor: Styles.PrimaryTextColor, labelColorFocused: Styles.InverseTextColor, backgroundColorFocused: Styles.PrimaryTextColor, @@ -50,7 +50,7 @@ func (b *Button) SetLabel(label string) *Button { b.Lock() defer b.Unlock() - b.label = label + b.label = []byte(label) return b } @@ -59,7 +59,7 @@ func (b *Button) GetLabel() string { b.RLock() defer b.RUnlock() - return b.label + return string(b.label) } // SetLabelColor sets the color of the button text. diff --git a/checkbox.go b/checkbox.go index 72788c3..ac2b89e 100644 --- a/checkbox.go +++ b/checkbox.go @@ -15,10 +15,10 @@ type CheckBox struct { checked bool // The text to be displayed before the checkbox. - label string + label []byte // The text to be displayed after the checkbox. - message string + message []byte // The screen width of the label area. A value of 0 means use the width of // the label text. @@ -106,7 +106,7 @@ func (c *CheckBox) SetLabel(label string) *CheckBox { c.Lock() defer c.Unlock() - c.label = label + c.label = []byte(label) return c } @@ -115,7 +115,7 @@ func (c *CheckBox) GetLabel() string { c.RLock() defer c.RUnlock() - return c.label + return string(c.label) } // SetMessage sets the text to be displayed after the checkbox @@ -123,7 +123,7 @@ func (c *CheckBox) SetMessage(message string) *CheckBox { c.Lock() defer c.Unlock() - c.message = message + c.message = []byte(message) return c } @@ -132,7 +132,7 @@ func (c *CheckBox) GetMessage() string { c.RLock() defer c.RUnlock() - return c.message + return string(c.message) } // SetLabelWidth sets the screen width of the label. A value of 0 will cause the @@ -209,7 +209,7 @@ func (c *CheckBox) GetFieldWidth() int { c.RLock() defer c.RUnlock() - if c.message == "" { + if len(c.message) == 0 { return 1 } @@ -321,7 +321,7 @@ func (c *CheckBox) Draw(screen tcell.Screen) { screen.SetContent(x+1, y, checkedRune, nil, fieldStyle) screen.SetContent(x+2, y, ' ', nil, fieldStyle) - if c.message != "" { + if len(c.message) > 0 { Print(screen, c.message, x+4, y, len(c.message), AlignLeft, labelColor) } } diff --git a/demos/primitive/main.go b/demos/primitive/main.go index b7e1808..56034da 100644 --- a/demos/primitive/main.go +++ b/demos/primitive/main.go @@ -37,7 +37,7 @@ func (r *RadioButtons) Draw(screen tcell.Screen) { radioButton = "\u25c9" // Checked. } line := fmt.Sprintf(`%s[white] %s`, radioButton, option) - cview.Print(screen, line, x, y+index, width, cview.AlignLeft, tcell.ColorYellow.TrueColor()) + cview.Print(screen, []byte(line), x, y+index, width, cview.AlignLeft, tcell.ColorYellow.TrueColor()) } } diff --git a/dropdown.go b/dropdown.go index 38c2b36..4ff2e14 100644 --- a/dropdown.go +++ b/dropdown.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/gdamore/tcell/v2" + "github.com/mattn/go-runewidth" ) // DropDownOption is one option that can be selected in a drop-down primitive. @@ -569,10 +570,10 @@ func (d *DropDown) Draw(screen tcell.Screen) { if labelWidth > rightLimit-x { labelWidth = rightLimit - x } - Print(screen, d.label, x, y, labelWidth, AlignLeft, labelColor) + Print(screen, []byte(d.label), x, y, labelWidth, AlignLeft, labelColor) x += labelWidth } else { - _, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, labelColor) + _, drawnWidth := Print(screen, []byte(d.label), x, y, rightLimit-x, AlignLeft, labelColor) x += drawnWidth } @@ -614,12 +615,12 @@ func (d *DropDown) Draw(screen tcell.Screen) { if d.open && len(d.prefix) > 0 { // Show the prefix. currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix) - prefixWidth := stringWidth(d.prefix) + prefixWidth := runewidth.StringWidth(d.prefix) listItemText := d.options[d.list.GetCurrentItemIndex()].text - Print(screen, d.currentOptionPrefix, x, y, fieldWidth, AlignLeft, fieldTextColor) - Print(screen, d.prefix, x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor) + Print(screen, []byte(d.currentOptionPrefix), x, y, fieldWidth, AlignLeft, fieldTextColor) + Print(screen, []byte(d.prefix), x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor) if len(d.prefix) < len(listItemText) { - Print(screen, listItemText[len(d.prefix):]+d.currentOptionSuffix, x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor) + Print(screen, []byte(listItemText[len(d.prefix):]+d.currentOptionSuffix), x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor) } } else { color := fieldTextColor @@ -633,7 +634,7 @@ func (d *DropDown) Draw(screen tcell.Screen) { } // Just show the current selection. - Print(screen, text, x, y, fieldWidth, AlignLeft, color) + Print(screen, []byte(text), x, y, fieldWidth, AlignLeft, color) } // Draw drop-down symbol diff --git a/frame.go b/frame.go index 5b9b49b..eeec6db 100644 --- a/frame.go +++ b/frame.go @@ -137,7 +137,7 @@ func (f *Frame) Draw(screen tcell.Screen) { } // Draw text. - Print(screen, text.Text, x, y, width, text.Align, text.Color) + Print(screen, []byte(text.Text), x, y, width, text.Align, text.Color) } // Set the size of the contained primitive. diff --git a/inputfield.go b/inputfield.go index d78dbea..edeece7 100644 --- a/inputfield.go +++ b/inputfield.go @@ -8,6 +8,7 @@ import ( "unicode/utf8" "github.com/gdamore/tcell/v2" + "github.com/mattn/go-runewidth" ) // InputField is a one-line box (three lines if there is a title) where the @@ -613,10 +614,10 @@ func (i *InputField) Draw(screen tcell.Screen) { if labelWidth > rightLimit-x { labelWidth = rightLimit - x } - Print(screen, i.label, x, y, labelWidth, AlignLeft, labelColor) + Print(screen, []byte(i.label), x, y, labelWidth, AlignLeft, labelColor) x += labelWidth } else { - _, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, labelColor) + _, drawnWidth := Print(screen, []byte(i.label), x, y, rightLimit-x, AlignLeft, labelColor) x += drawnWidth } @@ -643,7 +644,7 @@ func (i *InputField) Draw(screen tcell.Screen) { if i.GetFocusable().HasFocus() && i.placeholderTextColorFocused != ColorUnset { placeholderTextColor = i.placeholderTextColorFocused } - Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, placeholderTextColor) + Print(screen, []byte(Escape(i.placeholder)), x, y, fieldWidth, AlignLeft, placeholderTextColor) i.offset = 0 } else { // Draw entered text. @@ -651,10 +652,10 @@ func (i *InputField) Draw(screen tcell.Screen) { text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text)) } drawnText := "" - if fieldWidth >= stringWidth(text) { + if fieldWidth >= runewidth.StringWidth(text) { // We have enough space for the full text. drawnText = Escape(text) - Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor) + Print(screen, []byte(drawnText), x, y, fieldWidth, AlignLeft, fieldTextColor) i.offset = 0 iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { if textPos >= i.cursorPos { @@ -674,7 +675,7 @@ func (i *InputField) Draw(screen tcell.Screen) { var shiftLeft int if i.offset > i.cursorPos { i.offset = i.cursorPos - } else if subWidth := stringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 { + } else if subWidth := runewidth.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 { shiftLeft = subWidth - fieldWidth + 1 } currentOffset := i.offset @@ -693,17 +694,17 @@ func (i *InputField) Draw(screen tcell.Screen) { return false }) drawnText = Escape(text[i.offset:]) - Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor) + Print(screen, []byte(drawnText), x, y, fieldWidth, AlignLeft, fieldTextColor) } // Draw suggestion if i.maskCharacter == 0 && i.autocompleteListSuggestion != "" { - Print(screen, i.autocompleteListSuggestion, x+stringWidth(drawnText), y, fieldWidth-stringWidth(drawnText), AlignLeft, i.autocompleteSuggestionTextColor) + Print(screen, []byte(i.autocompleteListSuggestion), x+runewidth.StringWidth(drawnText), y, fieldWidth-runewidth.StringWidth(drawnText), AlignLeft, i.autocompleteSuggestionTextColor) } } // Draw field note if i.fieldNote != "" { - Print(screen, i.fieldNote, x, y+1, fieldWidth, AlignLeft, i.fieldNoteTextColor) + Print(screen, []byte(i.fieldNote), x, y+1, fieldWidth, AlignLeft, i.fieldNoteTextColor) } // Draw autocomplete list. diff --git a/list.go b/list.go index 2e97656..eafb4e2 100644 --- a/list.go +++ b/list.go @@ -1,6 +1,7 @@ package cview import ( + "bytes" "fmt" "strings" "sync" @@ -871,9 +872,9 @@ func (l *List) Draw(screen tcell.Screen) { } if item.mainText == "" && item.secondaryText == "" && item.shortcut == 0 { // Divider - Print(screen, string(tcell.RuneLTee), (x-5)-l.paddingLeft, y, 1, AlignLeft, l.mainTextColor) - Print(screen, strings.Repeat(string(tcell.RuneHLine), width+4+l.paddingLeft+l.paddingRight), (x-4)-l.paddingLeft, y, width+4+l.paddingLeft+l.paddingRight, AlignLeft, l.mainTextColor) - Print(screen, string(tcell.RuneRTee), (x-5)+width+5+l.paddingRight, y, 1, AlignLeft, l.mainTextColor) + Print(screen, []byte(string(tcell.RuneLTee)), (x-5)-l.paddingLeft, y, 1, AlignLeft, l.mainTextColor) + Print(screen, bytes.Repeat([]byte(string(tcell.RuneHLine)), width+4+l.paddingLeft+l.paddingRight), (x-4)-l.paddingLeft, y, width+4+l.paddingLeft+l.paddingRight, AlignLeft, l.mainTextColor) + Print(screen, []byte(string(tcell.RuneRTee)), (x-5)+width+5+l.paddingRight, y, 1, AlignLeft, l.mainTextColor) RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.offset, l.hasFocus, l.scrollBarColor) y++ @@ -881,11 +882,11 @@ func (l *List) Draw(screen tcell.Screen) { } else if !item.enabled { // Disabled item // Shortcuts. if showShortcuts && item.shortcut != 0 { - Print(screen, fmt.Sprintf("(%s)", string(item.shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray.TrueColor()) + Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray.TrueColor()) } // Main text. - Print(screen, item.mainText, x, y, width, AlignLeft, tcell.ColorGray.TrueColor()) + Print(screen, []byte(item.mainText), x, y, width, AlignLeft, tcell.ColorGray.TrueColor()) RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.offset, l.hasFocus, l.scrollBarColor) y++ @@ -894,11 +895,11 @@ func (l *List) Draw(screen tcell.Screen) { // Shortcuts. if showShortcuts && item.shortcut != 0 { - Print(screen, fmt.Sprintf("(%s)", string(item.shortcut)), x-5, y, 4, AlignRight, l.shortcutColor) + Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, l.shortcutColor) } // Main text. - Print(screen, item.mainText, x, y, width, AlignLeft, l.mainTextColor) + Print(screen, []byte(item.mainText), x, y, width, AlignLeft, l.mainTextColor) // Background color of selected text. if index == l.currentItem && (!l.selectedFocusOnly || hasFocus) { @@ -930,7 +931,7 @@ func (l *List) Draw(screen tcell.Screen) { // Secondary text. if l.showSecondaryText { - Print(screen, item.secondaryText, x, y, width, AlignLeft, l.secondaryTextColor) + Print(screen, []byte(item.secondaryText), x, y, width, AlignLeft, l.secondaryTextColor) RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.offset, l.hasFocus, l.scrollBarColor) diff --git a/modal.go b/modal.go index d5d928d..fb51caa 100644 --- a/modal.go +++ b/modal.go @@ -201,7 +201,7 @@ func (m *Modal) Draw(screen tcell.Screen) { // Calculate the width of this Modal. buttonsWidth := 0 for _, button := range m.form.buttons { - buttonsWidth += TaggedStringWidth(button.label) + 4 + 2 + buttonsWidth += TaggedTextWidth(button.label) + 4 + 2 } buttonsWidth -= 2 screenWidth, screenHeight := screen.Size() @@ -213,9 +213,9 @@ func (m *Modal) Draw(screen tcell.Screen) { // Reset the text and find out how wide it is. m.frame.Clear() - lines := WordWrap(m.text, width) + lines := WordWrap([]byte(m.text), width) for _, line := range lines { - m.frame.AddText(line, true, AlignCenter, m.textColor) + m.frame.AddText(string(line), true, AlignCenter, m.textColor) } // Set the Modal's position and size. diff --git a/mouse.go b/mouse.go new file mode 100644 index 0000000..998896b --- /dev/null +++ b/mouse.go @@ -0,0 +1,30 @@ +package cview + +import "time" + +// MouseAction indicates one of the actions the mouse is logically doing. +type MouseAction int16 + +// Available mouse actions. +const ( + MouseMove MouseAction = iota + MouseLeftDown + MouseLeftUp + MouseLeftClick + MouseLeftDoubleClick + MouseMiddleDown + MouseMiddleUp + MouseMiddleClick + MouseMiddleDoubleClick + MouseRightDown + MouseRightUp + MouseRightClick + MouseRightDoubleClick + MouseScrollUp + MouseScrollDown + MouseScrollLeft + MouseScrollRight +) + +// StandardDoubleClick is a commonly used double click interval. +const StandardDoubleClick = 500 * time.Millisecond diff --git a/table.go b/table.go index f2654c8..d1042e3 100644 --- a/table.go +++ b/table.go @@ -995,7 +995,7 @@ ColumnLoop: } for _, row := range evaluationRows { if cell := getCell(row, column); cell != nil { - _, _, _, _, _, _, cellWidth := decomposeString(cell.Text, true, false) + _, _, _, _, _, _, cellWidth := decomposeText([]byte(cell.Text), true, false) if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth { cellWidth = cell.MaxWidth } @@ -1089,10 +1089,10 @@ ColumnLoop: finalWidth = width - columnX - 1 } cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth - _, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, SetAttributes(tcell.StyleDefault.Foreground(cell.Color), cell.Attributes)) + _, printed := printWithStyle(screen, []byte(cell.Text), x+columnX+1, y+rowY, finalWidth, cell.Align, SetAttributes(tcell.StyleDefault.Foreground(cell.Color), cell.Attributes)) if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY) - printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style) + printWithStyle(screen, []byte(string(SemigraphicsHorizontalEllipsis)), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style) } } diff --git a/textview.go b/textview.go index f2b43b1..86500f4 100644 --- a/textview.go +++ b/textview.go @@ -32,13 +32,13 @@ type textViewIndex struct { ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset). BackgroundColor string // The starting background color ("" = don't change, "-" = reset). Attributes string // The starting attributes ("" = don't change, "-" = reset). - Region string // The starting region ID. + Region []byte // The starting region ID. } // textViewRegion contains information about a region. type textViewRegion struct { // The region ID. - ID string + ID []byte // The starting and end screen position of the region as determined the last // time Draw() was called. A negative value indicates out-of-rect positions. @@ -831,7 +831,7 @@ func (t *TextView) reindexBuffer(width int) { } // Initial states. - regionID := "" + var regionID []byte var ( highlighted bool foregroundColor, backgroundColor, attributes string @@ -839,12 +839,11 @@ func (t *TextView) reindexBuffer(width int) { // Go through each line in the buffer. for bufferIndex, buf := range t.buffer { - str := string(buf) - colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeString(str, t.dynamicColors, t.regions) + colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeText(buf, t.dynamicColors, t.regions) // Split the line if required. var splitLines []string - str = strippedStr + str := string(strippedStr) // TODO if t.wrap && len(str) > 0 { for len(str) > 0 { extract := runewidth.Truncate(str, width, "") @@ -938,14 +937,14 @@ func (t *TextView) reindexBuffer(width int) { case 1: // Process region tags. regionID = regions[regionPos][1] - _, highlighted = t.highlights[regionID] + _, highlighted = t.highlights[string(regionID)] // Update highlight range. if highlighted { line := len(t.index) if t.fromHighlight < 0 { t.fromHighlight, t.toHighlight = line, line - t.posHighlight = stringWidth(splitLine[:strippedTagStart]) + t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart]) } else if line > t.toHighlight { t.toHighlight = line } @@ -963,7 +962,7 @@ func (t *TextView) reindexBuffer(width int) { // Append this line. line.NextPos = originalPos - line.Width = stringWidth(splitLine) + line.Width = runewidth.StringWidth(splitLine) t.index = append(t.index, line) } @@ -975,7 +974,7 @@ func (t *TextView) reindexBuffer(width int) { if len(trimmed) != len(str) { oldNextPos := line.NextPos line.NextPos -= len(str) - len(trimmed) - line.Width -= stringWidth(string(t.buffer[line.Line][line.NextPos:oldNextPos])) + line.Width -= runewidth.StringWidth(string(t.buffer[line.Line][line.NextPos:oldNextPos])) } } } @@ -1106,7 +1105,7 @@ func (t *TextView) Draw(screen tcell.Screen) { backgroundColor := index.BackgroundColor attributes := index.Attributes regionID := index.Region - if t.regions && regionID != "" && (len(t.regionInfos) == 0 || t.regionInfos[len(t.regionInfos)-1].ID != regionID) { + if t.regions && len(regionID) > 0 && (len(t.regionInfos) == 0 || !bytes.Equal(t.regionInfos[len(t.regionInfos)-1].ID, regionID)) { t.regionInfos = append(t.regionInfos, &textViewRegion{ ID: regionID, FromX: x, @@ -1117,7 +1116,7 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Process tags. - colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(string(text), t.dynamicColors, t.regions) + colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeText(text, t.dynamicColors, t.regions) // Calculate the position of the line. var skip, posX int @@ -1136,7 +1135,7 @@ func (t *TextView) Draw(screen tcell.Screen) { // Print the line. if y+line-t.lineOffset >= 0 { var colorPos, regionPos, escapePos, tagOffset, skipped int - iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Process tags. for { if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { @@ -1146,13 +1145,13 @@ func (t *TextView) Draw(screen tcell.Screen) { colorPos++ } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] { // Get the region. - if regionID != "" && len(t.regionInfos) > 0 && t.regionInfos[len(t.regionInfos)-1].ID == regionID { + if len(regionID) > 0 && len(t.regionInfos) > 0 && !bytes.Equal(t.regionInfos[len(t.regionInfos)-1].ID, regionID) { // End last region. t.regionInfos[len(t.regionInfos)-1].ToX = x + posX t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset } regionID = regions[regionPos][1] - if regionID != "" { + if len(regionID) > 0 { // Start new region. t.regionInfos = append(t.regionInfos, &textViewRegion{ ID: regionID, @@ -1182,8 +1181,8 @@ func (t *TextView) Draw(screen tcell.Screen) { // Do we highlight this character? var highlighted bool - if regionID != "" { - if _, ok := t.highlights[regionID]; ok { + if len(regionID) > 0 { + if _, ok := t.highlights[string(regionID)]; ok { highlighted = true } } @@ -1313,7 +1312,7 @@ func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMou region.ToY >= 0 && y > region.ToY { continue } - t.Highlight(region.ID) + t.Highlight(string(region.ID)) break } } diff --git a/textview_test.go b/textview_test.go index f423856..8040e90 100644 --- a/textview_test.go +++ b/textview_test.go @@ -140,6 +140,9 @@ func BenchmarkTextViewIndex(b *testing.B) { b.Errorf("failed to write: expected to write %d bytes, wrote %d", randomDataSize, n) } + tv.index = nil + tv.reindexBuffer(80) + b.ReportAllocs() b.ResetTimer() @@ -210,13 +213,16 @@ func BenchmarkTextViewGetText(b *testing.B) { b.Errorf("failed to write: expected to write %d bytes, wrote %d", randomDataSize, n) } + v = tv.GetBytes(true) + b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { v = tv.GetBytes(true) - _ = v } + + _ = v }) } } @@ -286,6 +292,8 @@ func BenchmarkTextViewDraw(b *testing.B) { b.Errorf("failed to write: expected to write %d bytes, wrote %d", randomDataSize, n) } + tv.Draw(app.screen) + b.ReportAllocs() b.ResetTimer() diff --git a/treeview.go b/treeview.go index 8e4c935..382968b 100644 --- a/treeview.go +++ b/treeview.go @@ -331,7 +331,7 @@ type TreeView struct { topLevel int // Strings drawn before the nodes, based on their level. - prefixes []string + prefixes [][]byte // Vertical scroll offset. offsetY int @@ -452,7 +452,10 @@ func (t *TreeView) SetPrefixes(prefixes []string) *TreeView { t.Lock() defer t.Unlock() - t.prefixes = prefixes + t.prefixes = make([][]byte, len(prefixes)) + for i := range prefixes { + t.prefixes[i] = []byte(prefixes[i]) + } return t } @@ -880,7 +883,7 @@ func (t *TreeView) Draw(screen tcell.Screen) { } style = tcell.StyleDefault.Background(backgroundColor).Foreground(foregroundColor) } - printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style) + printWithStyle(screen, []byte(node.text), x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style) } } diff --git a/util.go b/util.go index 256d4e9..c60c49e 100644 --- a/util.go +++ b/util.go @@ -101,9 +101,9 @@ func init() { // the substrings (tagSubstrings) extracted by the regular expression for color // tags. The new colors and attributes are returned where empty strings mean // "don't modify" and a dash ("-") means "reset to default". -func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (newFgColor, newBgColor, newAttributes string) { - if tagSubstrings[colorForegroundPos] != "" { - color := tagSubstrings[colorForegroundPos] +func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings [][]byte) (newFgColor, newBgColor, newAttributes string) { + if len(tagSubstrings[colorForegroundPos]) > 0 { + color := string(tagSubstrings[colorForegroundPos]) if color == "-" { fgColor = "-" } else if color != "" { @@ -111,8 +111,8 @@ func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) ( } } - if tagSubstrings[colorBackgroundPos-1] != "" { - color := tagSubstrings[colorBackgroundPos] + if len(tagSubstrings[colorBackgroundPos-1]) > 0 { + color := string(tagSubstrings[colorBackgroundPos]) if color == "-" { bgColor = "-" } else if color != "" { @@ -120,8 +120,8 @@ func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) ( } } - if tagSubstrings[colorFlagPos-1] != "" { - flags := tagSubstrings[colorFlagPos] + if len(tagSubstrings[colorFlagPos-1]) > 0 { + flags := string(tagSubstrings[colorFlagPos]) if flags == "-" { attributes = "-" } else if flags != "" { @@ -200,7 +200,7 @@ func SetAttributes(style tcell.Style, attrs tcell.AttrMask) tcell.Style { Underline(attrs&tcell.AttrUnderline != 0) } -// decomposeString returns information about a string which may contain color +// decomposeText returns information about a string which may contain color // tags or region tags, depending on which ones are requested to be found. It // returns the indices of the color tags (as returned by // re.FindAllStringIndex()), the color tags themselves (as returned by @@ -208,22 +208,22 @@ func SetAttributes(style tcell.Style, attrs tcell.AttrMask) tcell.Style { // themselves, the indices of an escaped tags (only if at least color tags or // region tags are requested), the string stripped by any tags and escaped, and // the screen width of the stripped string. -func decomposeString(text string, findColors, findRegions bool) (colorIndices [][]int, colors [][]string, regionIndices [][]int, regions [][]string, escapeIndices [][]int, stripped string, width int) { +func decomposeText(text []byte, findColors, findRegions bool) (colorIndices [][]int, colors [][][]byte, regionIndices [][]int, regions [][][]byte, escapeIndices [][]int, stripped []byte, width int) { // Shortcut for the trivial case. if !findColors && !findRegions { - return nil, nil, nil, nil, nil, text, stringWidth(text) + return nil, nil, nil, nil, nil, text, runewidth.StringWidth(string(text)) } // Get positions of any tags. if findColors { - colorIndices = colorPattern.FindAllStringIndex(text, -1) - colors = colorPattern.FindAllStringSubmatch(text, -1) + colorIndices = colorPattern.FindAllIndex(text, -1) + colors = colorPattern.FindAllSubmatch(text, -1) } if findRegions { - regionIndices = regionPattern.FindAllStringIndex(text, -1) - regions = regionPattern.FindAllStringSubmatch(text, -1) + regionIndices = regionPattern.FindAllIndex(text, -1) + regions = regionPattern.FindAllSubmatch(text, -1) } - escapeIndices = escapePattern.FindAllStringIndex(text, -1) + escapeIndices = escapePattern.FindAllIndex(text, -1) // Because the color pattern detects empty tags, we need to filter them out. for i := len(colorIndices) - 1; i >= 0; i-- { @@ -259,10 +259,10 @@ func decomposeString(text string, findColors, findRegions bool) (colorIndices [] buf = append(buf, text[from:]...) // Escape string. - stripped = string(escapePattern.ReplaceAll(buf, []byte("[$1$2]"))) + stripped = escapePattern.ReplaceAll(buf, []byte("[$1$2]")) // Get the width of the stripped string. - width = stringWidth(stripped) + width = runewidth.StringWidth(string(stripped)) return } @@ -276,19 +276,19 @@ func decomposeString(text string, findColors, findRegions bool) (colorIndices [] // // Returns the number of actual bytes of the text printed (including color tags) // and the actual width used for the printed runes. -func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) { +func Print(screen tcell.Screen, text []byte, x, y, maxWidth, align int, color tcell.Color) (int, int) { return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color)) } // printWithStyle works like Print() but it takes a style instead of just a // foreground color. -func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) { +func printWithStyle(screen tcell.Screen, text []byte, x, y, maxWidth, align int, style tcell.Style) (int, int) { if maxWidth <= 0 || len(text) == 0 { return 0, 0 } // Decompose the text. - colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false) + colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeText(text, true, false) // We want to reduce all alignments to AlignLeft. if align == AlignRight { @@ -302,7 +302,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, foregroundColor, backgroundColor, attributes string ) _, originalBackground, _ := style.Decompose() - iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Update color/escape tag offset and style. if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) @@ -319,7 +319,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] { // Unescape open escape sequences. escapeCharPos := escapeIndices[escapePos-1][1] - 2 - text = text[:escapeCharPos] + text[escapeCharPos+1:] + text = append(text[:escapeCharPos], text[escapeCharPos+1:]...) } // Print and return. bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style) @@ -343,14 +343,14 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth { if choppedLeft < choppedRight { // Iterate on the left by one character. - iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + iterateString(string(strippedText[leftIndex:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { choppedLeft += screenWidth leftIndex += textWidth return true }) } else { // Iterate on the right by one character. - iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + iterateStringReverse(string(strippedText[leftIndex:rightIndex]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { choppedRight += screenWidth rightIndex -= textWidth return true @@ -371,7 +371,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] { // Unescape open escape sequences. escapeCharPos := escapeIndices[escapePos-1][1] - 2 - text = text[:escapeCharPos] + text[escapeCharPos+1:] + text = append(text[:escapeCharPos], text[escapeCharPos+1:]...) } break } @@ -399,7 +399,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, drawn, drawnWidth, colorPos, escapePos, tagOffset int foregroundColor, backgroundColor, attributes string ) - iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { + iterateString(string(strippedText), func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { // Only continue if there is still space. if drawnWidth+screenWidth > maxWidth { return true @@ -445,33 +445,21 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, } // PrintSimple prints white text to the screen at the given position. -func PrintSimple(screen tcell.Screen, text string, x, y int) { +func PrintSimple(screen tcell.Screen, text []byte, x, y int) { Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor) } +// TaggedTextWidth returns the width of the given string needed to print it on +// screen. The text may contain color tags which are not counted. +func TaggedTextWidth(text []byte) int { + _, _, _, _, _, _, width := decomposeText(text, true, false) + return width +} + // TaggedStringWidth returns the width of the given string needed to print it on // screen. The text may contain color tags which are not counted. func TaggedStringWidth(text string) int { - _, _, _, _, _, _, width := decomposeString(text, true, false) - return width -} - -// stringWidth returns the number of horizontal cells needed to print the given -// text. It splits the text into its grapheme clusters, calculates each -// cluster's width, and adds them up to a total. -func stringWidth(text string) (width int) { - g := uniseg.NewGraphemes(text) - for g.Next() { - var chWidth int - for _, r := range g.Runes() { - chWidth = runewidth.RuneWidth(r) - if chWidth > 0 { - break // Our best guess at this point is to use the width of the first non-zero-width rune. - } - } - width += chWidth - } - return + return TaggedTextWidth([]byte(text)) } // WordWrap splits a text such that each resulting line does not exceed the @@ -481,11 +469,11 @@ func stringWidth(text string) (width int) { // This function considers color tags to have no width. // // Text is always split at newline characters ('\n'). -func WordWrap(text string, width int) (lines []string) { - colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeString(text, true, false) +func WordWrap(text []byte, width int) (lines [][]byte) { + colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeText(text, true, false) // Find candidate breakpoints. - breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1) + breakpoints := boundaryPattern.FindAllSubmatchIndex(strippedText, -1) // Results in one entry for each candidate. Each entry is an array a of // indices into strippedText where a[6] < 0 for newline/punctuation matches // and a[4] < 0 for whitespace matches. @@ -497,17 +485,17 @@ func WordWrap(text string, width int) (lines []string) { lineWidth, overflow int forceBreak bool ) - unescape := func(substr string, startIndex int) string { + unescape := func(substr []byte, startIndex int) []byte { // A helper function to unescape escaped tags. for index := escapePos; index >= 0; index-- { if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 { pos := escapeIndices[index][1] - 2 - startIndex - return substr[:pos] + substr[pos+1:] + return append(substr[:pos], substr[pos+1:]...) } } return substr } - iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Handle tags. for { if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { @@ -603,7 +591,7 @@ func iterateString(text string, callback func(main rune, comb []rune, textPos, t for gr.Next() { r := gr.Runes() from, to := gr.Positions() - width := stringWidth(gr.Str()) + width := runewidth.StringWidth(gr.Str()) var comb []rune if len(r) > 1 { comb = r[1:] @@ -679,7 +667,15 @@ const ( ScrollBarAlways ) -// RenderScrollBar renders a scroll bar character at the specified position. +// Scroll bar render text (must be one cell wide) +var ( + ScrollBarArea = []byte("[-:-:-]░") + ScrollBarAreaFocused = []byte("[-:-:-]▒") + ScrollBarHandle = []byte("[-:-:-]▓") + ScrollBarHandleFocused = []byte("[::r] [-:-:-]") +) + +// RenderScrollBar renders a scroll bar at the specified position. func RenderScrollBar(screen tcell.Screen, visibility ScrollBarVisibility, x int, y int, height int, items int, cursor int, printed int, focused bool, color tcell.Color) { if visibility == ScrollBarNever || (visibility == ScrollBarAuto && items <= height) { return @@ -698,20 +694,20 @@ func RenderScrollBar(screen tcell.Screen, visibility ScrollBarVisibility, x int, // Calculate handle position. handlePosition := int(float64(height-1) * (float64(cursor) / float64(items-1))) - // Print character. - var scrollBar string + // Print scroll bar. + var text []byte if printed == handlePosition { if focused { - scrollBar = "[::r] [-:-:-]" + text = ScrollBarHandleFocused } else { - scrollBar = "[-:-:-]▓" + text = ScrollBarHandle } } else { if focused { - scrollBar = "[-:-:-]▒" + text = ScrollBarAreaFocused } else { - scrollBar = "[-:-:-]░" + text = ScrollBarArea } } - Print(screen, scrollBar, x, y, 1, AlignLeft, color) + Print(screen, text, x, y, 1, AlignLeft, color) }