mirror of https://github.com/rivo/tview.git
Implemented basic text area printing.
This commit is contained in:
parent
6537221da8
commit
ac1f564949
2
box.go
2
box.go
|
@ -125,7 +125,7 @@ func (b *Box) GetInnerRect() (int, int, int, int) {
|
||||||
// if this primitive is part of a layout (e.g. Flex, Grid) or if it was added
|
// if this primitive is part of a layout (e.g. Flex, Grid) or if it was added
|
||||||
// like this:
|
// like this:
|
||||||
//
|
//
|
||||||
// application.SetRoot(b, true)
|
// application.SetRoot(p, true)
|
||||||
func (b *Box) SetRect(x, y, width, height int) {
|
func (b *Box) SetRect(x, y, width, height int) {
|
||||||
b.x = x
|
b.x = x
|
||||||
b.y = y
|
b.y = y
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -10,8 +10,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
||||||
github.com/rivo/uniseg v0.3.1 h1:SDPP7SHNl1L7KrEFCSJslJ/DM9DT02Nq2C61XrfHMmk=
|
github.com/rivo/uniseg v0.3.1 h1:SDPP7SHNl1L7KrEFCSJslJ/DM9DT02Nq2C61XrfHMmk=
|
||||||
github.com/rivo/uniseg v0.3.1/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.3.1/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
|
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
|
||||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
||||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
|
|
@ -418,7 +418,7 @@ func (i *InputField) Draw(screen tcell.Screen) {
|
||||||
// We have enough space for the full text.
|
// We have enough space for the full text.
|
||||||
printWithStyle(screen, Escape(text), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true)
|
printWithStyle(screen, Escape(text), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true)
|
||||||
i.offset = 0
|
i.offset = 0
|
||||||
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
if textPos >= i.cursorPos {
|
if textPos >= i.cursorPos {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -440,7 +440,7 @@ func (i *InputField) Draw(screen tcell.Screen) {
|
||||||
shiftLeft = subWidth - fieldWidth + 1
|
shiftLeft = subWidth - fieldWidth + 1
|
||||||
}
|
}
|
||||||
currentOffset := i.offset
|
currentOffset := i.offset
|
||||||
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
if textPos >= currentOffset {
|
if textPos >= currentOffset {
|
||||||
if shiftLeft > 0 {
|
if shiftLeft > 0 {
|
||||||
i.offset = textPos + textWidth
|
i.offset = textPos + textWidth
|
||||||
|
@ -520,7 +520,7 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
moveRight := func() {
|
moveRight := func() {
|
||||||
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
i.cursorPos += textWidth
|
i.cursorPos += textWidth
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
@ -616,7 +616,7 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
|
||||||
i.offset = 0
|
i.offset = 0
|
||||||
}
|
}
|
||||||
case tcell.KeyDelete, tcell.KeyCtrlD: // Delete character after the cursor.
|
case tcell.KeyDelete, tcell.KeyCtrlD: // Delete character after the cursor.
|
||||||
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
|
i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
@ -688,7 +688,7 @@ func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventM
|
||||||
if action == MouseLeftClick && y == rectY {
|
if action == MouseLeftClick && y == rectY {
|
||||||
// Determine where to place the cursor.
|
// Determine where to place the cursor.
|
||||||
if x >= i.fieldX {
|
if x >= i.fieldX {
|
||||||
if !iterateString(i.text[i.offset:], func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
|
if !iterateString(i.text[i.offset:], func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth, boundaries int) bool {
|
||||||
if x-i.fieldX < screenPos+screenWidth {
|
if x-i.fieldX < screenPos+screenWidth {
|
||||||
i.cursorPos = textPos + i.offset
|
i.cursorPos = textPos + i.offset
|
||||||
return true
|
return true
|
||||||
|
|
619
textarea.go
619
textarea.go
|
@ -1,6 +1,23 @@
|
||||||
package tview
|
package tview
|
||||||
|
|
||||||
import "github.com/gdamore/tcell/v2"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// The minimum capacity of the text area's piece chain slice.
|
||||||
|
pieceChainMinCap = 10
|
||||||
|
|
||||||
|
// The minimum capacity of the text area's edit buffer.
|
||||||
|
editBufferMinCap = 200
|
||||||
|
|
||||||
|
// The maximum number of bytes making up a grapheme cluster. In theory, this
|
||||||
|
// could be longer but it would be highly unusual.
|
||||||
|
maxGraphemeClusterSize = 40
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// NewLine is the string sequence to be inserted when hitting the Enter key
|
// NewLine is the string sequence to be inserted when hitting the Enter key
|
||||||
|
@ -9,9 +26,40 @@ var (
|
||||||
NewLine = "\n"
|
NewLine = "\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// textAreaSpan represents a range of text in a text area. The text area widget
|
||||||
|
// roughly follows the concept of Piece Chains outline in
|
||||||
|
// http://www.catch22.net/tuts/neatpad/piece-chains with some modifications.
|
||||||
|
// This type represents a "span" (or "piece") and thus refers to a subset of the
|
||||||
|
// text in the editor as part of a doubly-linked list.
|
||||||
|
//
|
||||||
|
// In most places where we reference a position in the text, we use a
|
||||||
|
// two-element int array. The first element is the index of the referenced span
|
||||||
|
// in the piece chain. The second element is the offset into the span's
|
||||||
|
// referenced text (relative to the span's start), its value is always >= 0 and
|
||||||
|
// < span.length.
|
||||||
|
//
|
||||||
|
// A range of text is represented by a span range which is a starting position
|
||||||
|
// ([2]int) and an ending position ([2]int). The starting position references
|
||||||
|
// the first character of the range, the ending position references the position
|
||||||
|
// after the last character of the range. The end of the text therefore always
|
||||||
|
// [2]int{1, 0}, position 0 of the ending sentinel.
|
||||||
|
type textAreaSpan struct {
|
||||||
|
// Links to the previous and next textAreaSpan objects as indices into the
|
||||||
|
// TextArea.spans slice. The sentinel spans (index 0 and 1) have -1 as their
|
||||||
|
// previous or next links.
|
||||||
|
previous, next int
|
||||||
|
|
||||||
|
// The start index and the length of the text segment this span represents.
|
||||||
|
// If "length" is negative, the span represents a substring of
|
||||||
|
// TextArea.initialText and the actual length must be its absolute value. If
|
||||||
|
// it is positive, the span represents a substring of TextArea.editText. For
|
||||||
|
// the sentinel spans (index 0 and 1), both values will be 0.
|
||||||
|
offset, length int
|
||||||
|
}
|
||||||
|
|
||||||
// TextArea implements a simple text editor for multi-line text. Multi-color
|
// TextArea implements a simple text editor for multi-line text. Multi-color
|
||||||
// text is not supported. Text can be optionally word-wrapped to fit the
|
// text is not supported. Word-wrapping is enabled by default but can be turned
|
||||||
// available width.
|
// off or be changed to character-wrapping.
|
||||||
//
|
//
|
||||||
// Navigation and Editing
|
// Navigation and Editing
|
||||||
//
|
//
|
||||||
|
@ -33,11 +81,6 @@ var (
|
||||||
// - Alt-Right arrow: Scroll the page to the left, leaving the cursor in its
|
// - Alt-Right arrow: Scroll the page to the left, leaving the cursor in its
|
||||||
// position. Ignored if wrapping is enabled.
|
// position. Ignored if wrapping is enabled.
|
||||||
//
|
//
|
||||||
// If the mouse is enabled, clicking on a screen cell will move the cursor to
|
|
||||||
// that location or to the end of the line if past the last character. Turning
|
|
||||||
// the scroll wheel will scroll the text. Text can also be selected by moving
|
|
||||||
// the mouse while pressing the left mouse button (see below for details).
|
|
||||||
//
|
|
||||||
// Entering a character (rune) will insert it at the current cursor location.
|
// Entering a character (rune) will insert it at the current cursor location.
|
||||||
// Subsequent characters are moved accordingly. If the cursor is outside the
|
// Subsequent characters are moved accordingly. If the cursor is outside the
|
||||||
// visible area, any changes to the text will move it into the visible area. The
|
// visible area, any changes to the text will move it into the visible area. The
|
||||||
|
@ -55,38 +98,84 @@ var (
|
||||||
// character before the cursor up until the next newline character. This may
|
// character before the cursor up until the next newline character. This may
|
||||||
// span multiple lines if wrapping is enabled.
|
// span multiple lines if wrapping is enabled.
|
||||||
//
|
//
|
||||||
// Text can be selected by moving the cursor while holding the Shift key or
|
// Text can be selected by moving the cursor while holding the Shift key. Thus
|
||||||
// dragging the mouse. When text is selected:
|
// when text is selected:
|
||||||
//
|
//
|
||||||
// - Entering a character (rune) will replace the selected text with the new
|
// - Entering a character (rune) will replace the selected text with the new
|
||||||
// character. (The Enter key is an exception, see further below.)
|
// character.
|
||||||
// - Backspace, delete: Delete the selected text.
|
// - Backspace, delete: Delete the selected text.
|
||||||
// - Enter: Copy the selected text into the clipboard, unselect the text.
|
// - Ctrl-Q: Copy the selected text into the clipboard, unselect the text.
|
||||||
// - Ctrl-X: Copy the selected text into the clipboard and delete it.
|
// - Ctrl-X: Copy the selected text into the clipboard and delete it.
|
||||||
// - Ctrl-V: Replace the selected text with the clipboard text. If no text is
|
// - Ctrl-V: Replace the selected text with the clipboard text. If no text is
|
||||||
// selected, the clipboard text will be inserted at the cursor location.
|
// selected, the clipboard text will be inserted at the cursor location.
|
||||||
//
|
//
|
||||||
// The default clipboard is an internal text buffer, i.e. the operating system's
|
// The default clipboard is an internal text buffer, i.e. the operating system's
|
||||||
// clipboard is not used. The Enter key was chosen for the "copy" function
|
// clipboard is not used. The Ctrl-Q key was chosen for the "copy" function
|
||||||
// because the Ctrl-C key is the default key to stop the application. If your
|
// because the Ctrl-C key is the default key to stop the application. If your
|
||||||
// application frees up the global Ctrl-C key and you want to bind it to the
|
// application frees up the global Ctrl-C key and you want to bind it to the
|
||||||
// "copy to clipboard" function, you may use SetInputCapture() to override the
|
// "copy to clipboard" function, you may use SetInputCapture() to override the
|
||||||
// Enter/Ctrl-C keys to implement copying to the clipboard.
|
// Ctrl-Q key to implement copying to the clipboard.
|
||||||
//
|
//
|
||||||
// Similarly, if you want to implement your own clipboard (or make use of your
|
// Similarly, if you want to implement your own clipboard (or make use of your
|
||||||
// operating system's clipboard), you can also use SetInputCapture() to override
|
// operating system's clipboard), you can also use SetInputCapture() to override
|
||||||
// the key binds for copy, cut, and paste. The GetSelection(), ReplaceText(),
|
// the key binds for copy, cut, and paste. The GetSelection(), ReplaceText(),
|
||||||
// and SetSelection() provide all the functionality needed for your own
|
// and SetSelection() provide all the functionality needed for your own
|
||||||
// clipboard.
|
// clipboard. TODO: This will need to be reviewed.
|
||||||
|
//
|
||||||
|
// The text area also supports Undo:
|
||||||
//
|
//
|
||||||
// - Ctrl-Z: Undo the last change.
|
// - Ctrl-Z: Undo the last change.
|
||||||
// - Ctrl-Y: Redo the last change.
|
// - Ctrl-Y: Redo the last Undo change.
|
||||||
|
//
|
||||||
|
// If the mouse is enabled, clicking on a screen cell will move the cursor to
|
||||||
|
// that location or to the end of the line if past the last character. Turning
|
||||||
|
// the scroll wheel will scroll the text. Text can also be selected by moving
|
||||||
|
// the mouse while pressing the left mouse button (see below for details). The
|
||||||
|
// word underneath the mouse cursor can be selected by double-clicking.
|
||||||
|
|
||||||
type TextArea struct {
|
type TextArea struct {
|
||||||
*Box
|
*Box
|
||||||
|
|
||||||
// The text to be shown in the text area when it is empty.
|
// The text to be shown in the text area when it is empty.
|
||||||
placeholder string
|
placeholder string
|
||||||
|
|
||||||
|
// Styles:
|
||||||
|
|
||||||
|
// The style of the text. Background colors different from the Box's
|
||||||
|
// background color may lead to unwanted artefacts.
|
||||||
|
textStyle tcell.Style
|
||||||
|
|
||||||
|
// The style of the placeholder text.
|
||||||
|
placeholderStyle tcell.Style
|
||||||
|
|
||||||
|
// Text manipulation related fields:
|
||||||
|
|
||||||
|
// The text area's text prior to any editing.
|
||||||
|
initialText string
|
||||||
|
|
||||||
|
// Any text that's been added by the user at some point.
|
||||||
|
editText strings.Builder
|
||||||
|
|
||||||
|
// The total length of all text in the text area.
|
||||||
|
length int
|
||||||
|
|
||||||
|
// The maximum number of bytes allowed in the text area. If 0, there is no
|
||||||
|
// limit.
|
||||||
|
maxLength int
|
||||||
|
|
||||||
|
// The piece chain. The first two spans are sentinel spans which don't
|
||||||
|
// reference anything and always remain in the same place. Spans are never
|
||||||
|
// deleted.
|
||||||
|
spans []textAreaSpan
|
||||||
|
|
||||||
|
// The undo stack's items are the first of two consecutive indices into the
|
||||||
|
// spans slice. The first referenced span is a copy of the one before the
|
||||||
|
// modified span range, thse second referenced span is a copy of the one
|
||||||
|
// after the modified span range.
|
||||||
|
undoStack []int
|
||||||
|
|
||||||
|
// Display, navigation, and cursor related fields:
|
||||||
|
|
||||||
// If set to true, lines that are longer than the available width are
|
// If set to true, lines that are longer than the available width are
|
||||||
// wrapped onto the next line. If set to false, any characters beyond the
|
// wrapped onto the next line. If set to false, any characters beyond the
|
||||||
// available width are discarded.
|
// available width are discarded.
|
||||||
|
@ -96,32 +185,49 @@ type TextArea struct {
|
||||||
// after punctuation characters.
|
// after punctuation characters.
|
||||||
wordWrap bool
|
wordWrap bool
|
||||||
|
|
||||||
// The maximum number of bytes allowed in the text area. If 0, there is no
|
|
||||||
// limit.
|
|
||||||
maxLength int
|
|
||||||
|
|
||||||
// The index of the first line shown in the text area.
|
// The index of the first line shown in the text area.
|
||||||
lineOffset int
|
lineOffset int
|
||||||
|
|
||||||
// The number of cells to be skipped on each line (not used in wrap mode).
|
// The number of cells to be skipped on each line (not used in wrap mode).
|
||||||
columnOffset int
|
columnOffset int
|
||||||
|
|
||||||
// The height of the content the last time the text area was drawn.
|
// The inner width of the text area the last time it was drawn.
|
||||||
pageSize int
|
lastWidth int
|
||||||
|
|
||||||
// The style of the text. Background colors different from the Box's
|
// Text positions and states of the start of lines. Each element is a span
|
||||||
// background color may lead to unwanted artefacts.
|
// position (see textAreaSpan) and a state as returned by uniseg.Step(). Not
|
||||||
textStyle tcell.Style
|
// all lines of the text may be contained at any time, extend as needed with
|
||||||
|
// the TextArea.extendLines() function.
|
||||||
|
lineStarts [][3]int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTextArea returns a new text area.
|
// NewTextArea returns a new text area. For an empty text area, provide an empty
|
||||||
func NewTextArea() *TextArea {
|
// string.
|
||||||
return &TextArea{
|
func NewTextArea(text string) *TextArea {
|
||||||
Box: NewBox(),
|
t := &TextArea{
|
||||||
wrap: true,
|
Box: NewBox(),
|
||||||
wordWrap: true,
|
wrap: true,
|
||||||
textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor),
|
wordWrap: true,
|
||||||
|
placeholderStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.TertiaryTextColor),
|
||||||
|
textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor),
|
||||||
|
initialText: text,
|
||||||
|
spans: make([]textAreaSpan, 2, pieceChainMinCap), // We reserve some space to avoid reallocations right when editing starts.
|
||||||
}
|
}
|
||||||
|
t.editText.Grow(editBufferMinCap)
|
||||||
|
t.spans[0] = textAreaSpan{previous: -1, next: 1}
|
||||||
|
t.spans[1] = textAreaSpan{previous: 0, next: -1}
|
||||||
|
if len(text) > 0 {
|
||||||
|
t.spans = append(t.spans, textAreaSpan{
|
||||||
|
previous: 0,
|
||||||
|
next: 1,
|
||||||
|
offset: 0,
|
||||||
|
length: -len(text),
|
||||||
|
})
|
||||||
|
t.spans[0].next = 2
|
||||||
|
t.spans[1].previous = 2
|
||||||
|
t.length = len(text)
|
||||||
|
}
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWrap sets the flag that, if true, leads to lines that are longer than the
|
// SetWrap sets the flag that, if true, leads to lines that are longer than the
|
||||||
|
@ -143,26 +249,439 @@ func (t *TextArea) SetWordWrap(wrapOnWords bool) *TextArea {
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSelection selects the text starting at index "start" and ending just
|
// SetPlaceholder sets the text to be displayed when the text area is empty.
|
||||||
// before the index "end". Any previous selection is discarded. If "start" and
|
func (t *TextArea) SetPlaceholder(placeholder string) *TextArea {
|
||||||
// "end" are the same, currently selected text is unselected.
|
t.placeholder = placeholder
|
||||||
func (t *TextArea) SetSelection(start, end int) *TextArea {
|
|
||||||
//TODO
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSelection returns the currently selected text or an empty string if no
|
// SetMaxLength sets the maximum number of bytes allowed in the text area. If 0,
|
||||||
// text is currently selected. The start and end indices (a half-open range)
|
// there is no limit.
|
||||||
// into the text area's text are also returned.
|
func (t *TextArea) SetMaxLength(maxLength int) *TextArea {
|
||||||
func (t *TextArea) GetSelection() (string, int, int) {
|
t.maxLength = maxLength
|
||||||
return "", 0, 0 //TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplaceText replaces the text in the given range with the given text. The
|
|
||||||
// range is half-open, that is, the character at the "end" index is not
|
|
||||||
// replaced. If the provided range overlaps with a selection, the selected text
|
|
||||||
// will be unselected.
|
|
||||||
func (t *TextArea) ReplaceText(start, end int, text string) *TextArea {
|
|
||||||
//TODO
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTextStyle sets the style of the text. Background colors different from the
|
||||||
|
// Box's background color may lead to unwanted artefacts.
|
||||||
|
func (t *TextArea) SetTextStyle(style tcell.Style) *TextArea {
|
||||||
|
t.textStyle = style
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPlaceholderStyle sets the style of the placeholder text.
|
||||||
|
func (t *TextArea) SetPlaceholderStyle(style tcell.Style) *TextArea {
|
||||||
|
t.placeholderStyle = style
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace deletes a range of text and inserts the given text at that position.
|
||||||
|
// If the resulting text would exceed the maximum length, the function does not
|
||||||
|
// do anything. See textAreaSpan for information about text positions and span
|
||||||
|
// ranges.
|
||||||
|
//
|
||||||
|
// This function does not generate Undo events. Undo events are generated
|
||||||
|
// elsewhere, when the user changes their type of edit.
|
||||||
|
func (t *TextArea) replace(deleteStart, deleteEnd [2]int, insert string) {
|
||||||
|
// Check max length.
|
||||||
|
if t.maxLength > 0 && t.length+len(insert) > t.maxLength {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete.
|
||||||
|
for deleteStart[0] != deleteEnd[0] {
|
||||||
|
if deleteStart[1] == 0 {
|
||||||
|
// Delete this entire span.
|
||||||
|
deleteStart[0] = t.deleteSpan(deleteStart[0])
|
||||||
|
deleteStart[1] = 0
|
||||||
|
} else {
|
||||||
|
// Delete a partial span at the end.
|
||||||
|
if t.spans[deleteStart[0]].length < 0 {
|
||||||
|
// Initial text span. Has negative length.
|
||||||
|
t.length -= -t.spans[deleteStart[0]].length - deleteStart[1]
|
||||||
|
t.spans[deleteStart[0]].length = -deleteStart[1]
|
||||||
|
} else {
|
||||||
|
// Edit buffer span. Has positive length.
|
||||||
|
t.length -= t.spans[deleteStart[0]].length - deleteStart[1]
|
||||||
|
t.spans[deleteStart[0]].length = deleteStart[1]
|
||||||
|
}
|
||||||
|
deleteStart[0] = t.spans[deleteStart[0]].next
|
||||||
|
deleteStart[1] = 0
|
||||||
|
}
|
||||||
|
} // At this point, deleteStart[0] == deleteEnd[0].
|
||||||
|
if deleteEnd[1] > deleteStart[1] {
|
||||||
|
if deleteStart[1] == 0 {
|
||||||
|
// Delete a partial span at the beginning.
|
||||||
|
t.length -= deleteEnd[1]
|
||||||
|
if t.spans[deleteEnd[0]].length < 0 {
|
||||||
|
// Initial text span. Has negative length.
|
||||||
|
t.spans[deleteEnd[0]].length += deleteEnd[1]
|
||||||
|
} else {
|
||||||
|
// Edit buffer span. Has positive length.
|
||||||
|
t.spans[deleteEnd[0]].length -= deleteEnd[1]
|
||||||
|
}
|
||||||
|
t.spans[deleteEnd[0]].offset += deleteEnd[1]
|
||||||
|
} else {
|
||||||
|
// Delete in the middle by splitting the span.
|
||||||
|
deleteStart[0] = t.splitSpan(deleteStart[0], deleteStart[1])
|
||||||
|
deleteStart[1] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert.
|
||||||
|
if len(insert) > 0 {
|
||||||
|
spanIndex, offset := deleteStart[0], deleteStart[1]
|
||||||
|
span := t.spans[spanIndex]
|
||||||
|
|
||||||
|
if offset == 0 {
|
||||||
|
previousSpan := t.spans[span.previous]
|
||||||
|
if previousSpan.length > 0 && previousSpan.offset+previousSpan.length == t.editText.Len() {
|
||||||
|
// We can simply append to the edit buffer.
|
||||||
|
length, _ := t.editText.WriteString(insert)
|
||||||
|
previousSpan.length += length
|
||||||
|
t.length += length
|
||||||
|
} else {
|
||||||
|
// Insert a new span.
|
||||||
|
t.insertSpan(insert, spanIndex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Split and insert.
|
||||||
|
spanIndex = t.splitSpan(spanIndex, offset)
|
||||||
|
t.insertSpan(insert, spanIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteSpan removes the span with the given index from the piece chain. It
|
||||||
|
// returns the index of the span after the deleted span (or the provided index
|
||||||
|
// if no span was deleted due to an invalid span index).
|
||||||
|
//
|
||||||
|
// This function also adjusts TextArea.length.
|
||||||
|
func (t *TextArea) deleteSpan(index int) int {
|
||||||
|
if index < 2 || index >= len(t.spans) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from piece chain.
|
||||||
|
previous := t.spans[index].previous
|
||||||
|
next := t.spans[index].next
|
||||||
|
t.spans[previous].next = next
|
||||||
|
t.spans[next].previous = previous
|
||||||
|
|
||||||
|
// Adjust total length.
|
||||||
|
length := t.spans[index].length
|
||||||
|
if length < 0 {
|
||||||
|
length = -length
|
||||||
|
}
|
||||||
|
t.length -= length
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitSpan splits the span with the given index at the given offset into two
|
||||||
|
// spans. It returns the index of the span after the split or the provided
|
||||||
|
// index if no span was split due to an invalid span index or an invalid
|
||||||
|
// offset.
|
||||||
|
func (t *TextArea) splitSpan(index, offset int) int {
|
||||||
|
if index < 2 || index >= len(t.spans) || offset <= 0 || offset >= t.spans[index].length {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a new trailing span.
|
||||||
|
span := t.spans[index]
|
||||||
|
newSpan := textAreaSpan{
|
||||||
|
previous: index,
|
||||||
|
next: span.next,
|
||||||
|
offset: span.offset + offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust lengths.
|
||||||
|
if span.length < 0 {
|
||||||
|
// Initial text span. Has negative length.
|
||||||
|
newSpan.length = span.length + offset
|
||||||
|
span.length = -offset
|
||||||
|
} else {
|
||||||
|
// Edit buffer span. Has positive length.
|
||||||
|
newSpan.length = span.length - offset
|
||||||
|
span.length = offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert it.
|
||||||
|
newIndex := len(t.spans)
|
||||||
|
t.spans = append(t.spans, newSpan)
|
||||||
|
t.spans[span.next].previous = newIndex
|
||||||
|
span.next = newIndex
|
||||||
|
|
||||||
|
return newIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertSpan inserts the a span with the given text into the piece chain before
|
||||||
|
// the span with the given index and returns the index of the newly inserted
|
||||||
|
// span. If index <= 0, nothing happens and 1 is returned. The text is appended
|
||||||
|
// to the edit buffer. The length of the text is added to TextArea.length.
|
||||||
|
func (t *TextArea) insertSpan(text string, index int) int {
|
||||||
|
if index < 1 || index >= len(t.spans) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a new span.
|
||||||
|
nextSpan := t.spans[index]
|
||||||
|
span := textAreaSpan{
|
||||||
|
previous: nextSpan.previous,
|
||||||
|
next: index,
|
||||||
|
offset: t.editText.Len(),
|
||||||
|
}
|
||||||
|
span.length, _ = t.editText.WriteString(text)
|
||||||
|
|
||||||
|
// Insert into piece chain.
|
||||||
|
newIndex := len(t.spans)
|
||||||
|
t.spans[nextSpan.previous].next = newIndex
|
||||||
|
nextSpan.previous = newIndex
|
||||||
|
t.spans = append(t.spans, span)
|
||||||
|
|
||||||
|
// Adjust text area length.
|
||||||
|
t.length += span.length
|
||||||
|
|
||||||
|
return newIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw draws this primitive onto the screen.
|
||||||
|
func (t *TextArea) Draw(screen tcell.Screen) {
|
||||||
|
t.Box.DrawForSubclass(screen, t)
|
||||||
|
|
||||||
|
// Prepare
|
||||||
|
x, y, width, height := t.GetInnerRect()
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return // We have no space for anything.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder.
|
||||||
|
if t.length == 0 && len(t.placeholder) > 0 {
|
||||||
|
t.drawPlaceholder(screen, x, y, width, height)
|
||||||
|
return // We're done already.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the visible lines are broken over.
|
||||||
|
if t.lastWidth != width && t.lineStarts != nil {
|
||||||
|
t.lineStarts = t.lineStarts[:0]
|
||||||
|
t.lastWidth = width
|
||||||
|
}
|
||||||
|
t.extendLines(width, t.lineOffset+height)
|
||||||
|
if len(t.lineStarts) <= t.lineOffset {
|
||||||
|
return // It's scrolled out of view.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the text.
|
||||||
|
var cluster, text string
|
||||||
|
line := t.lineOffset
|
||||||
|
pos := t.lineStarts[line]
|
||||||
|
endPos := pos
|
||||||
|
posX, posY := x, y
|
||||||
|
for pos[0] != 1 {
|
||||||
|
cluster, text, _, pos, endPos = t.step(text, pos, endPos)
|
||||||
|
clusterWidth := stringWidth(cluster)
|
||||||
|
runes := []rune(cluster)
|
||||||
|
if posX+clusterWidth <= x+width {
|
||||||
|
screen.SetContent(posX, posY, runes[0], runes[1:], t.textStyle)
|
||||||
|
}
|
||||||
|
posX += clusterWidth
|
||||||
|
if line+1 < len(t.lineStarts) && t.lineStarts[line+1] == pos {
|
||||||
|
// We must break over.
|
||||||
|
posY++
|
||||||
|
if posY >= y+height {
|
||||||
|
break // Done.
|
||||||
|
}
|
||||||
|
posX = x
|
||||||
|
line++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawPlaceholder draws the placeholder text into the given rectangle. It does
|
||||||
|
// not do anything if the text area already contains text or if there is no
|
||||||
|
// placeholder text.
|
||||||
|
func (t *TextArea) drawPlaceholder(screen tcell.Screen, x, y, width, height int) {
|
||||||
|
posX, posY := x, y
|
||||||
|
lastLineBreak, lastGraphemeBreak := x, x // Screen positions of the last possible line/grapheme break.
|
||||||
|
iterateString(t.placeholder, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
|
if posX+screenWidth > x+width {
|
||||||
|
// This character doesn't fit. Break over to the next line.
|
||||||
|
// Perform word wrapping first by copying the last word over to
|
||||||
|
// the next line.
|
||||||
|
clearX := lastLineBreak
|
||||||
|
if lastLineBreak == x {
|
||||||
|
clearX = lastGraphemeBreak
|
||||||
|
}
|
||||||
|
posY++
|
||||||
|
if posY >= y+height {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
newPosX := x
|
||||||
|
for clearX < posX {
|
||||||
|
main, comb, _, _ := screen.GetContent(clearX, posY-1)
|
||||||
|
screen.SetContent(clearX, posY-1, ' ', nil, tcell.StyleDefault.Background(t.backgroundColor))
|
||||||
|
screen.SetContent(newPosX, posY, main, comb, t.placeholderStyle)
|
||||||
|
clearX++
|
||||||
|
newPosX++
|
||||||
|
}
|
||||||
|
lastLineBreak, lastGraphemeBreak, posX = x, x, newPosX
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw this character.
|
||||||
|
screen.SetContent(posX, posY, main, comb, t.placeholderStyle)
|
||||||
|
posX += screenWidth
|
||||||
|
switch boundaries & uniseg.MaskLine {
|
||||||
|
case uniseg.LineMustBreak:
|
||||||
|
posY++
|
||||||
|
if posY >= y+height {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
posX = x
|
||||||
|
case uniseg.LineCanBreak:
|
||||||
|
lastLineBreak = posX
|
||||||
|
}
|
||||||
|
lastGraphemeBreak = posX
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// extendLines traverses the current text and extends t.lineStarts such that it
|
||||||
|
// describes at least maxLines+1 lines (or less if the text is shorter). Text is
|
||||||
|
// laid out for the given width while respecting the wrapping settings. It is
|
||||||
|
// assumed that if t.lineStarts already has entries, they obey the same rules.
|
||||||
|
//
|
||||||
|
// If width is 0, nothing happens.
|
||||||
|
func (t *TextArea) extendLines(width, maxLines int) {
|
||||||
|
if width <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with the first span.
|
||||||
|
if len(t.lineStarts) == 0 {
|
||||||
|
if len(t.spans) > 2 {
|
||||||
|
t.lineStarts = append(t.lineStarts, [3]int{t.spans[0].next, 0, -1})
|
||||||
|
} else {
|
||||||
|
return // No text.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine starting positions and starting spans.
|
||||||
|
pos := t.lineStarts[len(t.lineStarts)-1] // The starting position is the last known line.
|
||||||
|
endPos := pos
|
||||||
|
var (
|
||||||
|
cluster, text string
|
||||||
|
lineWidth, boundaries int
|
||||||
|
lastGraphemeBreak, lastLineBreak [3]int
|
||||||
|
widthSinceLineBreak int
|
||||||
|
)
|
||||||
|
for pos[0] != 1 {
|
||||||
|
// Get the next grapheme cluster.
|
||||||
|
cluster, text, boundaries, pos, endPos = t.step(text, pos, endPos)
|
||||||
|
clusterWidth := stringWidth(cluster)
|
||||||
|
lineWidth += clusterWidth
|
||||||
|
widthSinceLineBreak += clusterWidth
|
||||||
|
|
||||||
|
// Any line breaks?
|
||||||
|
if !t.wrap || lineWidth <= width {
|
||||||
|
if pos[0] != 1 && boundaries&uniseg.MaskLine == uniseg.LineMustBreak {
|
||||||
|
// We must break over.
|
||||||
|
t.lineStarts = append(t.lineStarts, pos)
|
||||||
|
lineWidth = 0
|
||||||
|
lastGraphemeBreak = [3]int{}
|
||||||
|
lastLineBreak = [3]int{}
|
||||||
|
widthSinceLineBreak = 0
|
||||||
|
if len(t.lineStarts) > maxLines {
|
||||||
|
break // We have enough lines, we can stop.
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else { // t.wrap && lineWidth > width
|
||||||
|
if !t.wordWrap || lastLineBreak == [3]int{} {
|
||||||
|
if lastGraphemeBreak != [3]int{} { // We have at least one character on each line.
|
||||||
|
// Break after last grapheme.
|
||||||
|
t.lineStarts = append(t.lineStarts, lastGraphemeBreak)
|
||||||
|
lineWidth = clusterWidth
|
||||||
|
lastLineBreak = [3]int{}
|
||||||
|
}
|
||||||
|
} else { // t.wordWrap && lastLineBreak != [3]int{}
|
||||||
|
// Break after last line break opportunity.
|
||||||
|
t.lineStarts = append(t.lineStarts, lastLineBreak)
|
||||||
|
lineWidth = widthSinceLineBreak
|
||||||
|
lastLineBreak = [3]int{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze break opportunities.
|
||||||
|
if boundaries&uniseg.MaskLine == uniseg.LineCanBreak {
|
||||||
|
lastLineBreak = pos
|
||||||
|
widthSinceLineBreak = 0
|
||||||
|
}
|
||||||
|
lastGraphemeBreak = pos
|
||||||
|
|
||||||
|
// Can we stop?
|
||||||
|
if len(t.lineStarts) > maxLines {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// step is similar to uniseg.StepString() but it iterates over the piece chain,
|
||||||
|
// starting with "pos", a span position plus state (which may be -1 for the
|
||||||
|
// start of the text). The returned "boundaries" value is same value returned by
|
||||||
|
// uniseg.StepString(). The "pos" and "endPos" positions refer to the start and
|
||||||
|
// the end of the "text" string, respectively. For the first call, text may be
|
||||||
|
// empty and pos/endPos may be the same. For consecutive calls, provide "rest"
|
||||||
|
// as the text and "newPos" and "newEndPos" as the new positions/states. An
|
||||||
|
// empty "rest" string indicates the end of the text. The "endPos" state is not
|
||||||
|
// used.
|
||||||
|
func (t *TextArea) step(text string, pos, endPos [3]int) (cluster, rest string, boundaries int, newPos, newEndPos [3]int) {
|
||||||
|
if pos[0] == 1 {
|
||||||
|
return // We're already past the end.
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to make sure we have a text at least the size of a grapheme
|
||||||
|
// cluster.
|
||||||
|
span := t.spans[pos[0]]
|
||||||
|
if len(text) < maxGraphemeClusterSize &&
|
||||||
|
(span.length < 0 && -span.length-pos[1] >= maxGraphemeClusterSize ||
|
||||||
|
span.length > 0 && t.spans[pos[0]].length-pos[1] >= maxGraphemeClusterSize) {
|
||||||
|
// We can use a substring of one span.
|
||||||
|
if span.length < 0 {
|
||||||
|
text = t.initialText[span.offset+pos[1] : span.offset-span.length]
|
||||||
|
} else {
|
||||||
|
text = t.editText.String()[span.offset+pos[1] : span.offset+span.length]
|
||||||
|
}
|
||||||
|
endPos = [3]int{span.next, 0, -1}
|
||||||
|
} else {
|
||||||
|
// We have to compose the text from multiple spans.
|
||||||
|
for len(text) < maxGraphemeClusterSize && endPos[0] != 1 {
|
||||||
|
endSpan := t.spans[endPos[0]]
|
||||||
|
var moreText string
|
||||||
|
if endSpan.length < 0 {
|
||||||
|
moreText = t.initialText[endSpan.offset+endPos[1] : endSpan.offset-endSpan.length]
|
||||||
|
} else {
|
||||||
|
moreText = t.editText.String()[endSpan.offset+endPos[1] : endSpan.offset+endSpan.length]
|
||||||
|
}
|
||||||
|
if len(moreText) > maxGraphemeClusterSize {
|
||||||
|
moreText = moreText[:maxGraphemeClusterSize]
|
||||||
|
}
|
||||||
|
text += moreText
|
||||||
|
endPos[1] += len(moreText)
|
||||||
|
if endPos[1] >= endSpan.length {
|
||||||
|
endPos[0], endPos[1] = endSpan.next, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the grapheme cluster iterator.
|
||||||
|
cluster, text, boundaries, pos[2] = uniseg.StepString(text, pos[2])
|
||||||
|
pos[1] += len(cluster)
|
||||||
|
for pos[0] != 1 && (span.length < 0 && pos[1] >= -span.length || span.length >= 0 && pos[1] >= span.length) {
|
||||||
|
pos[0] = span.next
|
||||||
|
pos[1] -= span.length
|
||||||
|
span = t.spans[pos[0]]
|
||||||
|
}
|
||||||
|
|
||||||
|
return cluster, text, boundaries, pos, endPos
|
||||||
|
}
|
||||||
|
|
|
@ -1143,7 +1143,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
|
||||||
// Print the line.
|
// Print the line.
|
||||||
if y+line-t.lineOffset >= 0 {
|
if y+line-t.lineOffset >= 0 {
|
||||||
var colorPos, regionPos, escapePos, tagOffset, skipped int
|
var colorPos, regionPos, escapePos, tagOffset, skipped int
|
||||||
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
// Process tags.
|
// Process tags.
|
||||||
for {
|
for {
|
||||||
if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
|
if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
|
||||||
|
|
26
util.go
26
util.go
|
@ -262,7 +262,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth,
|
||||||
foregroundColor, backgroundColor, attributes string
|
foregroundColor, backgroundColor, attributes string
|
||||||
)
|
)
|
||||||
originalStyle := style
|
originalStyle := style
|
||||||
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
// Update color/escape tag offset and style.
|
// Update color/escape tag offset and style.
|
||||||
if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
|
if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
|
||||||
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
||||||
|
@ -305,7 +305,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth,
|
||||||
for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth {
|
for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth {
|
||||||
if skipWidth > 0 || choppedLeft < choppedRight {
|
if skipWidth > 0 || choppedLeft < choppedRight {
|
||||||
// Iterate on the left by one character.
|
// Iterate on the left by one character.
|
||||||
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
if skipWidth > 0 {
|
if skipWidth > 0 {
|
||||||
skipWidth -= screenWidth
|
skipWidth -= screenWidth
|
||||||
strippedWidth -= screenWidth
|
strippedWidth -= screenWidth
|
||||||
|
@ -369,7 +369,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth,
|
||||||
drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int
|
drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int
|
||||||
foregroundColor, backgroundColor, attributes string
|
foregroundColor, backgroundColor, attributes string
|
||||||
)
|
)
|
||||||
iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
|
iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth, boundaries int) bool {
|
||||||
// Skip character if necessary.
|
// Skip character if necessary.
|
||||||
if skipWidth > 0 {
|
if skipWidth > 0 {
|
||||||
skipWidth -= screenWidth
|
skipWidth -= screenWidth
|
||||||
|
@ -496,7 +496,7 @@ func WordWrap(text string, width int) (lines []string) {
|
||||||
}
|
}
|
||||||
return substr
|
return substr
|
||||||
}
|
}
|
||||||
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||||
// Handle tags.
|
// Handle tags.
|
||||||
for {
|
for {
|
||||||
if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
|
if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
|
||||||
|
@ -582,16 +582,18 @@ func Escape(text string) string {
|
||||||
// Unicode code points of the character (the first rune and any combining runes
|
// Unicode code points of the character (the first rune and any combining runes
|
||||||
// which may be nil if there aren't any), the starting position (in bytes)
|
// which may be nil if there aren't any), the starting position (in bytes)
|
||||||
// within the original string, its length in bytes, the screen position of the
|
// within the original string, its length in bytes, the screen position of the
|
||||||
// character, and the screen width of it. The iteration stops if the callback
|
// character, the screen width of it, and a boundaries value which includes
|
||||||
// returns true. This function returns true if the iteration was stopped before
|
// word/sentence boundary or line break information (see the
|
||||||
// the last character.
|
// github.com/rivo/uniseg package, Step() function, for more information). The
|
||||||
func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
|
// iteration stops if the callback returns true. This function returns true if
|
||||||
var screenPos, textPos int
|
// the iteration was stopped before the last character.
|
||||||
|
func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool) bool {
|
||||||
|
var screenPos, textPos, boundaries int
|
||||||
|
|
||||||
state := -1
|
state := -1
|
||||||
for len(text) > 0 {
|
for len(text) > 0 {
|
||||||
var cluster string
|
var cluster string
|
||||||
cluster, text, _, state = uniseg.FirstGraphemeClusterInString(text, state)
|
cluster, text, boundaries, state = uniseg.StepString(text, state)
|
||||||
|
|
||||||
var width int
|
var width int
|
||||||
runes := make([]rune, 0, len(cluster))
|
runes := make([]rune, 0, len(cluster))
|
||||||
|
@ -608,7 +610,7 @@ func iterateString(text string, callback func(main rune, comb []rune, textPos, t
|
||||||
comb = runes[1:]
|
comb = runes[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
if callback(runes[0], comb, textPos, len(cluster), screenPos, width) {
|
if callback(runes[0], comb, textPos, len(cluster), screenPos, width, boundaries) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -636,7 +638,7 @@ func iterateStringReverse(text string, callback func(main rune, comb []rune, tex
|
||||||
|
|
||||||
// Create the grapheme clusters.
|
// Create the grapheme clusters.
|
||||||
var clusters []cluster
|
var clusters []cluster
|
||||||
iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
|
iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth, boundaries int) bool {
|
||||||
clusters = append(clusters, cluster{
|
clusters = append(clusters, cluster{
|
||||||
main: main,
|
main: main,
|
||||||
comb: comb,
|
comb: comb,
|
||||||
|
|
Loading…
Reference in New Issue