mirror of
https://github.com/rivo/tview.git
synced 2025-04-24 13:48:56 +08:00
Implemented string parser and migrated all widgets but TextView.
This commit is contained in:
parent
ccc2c81197
commit
caea67a4ef
6
ansi.go
6
ansi.go
@ -31,7 +31,7 @@ type ansi struct {
|
||||
}
|
||||
|
||||
// ANSIWriter returns an io.Writer which translates any ANSI escape codes
|
||||
// written to it into tview color tags. Other escape codes don't have an effect
|
||||
// written to it into tview style tags. Other escape codes don't have an effect
|
||||
// and are simply removed. The translated text is written to the provided
|
||||
// writer.
|
||||
func ANSIWriter(writer io.Writer) io.Writer {
|
||||
@ -45,7 +45,7 @@ func ANSIWriter(writer io.Writer) io.Writer {
|
||||
}
|
||||
|
||||
// Write parses the given text as a string of runes, translates ANSI escape
|
||||
// codes to color tags and writes them to the output writer.
|
||||
// codes to style tags and writes them to the output writer.
|
||||
func (a *ansi) Write(text []byte) (int, error) {
|
||||
defer func() {
|
||||
a.buffer.Reset()
|
||||
@ -249,7 +249,7 @@ func (a *ansi) Write(text []byte) (int, error) {
|
||||
}
|
||||
|
||||
// TranslateANSI replaces ANSI escape sequences found in the provided string
|
||||
// with tview's color tags and returns the resulting string.
|
||||
// with tview's style tags and returns the resulting string.
|
||||
func TranslateANSI(text string) string {
|
||||
var buffer bytes.Buffer
|
||||
writer := ANSIWriter(&buffer)
|
||||
|
8
box.go
8
box.go
@ -387,9 +387,13 @@ func (b *Box) DrawForSubclass(screen tcell.Screen, p Primitive) {
|
||||
if b.title != "" && 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)
|
||||
xEllipsis := b.x + b.width - 2
|
||||
if b.titleAlign == AlignRight {
|
||||
xEllipsis = b.x + 1
|
||||
}
|
||||
_, _, style, _ := screen.GetContent(xEllipsis, b.y)
|
||||
fg, _, _ := style.Decompose()
|
||||
Print(screen, string(SemigraphicsHorizontalEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg)
|
||||
Print(screen, string(SemigraphicsHorizontalEllipsis), xEllipsis, b.y, 1, AlignLeft, fg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ func main() {
|
||||
box := tview.NewBox().
|
||||
SetBorder(true).
|
||||
SetBorderAttributes(tcell.AttrBold).
|
||||
SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] [black:red]c[:yellow]o[:green]l[:darkcyan]o[:blue]r[:darkmagenta]f[:red]u[:yellow]l[white:] [::bu]title")
|
||||
SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] [black:red]c[:yellow]o[:green]l[:darkcyan]o[:blue]r[:darkmagenta]f[:red]u[:yellow]l[white:-] [::bu]title")
|
||||
if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ func main() {
|
||||
SetLabel("Enter a number: ").
|
||||
SetPlaceholder("E.g. 1234").
|
||||
SetFieldWidth(10).
|
||||
SetAcceptanceFunc(tview.InputFieldInteger).
|
||||
//SetAcceptanceFunc(tview.InputFieldInteger).
|
||||
SetDoneFunc(func(key tcell.Key) {
|
||||
app.Stop()
|
||||
})
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
const colorsText = `You can use color tags almost everywhere to partially change the color of a string. Simply put a color name or hex string in square brackets to change [::s]all[::-]the following characters' color. H[green]er[white]e i[yellow]s a[darkcyan]n ex[red]amp[white]le. [::i]The [black:red]tags [black:green]look [black:yellow]like [::u]this: [blue:yellow:u[] [#00ff00[]`
|
||||
const colorsText = `You can use style tags almost everywhere to partially change the color of a string. Simply put a color name or hex string in square brackets to change [::s]all[::-]the following characters' color. H[green]er[white]e i[yellow]s a[darkcyan]n ex[red]amp[white]le. [::i]The [black:red]tags [black:green]look [black:yellow]like [::u]this: [blue:yellow:u[] or [#00ff00[]. [:::https://github.com/rivo/tview]Hyperlinks[:::-] are also supported.`
|
||||
|
||||
// Colors demonstrates how to use colors.
|
||||
func Colors(nextSlide func()) (title string, content tview.Primitive) {
|
||||
@ -28,6 +28,6 @@ func Colors(nextSlide func()) (title string, content tview.Primitive) {
|
||||
}
|
||||
table.SetBorderPadding(1, 1, 2, 2).
|
||||
SetBorder(true).
|
||||
SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] [black:red]c[:yellow]o[:green]l[:darkcyan]o[:blue]r[:darkmagenta]f[:red]u[:yellow]l[white:] [::bu]title")
|
||||
return "Colors", Center(78, 19, table)
|
||||
SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] [black:red]c[:yellow]o[:green]l[:darkcyan]o[:blue]r[:darkmagenta]f[:red]u[:yellow]l[white:-] [::bu]title")
|
||||
return "Colors", Center(82, 19, table)
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ func TextView2(nextSlide func()) (title string, content tview.Primitive) {
|
||||
codeView := tview.NewTextView().
|
||||
SetWrap(false)
|
||||
fmt.Fprint(codeView, textView2)
|
||||
codeView.SetBorder(true).SetTitle("Buffer content")
|
||||
codeView.SetBorder(true).SetTitle("Raw text")
|
||||
|
||||
textView := tview.NewTextView()
|
||||
textView.SetDynamicColors(true).
|
||||
|
60
doc.go
60
doc.go
@ -64,37 +64,41 @@ You will find more demos in the "demos" subdirectory. It also contains a
|
||||
presentation (written using tview) which gives an overview of the different
|
||||
widgets and how they can be used.
|
||||
|
||||
# Colors
|
||||
# Styles, Colors, and Hyperlinks
|
||||
|
||||
Throughout this package, colors are specified using the [tcell.Color] type.
|
||||
Functions such as [tcell.GetColor], [tcell.NewHexColor], and [tcell.NewRGBColor]
|
||||
can be used to create colors from W3C color names or RGB values.
|
||||
Throughout this package, styles are specified using the [tcell.Style] type.
|
||||
Styles specify colors with the [tcell.Color] type. Functions such as
|
||||
[tcell.GetColor], [tcell.NewHexColor], and [tcell.NewRGBColor] can be used to
|
||||
create colors from W3C color names or RGB values. The [tcell.Style] type also
|
||||
allows you to specify text attributes such as "bold" or "underline" or a URL
|
||||
which some terminals use to display hyperlinks.
|
||||
|
||||
Almost all strings which are displayed can contain color tags. Color tags are
|
||||
W3C color names or six hexadecimal digits following a hash tag, wrapped in
|
||||
square brackets. Examples:
|
||||
Almost all strings which are displayed may contain style tags. A style tag's
|
||||
content is always wrapped in square brackets. In its simplest form, a style tag
|
||||
specifies the foreground color of the text. Colors in these tags are W3C color
|
||||
names or six hexadecimal digits following a hash tag. Examples:
|
||||
|
||||
This is a [red]warning[white]!
|
||||
The sky is [#8080ff]blue[#ffffff].
|
||||
|
||||
A color tag changes the color of the characters following that color tag. This
|
||||
applies to almost everything from box titles, list text, form item labels, to
|
||||
table cells. In a TextView, this functionality has to be switched on explicitly.
|
||||
See the TextView documentation for more information.
|
||||
A style tag changes the style of the characters following that style tag. There
|
||||
is no style stack and no nesting of style tags.
|
||||
|
||||
Color tags may contain not just the foreground (text) color but also the
|
||||
background color and additional flags. In fact, the full definition of a color
|
||||
tag is as follows:
|
||||
Style tags are used in almost everything from box titles, list text, form item
|
||||
labels, to table cells. In a [TextView], this functionality has to be switched
|
||||
on explicitly. See the [TextView] documentation for more information.
|
||||
|
||||
[<foreground>:<background>:<flags>]
|
||||
A style tag's full format looks like this:
|
||||
|
||||
Each of the three fields can be left blank and trailing fields can be omitted.
|
||||
(Empty square brackets "[]", however, are not considered color tags.) Colors
|
||||
[<foreground>:<background>:<attribute flags>:<url>]
|
||||
|
||||
Each of the four fields can be left blank and trailing fields can be omitted.
|
||||
(Empty square brackets "[]", however, are not considered style tags.) Fields
|
||||
that are not specified will be left unchanged. A field with just a dash ("-")
|
||||
means "reset to default".
|
||||
|
||||
You can specify the following flags (some flags may not be supported by your
|
||||
terminal):
|
||||
You can specify the following flags to turn on certain attributes (some flags
|
||||
may not be supported by your terminal):
|
||||
|
||||
l: blink
|
||||
b: bold
|
||||
@ -104,6 +108,15 @@ terminal):
|
||||
u: underline
|
||||
s: strike-through
|
||||
|
||||
Use uppercase letters to turn off the corresponding attribute, for example,
|
||||
"B" to turn off bold. Uppercase letters have no effect if the attribute was not
|
||||
previously set.
|
||||
|
||||
Setting a URL allows you to turn a piece of text into a hyperlink in some
|
||||
terminals. Specify a dash ("-") to specify the end of the hyperlink. Hyperlinks
|
||||
must only contain single-byte characters (e.g. ASCII), excluding bracket
|
||||
characters ("[" or "]").
|
||||
|
||||
Examples:
|
||||
|
||||
[yellow]Yellow text
|
||||
@ -113,9 +126,12 @@ Examples:
|
||||
[::bl]Bold, blinking text
|
||||
[::-]Colors unchanged, flags reset
|
||||
[-]Reset foreground color
|
||||
[-:-:-]Reset everything
|
||||
[::i]Italic and [::I]not italic
|
||||
Click [:::https://example.com]here[:::-] for example.com.
|
||||
Send an email to [:::mailto:her@example.com]her/[:::mail:him@example.com]him[:::-].
|
||||
[-:-:-:-]Reset everything
|
||||
[:]No effect
|
||||
[]Not a valid color tag, will print square brackets as they are
|
||||
[]Not a valid style tag, will print square brackets as they are
|
||||
|
||||
In the rare event that you want to display a string such as "[red]" or
|
||||
"[#00ff1a]" without applying its effect, you need to put an opening square
|
||||
@ -127,7 +143,7 @@ character that may be used in color or region tags will be recognized. Examples:
|
||||
["123"[] will be output as ["123"]
|
||||
[#6aff00[[] will be output as [#6aff00[]
|
||||
[a#"[[[] will be output as [a#"[[]
|
||||
[] will be output as [] (see color tags above)
|
||||
[] will be output as [] (see style tags above)
|
||||
[[] will be output as [[] (not an escaped tag)
|
||||
|
||||
You can use the Escape() function to insert brackets automatically where needed.
|
||||
|
10
form.go
10
form.go
@ -119,6 +119,7 @@ func NewForm() *Form {
|
||||
fieldTextColor: Styles.PrimaryTextColor,
|
||||
buttonStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
|
||||
buttonActivatedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.ContrastBackgroundColor),
|
||||
buttonDisabledStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.ContrastSecondaryTextColor),
|
||||
lastFinishedKey: tcell.KeyTab, // To skip over inactive elements at the beginning of the form.
|
||||
}
|
||||
|
||||
@ -195,6 +196,12 @@ func (f *Form) SetButtonActivatedStyle(style tcell.Style) *Form {
|
||||
return f
|
||||
}
|
||||
|
||||
// SetButtonDisabledStyle sets the style of the buttons when they are disabled.
|
||||
func (f *Form) SetButtonDisabledStyle(style tcell.Style) *Form {
|
||||
f.buttonDisabledStyle = style
|
||||
return f
|
||||
}
|
||||
|
||||
// SetFocus shifts the focus to the form element with the given index, counting
|
||||
// non-button items first and buttons last. Note that this index is only used
|
||||
// when the form itself receives focus.
|
||||
@ -605,7 +612,8 @@ func (f *Form) Draw(screen tcell.Screen) {
|
||||
buttonWidth = space
|
||||
}
|
||||
button.SetStyle(f.buttonStyle).
|
||||
SetActivatedStyle(f.buttonActivatedStyle)
|
||||
SetActivatedStyle(f.buttonActivatedStyle).
|
||||
SetDisabledStyle(f.buttonDisabledStyle)
|
||||
|
||||
buttonIndex := index + len(f.items)
|
||||
positions[buttonIndex].x = x
|
||||
|
2
image.go
2
image.go
@ -729,7 +729,7 @@ func (i *Image) Draw(screen tcell.Screen) {
|
||||
viewX += labelWidth
|
||||
viewWidth -= labelWidth
|
||||
} else {
|
||||
_, drawnWidth, _, _ := printWithStyle(screen, i.label, viewX, viewY, 0, viewWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
|
||||
_, _, drawnWidth := printWithStyle(screen, i.label, viewX, viewY, 0, viewWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
|
||||
viewX += drawnWidth
|
||||
viewWidth -= drawnWidth
|
||||
}
|
||||
|
109
inputfield.go
109
inputfield.go
@ -310,7 +310,7 @@ func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entr
|
||||
|
||||
// SetAutocompletedFunc sets a callback function which is invoked when the user
|
||||
// selects an entry from the autocomplete drop-down list. The function is passed
|
||||
// the text of the selected entry (stripped of any color tags), the index of the
|
||||
// the text of the selected entry (stripped of any style tags), the index of the
|
||||
// entry, and the user action that caused the selection, e.g.
|
||||
// [AutocompletedNavigate]. It returns true if the autocomplete drop-down should
|
||||
// be closed after the callback returns or false if it should remain open, in
|
||||
@ -454,7 +454,7 @@ func (i *InputField) Draw(screen tcell.Screen) {
|
||||
printWithStyle(screen, i.label, x, y, 0, labelWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
|
||||
x += labelWidth
|
||||
} else {
|
||||
_, drawnWidth, _, _ := printWithStyle(screen, i.label, x, y, 0, width, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
|
||||
_, _, drawnWidth := printWithStyle(screen, i.label, x, y, 0, width, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
|
||||
x += drawnWidth
|
||||
}
|
||||
|
||||
@ -498,13 +498,20 @@ func (i *InputField) Draw(screen tcell.Screen) {
|
||||
// We have enough space for the full text.
|
||||
printWithStyle(screen, Escape(text), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true)
|
||||
i.offset = 0
|
||||
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||
if textPos >= i.cursorPos {
|
||||
return true
|
||||
// Find cursor position.
|
||||
var (
|
||||
state *stepState
|
||||
textPos int
|
||||
)
|
||||
str := text
|
||||
for len(str) > 0 {
|
||||
_, str, state = step(str, state, stepOptionsNone)
|
||||
textPos += state.GrossLength()
|
||||
if textPos > i.cursorPos {
|
||||
break
|
||||
}
|
||||
cursorScreenPos += screenWidth
|
||||
return false
|
||||
})
|
||||
cursorScreenPos += state.Width()
|
||||
}
|
||||
} else {
|
||||
// The text doesn't fit. Where is the cursor?
|
||||
if i.cursorPos < 0 {
|
||||
@ -520,20 +527,26 @@ func (i *InputField) Draw(screen tcell.Screen) {
|
||||
shiftLeft = subWidth - fieldWidth + 1
|
||||
}
|
||||
currentOffset := i.offset
|
||||
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||
var (
|
||||
state *stepState
|
||||
textPos int
|
||||
)
|
||||
str := text
|
||||
for len(str) > 0 {
|
||||
_, str, state = step(str, state, stepOptionsNone)
|
||||
if textPos >= currentOffset {
|
||||
if shiftLeft > 0 {
|
||||
i.offset = textPos + textWidth
|
||||
shiftLeft -= screenWidth
|
||||
i.offset = textPos + state.GrossLength()
|
||||
shiftLeft -= state.Width()
|
||||
} else {
|
||||
if textPos+textWidth > i.cursorPos {
|
||||
return true
|
||||
if textPos+state.GrossLength() > i.cursorPos {
|
||||
break
|
||||
}
|
||||
cursorScreenPos += screenWidth
|
||||
cursorScreenPos += state.Width()
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
textPos += state.GrossLength()
|
||||
}
|
||||
printWithStyle(screen, Escape(text[i.offset:]), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true)
|
||||
}
|
||||
}
|
||||
@ -598,16 +611,22 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
|
||||
home := func() { i.cursorPos = 0 }
|
||||
end := func() { i.cursorPos = len(i.text) }
|
||||
moveLeft := func() {
|
||||
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
i.cursorPos -= textWidth
|
||||
return true
|
||||
})
|
||||
var state *stepState
|
||||
str := i.text
|
||||
for len(str) > 0 {
|
||||
_, str, state = step(str, state, stepOptionsNone)
|
||||
if len(str) <= len(i.text)-i.cursorPos {
|
||||
i.cursorPos -= state.GrossLength()
|
||||
if i.cursorPos < 0 {
|
||||
i.cursorPos = 0
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
moveRight := func() {
|
||||
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||
i.cursorPos += textWidth
|
||||
return true
|
||||
})
|
||||
_, _, state := step(i.text[i.cursorPos:], nil, stepOptionsNone)
|
||||
i.cursorPos += state.GrossLength()
|
||||
}
|
||||
moveWordLeft := func() {
|
||||
i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
|
||||
@ -718,19 +737,24 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
|
||||
i.cursorPos -= len(i.text) - len(newText)
|
||||
i.text = newText
|
||||
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
|
||||
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
i.text = i.text[:textPos] + i.text[textPos+textWidth:]
|
||||
i.cursorPos -= textWidth
|
||||
return true
|
||||
})
|
||||
var state *stepState
|
||||
str := i.text
|
||||
for len(str) > 0 && i.cursorPos > 0 {
|
||||
_, str, state = step(str, state, stepOptionsNone)
|
||||
if len(str) <= len(i.text)-i.cursorPos {
|
||||
i.cursorPos -= state.GrossLength()
|
||||
i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+state.GrossLength():]
|
||||
break
|
||||
}
|
||||
}
|
||||
if i.offset >= i.cursorPos {
|
||||
i.offset = 0
|
||||
}
|
||||
case tcell.KeyDelete, tcell.KeyCtrlD: // Delete character after the cursor.
|
||||
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:]
|
||||
return true
|
||||
})
|
||||
if len(i.text) > i.cursorPos {
|
||||
_, rest, _ := step(i.text[i.cursorPos:], nil, stepOptionsNone)
|
||||
i.text = i.text[:i.cursorPos] + rest
|
||||
}
|
||||
case tcell.KeyLeft:
|
||||
if event.Modifiers()&tcell.ModAlt > 0 {
|
||||
moveWordLeft()
|
||||
@ -815,14 +839,19 @@ func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventM
|
||||
} else if action == MouseLeftClick {
|
||||
// Determine where to place the cursor.
|
||||
if x >= i.fieldX {
|
||||
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 {
|
||||
i.cursorPos = textPos + i.offset
|
||||
return true
|
||||
var (
|
||||
state *stepState
|
||||
screenPos int
|
||||
str = i.text[i.offset:]
|
||||
)
|
||||
i.cursorPos = i.offset
|
||||
for len(str) > 0 {
|
||||
_, str, state = step(str, state, stepOptionsNone)
|
||||
screenPos += state.Width()
|
||||
if screenPos > x-i.fieldX {
|
||||
break
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
i.cursorPos = len(i.text)
|
||||
i.cursorPos += state.GrossLength()
|
||||
}
|
||||
}
|
||||
consumed = true
|
||||
|
6
list.go
6
list.go
@ -253,7 +253,7 @@ func (l *List) SetShortcutStyle(style tcell.Style) *List {
|
||||
|
||||
// SetSelectedTextColor sets the text color of selected items. Note that the
|
||||
// color of main text characters that are different from the main text color
|
||||
// (e.g. color tags) is maintained.
|
||||
// (e.g. style tags) is maintained.
|
||||
func (l *List) SetSelectedTextColor(color tcell.Color) *List {
|
||||
l.selectedStyle = l.selectedStyle.Foreground(color)
|
||||
return l
|
||||
@ -514,7 +514,7 @@ func (l *List) Draw(screen tcell.Screen) {
|
||||
}
|
||||
|
||||
// Main text.
|
||||
_, printedWidth, _, end := printWithStyle(screen, item.MainText, x, y, l.horizontalOffset, width, AlignLeft, l.mainTextStyle, true)
|
||||
_, end, printedWidth := printWithStyle(screen, item.MainText, x, y, l.horizontalOffset, width, AlignLeft, l.mainTextStyle, true)
|
||||
if printedWidth > maxWidth {
|
||||
maxWidth = printedWidth
|
||||
}
|
||||
@ -551,7 +551,7 @@ func (l *List) Draw(screen tcell.Screen) {
|
||||
|
||||
// Secondary text.
|
||||
if l.showSecondaryText {
|
||||
_, printedWidth, _, end := printWithStyle(screen, item.SecondaryText, x, y, l.horizontalOffset, width, AlignLeft, l.secondaryTextStyle, true)
|
||||
_, end, printedWidth := printWithStyle(screen, item.SecondaryText, x, y, l.horizontalOffset, width, AlignLeft, l.secondaryTextStyle, true)
|
||||
if printedWidth > maxWidth {
|
||||
maxWidth = printedWidth
|
||||
}
|
||||
|
14
modal.go
14
modal.go
@ -1,8 +1,6 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
@ -101,7 +99,7 @@ func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) *
|
||||
}
|
||||
|
||||
// SetText sets the message text of the window. The text may contain line
|
||||
// breaks but color tag states will not transfer to following lines. Note that
|
||||
// breaks but style tag states will not transfer to following lines. Note that
|
||||
// words are wrapped, too, based on the final size of the window.
|
||||
func (m *Modal) SetText(text string) *Modal {
|
||||
m.text = text
|
||||
@ -172,15 +170,7 @@ func (m *Modal) Draw(screen tcell.Screen) {
|
||||
|
||||
// Reset the text and find out how wide it is.
|
||||
m.frame.Clear()
|
||||
var lines []string
|
||||
for _, line := range strings.Split(m.text, "\n") {
|
||||
if len(line) == 0 {
|
||||
lines = append(lines, "")
|
||||
continue
|
||||
}
|
||||
lines = append(lines, WordWrap(line, width)...)
|
||||
}
|
||||
//lines := WordWrap(m.text, width)
|
||||
lines := WordWrap(m.text, width)
|
||||
for _, line := range lines {
|
||||
m.frame.AddText(line, true, AlignCenter, m.textColor)
|
||||
}
|
||||
|
497
strings.go
Normal file
497
strings.go
Normal file
@ -0,0 +1,497 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// escapedTagPattern matches an escaped tag, e.g. "[red[]", at the beginning of
|
||||
// a string.
|
||||
var escapedTagPattern = regexp.MustCompile(`^\[[^\[\]]+\[+\]`)
|
||||
|
||||
// stepOptions is a bit field of options for [step]. A value of 0 results in
|
||||
// [step] having the same behavior as uniseg.Step, i.e. no tview-related parsing
|
||||
// is performed.
|
||||
type stepOptions int
|
||||
|
||||
// Bit fields for [stepOptions].
|
||||
const (
|
||||
stepOptionsNone stepOptions = 0
|
||||
stepOptionsStyle stepOptions = 1 << iota // Parse style tags.
|
||||
stepOptionsRegion // Parse region tags.
|
||||
)
|
||||
|
||||
// stepState represents the current state of the parser implemented in [step].
|
||||
type stepState struct {
|
||||
unisegState int // The state of the uniseg parser.
|
||||
boundaries int // Information about boundaries, as returned by uniseg.Step.
|
||||
style tcell.Style // The current style.
|
||||
region string // The current region.
|
||||
escapedTagState int // States for parsing escaped tags (defined in [step]).
|
||||
grossLength int // The length of the cluster, including any tags not returned.
|
||||
|
||||
// The styles for the initial call to [step].
|
||||
initialForeground tcell.Color
|
||||
initialBackground tcell.Color
|
||||
initialAttributes tcell.AttrMask
|
||||
}
|
||||
|
||||
// IsWordBoundary returns true if the boundary between the returned grapheme
|
||||
// cluster and the one following it is a word boundary.
|
||||
func (s *stepState) IsWordBoundary() bool {
|
||||
return s.boundaries&uniseg.MaskWord != 0
|
||||
}
|
||||
|
||||
// IsSentenceBoundary returns true if the boundary between the returned grapheme
|
||||
// cluster and the one following it is a sentence boundary.
|
||||
func (s *stepState) IsSentenceBoundary() bool {
|
||||
return s.boundaries&uniseg.MaskSentence != 0
|
||||
}
|
||||
|
||||
// LineBreak returns whether the string can be broken into the next line after
|
||||
// the returned grapheme cluster. If optional is true, the line break is
|
||||
// optional. If false, the line break is mandatory, e.g. after a newline
|
||||
// character.
|
||||
func (s *stepState) LineBreak() (lineBreak, optional bool) {
|
||||
switch s.boundaries & uniseg.MaskLine {
|
||||
case uniseg.LineCanBreak:
|
||||
return true, true
|
||||
case uniseg.LineMustBreak:
|
||||
return true, false
|
||||
}
|
||||
return false, false // uniseg.LineDontBreak.
|
||||
}
|
||||
|
||||
// Width returns the grapheme cluster's width in cells.
|
||||
func (s *stepState) Width() int {
|
||||
return s.boundaries >> uniseg.ShiftWidth
|
||||
}
|
||||
|
||||
// GrossLength returns the grapheme cluster's length in bytes, including any
|
||||
// tags that were parsed but not explicitly returned.
|
||||
func (s *stepState) GrossLength() int {
|
||||
return s.grossLength
|
||||
}
|
||||
|
||||
// Style returns the style for the grapheme cluster.
|
||||
func (s *stepState) Style() tcell.Style {
|
||||
return s.style
|
||||
}
|
||||
|
||||
// step uses uniseg.Step to iterate over the grapheme clusters of a string but
|
||||
// (optionally) also parses the string for style or region tags.
|
||||
//
|
||||
// This function can be called consecutively to extract all grapheme clusters
|
||||
// from str, without returning any contained (parsed) tags. The return values
|
||||
// are the first grapheme cluster, the remaining string, and the new state. Pass
|
||||
// the remaining string and the returned state to the next call. If the rest
|
||||
// string is empty, parsing is complete. Call the returned state's methods for
|
||||
// boundary and width information.
|
||||
//
|
||||
// The returned cluster may be empty if the given string consists of only
|
||||
// (parsed) tags. The boundary and width information will be meaningless in
|
||||
// this case but the style will describe the style at the end of the string.
|
||||
//
|
||||
// Pass nil for state on the first call. This will assume an initial style with
|
||||
// [Styles.PrimitiveBackgroundColor] as the background color and
|
||||
// [Styles.PrimaryTextColor] as the text color, no current region. If you want
|
||||
// to start with a different style or region, you can set the state accordingly
|
||||
// but you must then set [state.unisegState] to -1.
|
||||
//
|
||||
// You may call uniseg.HasTrailingLineBreakInString on the last non-empty
|
||||
// cluster to determine if the string ends with a hard line break.
|
||||
func step(str string, state *stepState, opts stepOptions) (cluster, rest string, newState *stepState) {
|
||||
// Set up initial state.
|
||||
if state == nil {
|
||||
state = &stepState{
|
||||
unisegState: -1,
|
||||
style: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor),
|
||||
}
|
||||
}
|
||||
if state.unisegState < 0 {
|
||||
state.initialForeground, state.initialBackground, state.initialAttributes = state.style.Decompose()
|
||||
}
|
||||
if len(str) == 0 {
|
||||
newState = state
|
||||
return
|
||||
}
|
||||
|
||||
// Get a grapheme cluster.
|
||||
preState := state.unisegState
|
||||
cluster, rest, state.boundaries, state.unisegState = uniseg.StepString(str, preState)
|
||||
state.grossLength = len(cluster)
|
||||
|
||||
// Parse tags.
|
||||
if opts != 0 {
|
||||
const (
|
||||
etNone int = iota
|
||||
etStart
|
||||
etChar
|
||||
etClosing
|
||||
)
|
||||
|
||||
// Finite state machine for escaped tags.
|
||||
switch state.escapedTagState {
|
||||
case etStart:
|
||||
if cluster[0] == '[' || cluster[0] == ']' { // Invalid escaped tag.
|
||||
state.escapedTagState = etNone
|
||||
} else { // Other characters are allowed.
|
||||
state.escapedTagState = etChar
|
||||
}
|
||||
case etChar:
|
||||
if cluster[0] == ']' { // In theory, this should not happen.
|
||||
state.escapedTagState = etNone
|
||||
} else if cluster[0] == '[' { // Starting closing sequence.
|
||||
// Swallow the first one.
|
||||
cluster, rest, state.boundaries, state.unisegState = uniseg.StepString(rest, preState)
|
||||
state.grossLength = len(cluster)
|
||||
if cluster[0] == ']' {
|
||||
state.escapedTagState = etNone
|
||||
} else {
|
||||
state.escapedTagState = etClosing
|
||||
}
|
||||
} // More characters. Remain in etChar.
|
||||
case etClosing:
|
||||
if cluster[0] != '[' {
|
||||
state.escapedTagState = etNone
|
||||
}
|
||||
}
|
||||
|
||||
// Regular tags.
|
||||
if state.escapedTagState == etNone {
|
||||
if cluster[0] == '[' {
|
||||
// We've already opened a tag. Parse it.
|
||||
length, style, region := parseTag(str, state)
|
||||
if length > 0 {
|
||||
state.style = style
|
||||
state.region = region
|
||||
cluster, rest, state.boundaries, state.unisegState = uniseg.StepString(str[length:], preState)
|
||||
state.grossLength = len(cluster) + length
|
||||
}
|
||||
// Is this an escaped tag?
|
||||
if escapedTagPattern.MatchString(str[length:]) {
|
||||
state.escapedTagState = etStart
|
||||
}
|
||||
}
|
||||
if len(rest) > 0 && rest[0] == '[' {
|
||||
// A tag might follow the cluster. If so, we need to fix the state
|
||||
// for the boundaries to be correct.
|
||||
if length, _, _ := parseTag(rest, state); length > 0 {
|
||||
if len(rest) > length {
|
||||
_, l := utf8.DecodeRuneInString(rest[length:])
|
||||
cluster += rest[length : length+l]
|
||||
}
|
||||
cluster, _, state.boundaries, state.unisegState = uniseg.StepString(cluster, preState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newState = state
|
||||
return
|
||||
}
|
||||
|
||||
// parseTag parses str for consecutive style and/or region tags, assuming that
|
||||
// str starts with the opening bracket for the first tag. It returns the string
|
||||
// length of all valid tags (0 if the first tag is not valid) and the updated
|
||||
// style and region for valid tags (based on the provided state).
|
||||
func parseTag(str string, state *stepState) (length int, style tcell.Style, region string) {
|
||||
// Automata states for parsing tags.
|
||||
const (
|
||||
tagStateNone = iota
|
||||
tagStateDoneTag
|
||||
tagStateStart
|
||||
tagStateRegionStart
|
||||
tagStateEndForeground
|
||||
tagStateStartBackground
|
||||
tagStateNumericForeground
|
||||
tagStateNameForeground
|
||||
tagStateEndBackground
|
||||
tagStateStartAttributes
|
||||
tagStateNumericBackground
|
||||
tagStateNameBackground
|
||||
tagStateAttributes
|
||||
tagStateRegionEnd
|
||||
tagStateRegionName
|
||||
tagStateEndAttributes
|
||||
tagStateStartURL
|
||||
tagStateEndURL
|
||||
tagStateURL
|
||||
)
|
||||
|
||||
// Helper function which checks if the given byte is one of a list of
|
||||
// characters, including letters and digits.
|
||||
isOneOf := func(b byte, chars string) bool {
|
||||
if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' {
|
||||
return true
|
||||
}
|
||||
return strings.IndexByte(chars, b) >= 0
|
||||
}
|
||||
|
||||
// Attribute map.
|
||||
attrs := map[byte]tcell.AttrMask{
|
||||
'B': tcell.AttrBold,
|
||||
'U': tcell.AttrUnderline,
|
||||
'I': tcell.AttrItalic,
|
||||
'L': tcell.AttrBlink,
|
||||
'D': tcell.AttrDim,
|
||||
'S': tcell.AttrStrikeThrough,
|
||||
'R': tcell.AttrReverse,
|
||||
}
|
||||
|
||||
var (
|
||||
tagState, tagLength int
|
||||
tempStr strings.Builder
|
||||
)
|
||||
tStyle := state.style
|
||||
tRegion := state.region
|
||||
|
||||
// Process state transitions.
|
||||
for len(str) > 0 {
|
||||
ch := str[0]
|
||||
str = str[1:]
|
||||
tagLength++
|
||||
|
||||
// Transition.
|
||||
switch tagState {
|
||||
case tagStateNone:
|
||||
if ch == '[' { // Start of a tag.
|
||||
tagState = tagStateStart
|
||||
} else { // Not a tag. We're done.
|
||||
return
|
||||
}
|
||||
case tagStateStart:
|
||||
if ch == '"' { // Start of a region tag.
|
||||
tempStr.Reset()
|
||||
tagState = tagStateRegionStart
|
||||
} else if !isOneOf(ch, "#:-") { // Invalid style tag.
|
||||
return
|
||||
} else if ch == '-' { // Reset foreground color.
|
||||
tStyle = tStyle.Foreground(state.initialForeground)
|
||||
tagState = tagStateEndForeground
|
||||
} else if ch == ':' { // No foreground color.
|
||||
tagState = tagStateStartBackground
|
||||
} else {
|
||||
tempStr.Reset()
|
||||
tempStr.WriteByte(ch)
|
||||
if ch == '#' { // Numeric foreground color.
|
||||
tagState = tagStateNumericForeground
|
||||
} else { // Letters or numbers.
|
||||
tagState = tagStateNameForeground
|
||||
}
|
||||
}
|
||||
case tagStateEndForeground:
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == ':' {
|
||||
tagState = tagStateStartBackground
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateNumericForeground:
|
||||
if ch == ']' || ch == ':' {
|
||||
if tempStr.Len() != 7 { // Must be #rrggbb.
|
||||
return
|
||||
}
|
||||
tStyle = tStyle.Foreground(tcell.GetColor(tempStr.String()))
|
||||
}
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == ':' { // Start of background color.
|
||||
tagState = tagStateStartBackground
|
||||
} else if strings.IndexByte("0123456789abcdefABCDEF", ch) >= 0 { // Hex digit.
|
||||
tempStr.WriteByte(ch)
|
||||
tagState = tagStateNumericForeground
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateNameForeground:
|
||||
if ch == ']' || ch == ':' {
|
||||
name := tempStr.String()
|
||||
if name[0] >= '0' && name[0] <= '9' { // Must not start with a digit.
|
||||
return
|
||||
}
|
||||
tStyle = tStyle.Foreground(tcell.ColorNames[name])
|
||||
}
|
||||
if !isOneOf(ch, "]:") { // Invalid tag.
|
||||
return
|
||||
} else if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == ':' { // Start of background color.
|
||||
tagState = tagStateStartBackground
|
||||
} else { // Letters or numbers.
|
||||
tempStr.WriteByte(ch)
|
||||
}
|
||||
case tagStateStartBackground:
|
||||
if !isOneOf(ch, "#:-]") { // Invalid style tag.
|
||||
return
|
||||
} else if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == '-' { // Reset background color.
|
||||
tStyle = tStyle.Background(state.initialBackground)
|
||||
tagState = tagStateEndBackground
|
||||
} else if ch == ':' { // No background color.
|
||||
tagState = tagStateStartAttributes
|
||||
} else {
|
||||
tempStr.Reset()
|
||||
tempStr.WriteByte(ch)
|
||||
if ch == '#' { // Numeric background color.
|
||||
tagState = tagStateNumericBackground
|
||||
} else { // Letters or numbers.
|
||||
tagState = tagStateNameBackground
|
||||
}
|
||||
}
|
||||
case tagStateEndBackground:
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == ':' { // Start of attributes.
|
||||
tagState = tagStateStartAttributes
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateNumericBackground:
|
||||
if ch == ']' || ch == ':' {
|
||||
if tempStr.Len() != 7 { // Must be #rrggbb.
|
||||
return
|
||||
}
|
||||
tStyle = tStyle.Background(tcell.GetColor(tempStr.String()))
|
||||
}
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == ':' { // Start of attributes.
|
||||
tagState = tagStateStartAttributes
|
||||
} else if strings.IndexByte("0123456789abcdefABCDEF", ch) >= 0 { // Hex digit.
|
||||
tempStr.WriteByte(ch)
|
||||
tagState = tagStateNumericBackground
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateNameBackground:
|
||||
if ch == ']' || ch == ':' {
|
||||
name := tempStr.String()
|
||||
if name[0] >= '0' && name[0] <= '9' { // Must not start with a digit.
|
||||
return
|
||||
}
|
||||
tStyle = tStyle.Background(tcell.ColorNames[name])
|
||||
}
|
||||
if !isOneOf(ch, "]:") { // Invalid tag.
|
||||
return
|
||||
} else if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == ':' { // Start of background color.
|
||||
tagState = tagStateStartAttributes
|
||||
} else { // Letters or numbers.
|
||||
tempStr.WriteByte(ch)
|
||||
}
|
||||
case tagStateStartAttributes:
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == '-' { // Reset attributes.
|
||||
tStyle = tStyle.Attributes(state.initialAttributes)
|
||||
tagState = tagStateEndAttributes
|
||||
} else if ch == ':' { // Start of URL.
|
||||
tagState = tagStateStartURL
|
||||
} else if strings.IndexByte("buildsrBUILDSR", ch) >= 0 { // Attribute tag.
|
||||
tempStr.Reset()
|
||||
tempStr.WriteByte(ch)
|
||||
tagState = tagStateAttributes
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateAttributes:
|
||||
if ch == ']' || ch == ':' {
|
||||
flags := tempStr.String()
|
||||
_, _, a := tStyle.Decompose()
|
||||
for index := 0; index < len(flags); index++ {
|
||||
ch := flags[index]
|
||||
if ch >= 'a' && ch <= 'z' {
|
||||
a |= attrs[ch-('a'-'A')]
|
||||
} else {
|
||||
a &^= attrs[ch]
|
||||
}
|
||||
}
|
||||
tStyle = tStyle.Attributes(a)
|
||||
}
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == ':' { // Start of URL.
|
||||
tagState = tagStateStartURL
|
||||
} else if strings.IndexByte("buildsrBUILDSR", ch) >= 0 { // Attribute tag.
|
||||
tempStr.WriteByte(ch)
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateEndAttributes:
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == ':' { // Start of URL.
|
||||
tagState = tagStateStartURL
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateStartURL:
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else if ch == '-' { // Reset URL.
|
||||
tStyle = tStyle.Url("").UrlId("")
|
||||
tagState = tagStateEndURL
|
||||
} else { // URL character.
|
||||
tempStr.Reset()
|
||||
tempStr.WriteByte(ch)
|
||||
tStyle = tStyle.UrlId(strconv.Itoa(int(rand.Uint32()))) // Generate a unique ID for this URL.
|
||||
tagState = tagStateURL
|
||||
}
|
||||
case tagStateEndURL:
|
||||
if ch == ']' { // End of tag.
|
||||
tagState = tagStateDoneTag
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateURL:
|
||||
if ch == ']' { // End of tag.
|
||||
tStyle = tStyle.Url(tempStr.String())
|
||||
tagState = tagStateDoneTag
|
||||
} else { // URL character.
|
||||
tempStr.WriteByte(ch)
|
||||
}
|
||||
case tagStateRegionStart:
|
||||
if ch == '"' { // End of region tag.
|
||||
tagState = tagStateRegionEnd
|
||||
} else if isOneOf(ch, "_,;: -.") { // Region name.
|
||||
tempStr.WriteByte(ch)
|
||||
tagState = tagStateRegionName
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateRegionEnd:
|
||||
if ch == ']' { // End of tag.
|
||||
tRegion = tempStr.String()
|
||||
tagState = tagStateDoneTag
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
case tagStateRegionName:
|
||||
if ch == '"' { // End of region tag.
|
||||
tagState = tagStateRegionEnd
|
||||
} else if isOneOf(ch, "_,;: -.") { // Region name.
|
||||
tempStr.WriteByte(ch)
|
||||
} else { // Invalid tag.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The last transition led to a tag end. Make the tag permanent.
|
||||
if tagState == tagStateDoneTag {
|
||||
length, style, region = tagLength, tStyle, tRegion
|
||||
tagState = tagStateNone // Reset state.
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
5
table.go
5
table.go
@ -977,7 +977,7 @@ func (t *Table) Draw(screen tcell.Screen) {
|
||||
}
|
||||
for _, row := range evaluationRows {
|
||||
if cell := t.content.GetCell(row, column); cell != nil {
|
||||
_, _, _, _, _, _, cellWidth := decomposeString(cell.Text, true, false)
|
||||
cellWidth := TaggedStringWidth(cell.Text)
|
||||
if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth {
|
||||
cellWidth = cell.MaxWidth
|
||||
}
|
||||
@ -1154,7 +1154,8 @@ func (t *Table) Draw(screen tcell.Screen) {
|
||||
finalWidth = width - columnX
|
||||
}
|
||||
cell.x, cell.y, cell.width = x+columnX, y+rowY, finalWidth
|
||||
_, printed, _, _ := printWithStyle(screen, cell.Text, x+columnX, y+rowY, 0, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color).Attributes(cell.Attributes), true)
|
||||
start, end, _ := printWithStyle(screen, cell.Text, x+columnX, y+rowY, 0, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color).Attributes(cell.Attributes), true)
|
||||
printed := end - start
|
||||
if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 {
|
||||
_, _, style, _ := screen.GetContent(x+columnX+finalWidth-1, y+rowY)
|
||||
printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth-1, y+rowY, 0, 1, AlignLeft, style, false)
|
||||
|
53
textarea.go
53
textarea.go
@ -1074,7 +1074,7 @@ func (t *TextArea) Draw(screen tcell.Screen) {
|
||||
x += labelWidth
|
||||
width -= labelWidth
|
||||
} else {
|
||||
_, drawnWidth, _, _ := printWithStyle(screen, t.label, x, y, 0, width, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault)
|
||||
_, _, drawnWidth := printWithStyle(screen, t.label, x, y, 0, width, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault)
|
||||
x += drawnWidth
|
||||
width -= drawnWidth
|
||||
}
|
||||
@ -1201,49 +1201,14 @@ func (t *TextArea) Draw(screen tcell.Screen) {
|
||||
// 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
|
||||
})
|
||||
// We use a TextView to draw the placeholder. It will take care of word
|
||||
// wrapping etc.
|
||||
textView := NewTextView().
|
||||
SetText(t.placeholder).
|
||||
SetTextStyle(t.placeholderStyle)
|
||||
textView.SetBackgroundColor(t.backgroundColor).
|
||||
SetRect(x, y, width, height)
|
||||
textView.Draw(screen)
|
||||
}
|
||||
|
||||
// reset resets many of the local variables of the text area because they cannot
|
||||
|
14
textview.go
14
textview.go
@ -413,7 +413,7 @@ func (t *TextView) SetText(text string) *TextView {
|
||||
}
|
||||
|
||||
// GetText returns the current text of this text view. If "stripAllTags" is set
|
||||
// to true, any region/color tags are stripped from the text.
|
||||
// to true, any region/style tags are stripped from the text.
|
||||
func (t *TextView) GetText(stripAllTags bool) string {
|
||||
// Get the buffer.
|
||||
buffer := t.buffer
|
||||
@ -686,7 +686,7 @@ func (t *TextView) ScrollToHighlight() *TextView {
|
||||
}
|
||||
|
||||
// GetRegionText returns the text of the region with the given ID. If dynamic
|
||||
// colors are enabled, color tags are stripped from the text. Newlines are
|
||||
// colors are enabled, style tags are stripped from the text. Newlines are
|
||||
// always returned as '\n' runes.
|
||||
//
|
||||
// If the region does not exist or if regions are turned off, an empty string
|
||||
@ -702,7 +702,7 @@ func (t *TextView) GetRegionText(regionID string) string {
|
||||
)
|
||||
|
||||
for _, str := range t.buffer {
|
||||
// Find all color tags in this line.
|
||||
// Find all style tags in this line.
|
||||
var colorTagIndices [][]int
|
||||
if t.dynamicColors {
|
||||
colorTagIndices = colorPattern.FindAllStringIndex(str, -1)
|
||||
@ -721,7 +721,7 @@ func (t *TextView) GetRegionText(regionID string) string {
|
||||
// Analyze this line.
|
||||
var currentTag, currentRegion int
|
||||
for pos, ch := range str {
|
||||
// Skip any color tags.
|
||||
// Skip any style tags.
|
||||
if currentTag < len(colorTagIndices) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
|
||||
tag := currentTag
|
||||
if pos == colorTagIndices[tag][1]-1 {
|
||||
@ -972,7 +972,7 @@ func (t *TextView) reindexBuffer(width int) {
|
||||
// Which tag comes next?
|
||||
nextTag := make([][3]int, 0, 3)
|
||||
if colorPos < len(colorTagIndices) {
|
||||
nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag.
|
||||
nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = style tag.
|
||||
}
|
||||
if regionPos < len(regionIndices) {
|
||||
nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag.
|
||||
@ -1007,7 +1007,7 @@ func (t *TextView) reindexBuffer(width int) {
|
||||
// Process the tag.
|
||||
switch nextTag[tagIndex][2] {
|
||||
case 0:
|
||||
// Process color tags.
|
||||
// Process style tags.
|
||||
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
|
||||
colorPos++
|
||||
case 1:
|
||||
@ -1136,7 +1136,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
|
||||
x += labelWidth
|
||||
width -= labelWidth
|
||||
} else {
|
||||
_, drawnWidth, _, _ := printWithStyle(screen, t.label, x, y, 0, width, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault)
|
||||
_, _, drawnWidth := printWithStyle(screen, t.label, x, y, 0, width, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault)
|
||||
x += drawnWidth
|
||||
width -= drawnWidth
|
||||
}
|
||||
|
469
util.go
469
util.go
@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/uniseg"
|
||||
@ -168,10 +169,10 @@ func overlayStyle(style tcell.Style, fgColor, bgColor, attributes string) tcell.
|
||||
|
||||
// decomposeString 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
|
||||
// returns the indices of the style tags (as returned by
|
||||
// re.FindAllStringIndex()), the style tags themselves (as returned by
|
||||
// re.FindAllStringSubmatch()), the indices of region tags and the region tags
|
||||
// themselves, the indices of an escaped tags (only if at least color tags or
|
||||
// themselves, the indices of an escaped tags (only if at least style 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) {
|
||||
@ -236,208 +237,120 @@ func decomposeString(text string, findColors, findRegions bool) (colorIndices []
|
||||
// not exceeding that box. "align" is one of AlignLeft, AlignCenter, or
|
||||
// AlignRight. The screen's background color will not be changed.
|
||||
//
|
||||
// You can change the colors and text styles mid-text by inserting a color tag.
|
||||
// You can change the colors and text styles mid-text by inserting a style tag.
|
||||
// See the package description for details.
|
||||
//
|
||||
// Returns the number of actual bytes of the text printed (including color tags)
|
||||
// Returns the number of actual bytes of the text printed (including style 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) {
|
||||
bytes, width, _, _ := printWithStyle(screen, text, x, y, 0, maxWidth, align, tcell.StyleDefault.Foreground(color), true)
|
||||
return bytes, width
|
||||
start, end, width := printWithStyle(screen, text, x, y, 0, maxWidth, align, tcell.StyleDefault.Foreground(color), true)
|
||||
return end - start, width
|
||||
}
|
||||
|
||||
// printWithStyle works like Print() but it takes a style instead of just a
|
||||
// foreground color. The skipWidth parameter specifies the number of cells
|
||||
// skipped at the beginning of the text. It also returns the start and end index
|
||||
// (exclusively) of the text actually printed. If maintainBackground is "true",
|
||||
// The existing screen background is not changed (i.e. the style's background
|
||||
// color is ignored).
|
||||
func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, align int, style tcell.Style, maintainBackground bool) (int, int, int, int) {
|
||||
// skipped at the beginning of the text. It returns the start index, end index
|
||||
// (exclusively), and screen width of the text actually printed. If
|
||||
// maintainBackground is "true", the existing screen background is not changed
|
||||
// (i.e. the style's background color is ignored).
|
||||
func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, align int, style tcell.Style, maintainBackground bool) (start, end, printedWidth int) {
|
||||
totalWidth, totalHeight := screen.Size()
|
||||
if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight {
|
||||
return 0, 0, 0, 0
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
// Decompose the text.
|
||||
colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false)
|
||||
|
||||
// We want to reduce all alignments to AlignLeft.
|
||||
if align == AlignRight {
|
||||
if strippedWidth-skipWidth <= maxWidth {
|
||||
// There's enough space for the entire text.
|
||||
return printWithStyle(screen, text, x+maxWidth-strippedWidth+skipWidth, y, skipWidth, maxWidth, AlignLeft, style, maintainBackground)
|
||||
}
|
||||
// Trim characters off the beginning.
|
||||
var (
|
||||
bytes, width, colorPos, escapePos, tagOffset, from, to int
|
||||
foregroundColor, backgroundColor, attributes string
|
||||
)
|
||||
originalStyle := style
|
||||
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries 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])
|
||||
style = overlayStyle(originalStyle, foregroundColor, backgroundColor, attributes)
|
||||
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
||||
colorPos++
|
||||
}
|
||||
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
|
||||
tagOffset++
|
||||
escapePos++
|
||||
}
|
||||
if strippedWidth-screenPos <= maxWidth {
|
||||
// We chopped off enough.
|
||||
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:]
|
||||
}
|
||||
// Print and return.
|
||||
bytes, width, from, to = printWithStyle(screen, text[textPos+tagOffset:], x, y, 0, maxWidth, AlignLeft, style, maintainBackground)
|
||||
from += textPos + tagOffset
|
||||
to += textPos + tagOffset
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return bytes, width, from, to
|
||||
} else if align == AlignCenter {
|
||||
if strippedWidth-skipWidth == maxWidth {
|
||||
// Use the exact space.
|
||||
return printWithStyle(screen, text, x, y, skipWidth, maxWidth, AlignLeft, style, maintainBackground)
|
||||
} else if strippedWidth-skipWidth < maxWidth {
|
||||
// We have more space than we need.
|
||||
half := (maxWidth - strippedWidth + skipWidth) / 2
|
||||
return printWithStyle(screen, text, x+half, y, skipWidth, maxWidth-half, AlignLeft, style, maintainBackground)
|
||||
} else {
|
||||
// Chop off runes until we have a perfect fit.
|
||||
var choppedLeft, choppedRight, leftIndex, rightIndex int
|
||||
rightIndex = len(strippedText)
|
||||
for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth {
|
||||
if skipWidth > 0 || choppedLeft < choppedRight {
|
||||
// Iterate on the left by one character.
|
||||
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||
if skipWidth > 0 {
|
||||
skipWidth -= screenWidth
|
||||
strippedWidth -= screenWidth
|
||||
} else {
|
||||
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 {
|
||||
choppedRight += screenWidth
|
||||
rightIndex -= textWidth
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add tag offsets and determine start style.
|
||||
var (
|
||||
colorPos, escapePos, tagOffset int
|
||||
foregroundColor, backgroundColor, attributes string
|
||||
)
|
||||
originalStyle := style
|
||||
for index := range strippedText {
|
||||
// We only need the offset of the left index.
|
||||
if index > leftIndex {
|
||||
// We're done.
|
||||
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:]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Update color/escape tag offset.
|
||||
if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] {
|
||||
if index <= leftIndex {
|
||||
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
||||
style = overlayStyle(originalStyle, foregroundColor, backgroundColor, attributes)
|
||||
}
|
||||
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
||||
colorPos++
|
||||
}
|
||||
if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] {
|
||||
tagOffset++
|
||||
escapePos++
|
||||
}
|
||||
}
|
||||
bytes, width, from, to := printWithStyle(screen, text[leftIndex+tagOffset:], x, y, 0, maxWidth, AlignLeft, style, maintainBackground)
|
||||
from += leftIndex + tagOffset
|
||||
to += leftIndex + tagOffset
|
||||
return bytes, width, from, to
|
||||
}
|
||||
// If we don't overwrite the background, we use the default color.
|
||||
if maintainBackground {
|
||||
style = style.Background(tcell.ColorDefault)
|
||||
}
|
||||
|
||||
// Draw text.
|
||||
// Skip beginning and measure width.
|
||||
var (
|
||||
drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int
|
||||
foregroundColor, backgroundColor, attributes string
|
||||
state *stepState
|
||||
textWidth int
|
||||
)
|
||||
iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth, boundaries int) bool {
|
||||
// Skip character if necessary.
|
||||
str := text
|
||||
for len(str) > 0 {
|
||||
_, str, state = step(str, state, stepOptionsStyle)
|
||||
if skipWidth > 0 {
|
||||
skipWidth -= screenWidth
|
||||
from = textPos + length
|
||||
to = from
|
||||
return false
|
||||
skipWidth -= state.Width()
|
||||
if skipWidth <= 0 {
|
||||
text = str
|
||||
style = state.Style()
|
||||
}
|
||||
start += state.GrossLength()
|
||||
} else {
|
||||
textWidth += state.Width()
|
||||
}
|
||||
}
|
||||
|
||||
// Only continue if there is still space.
|
||||
if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth {
|
||||
return true
|
||||
// Reduce all alignments to AlignLeft.
|
||||
if align == AlignRight {
|
||||
// Chop off characters on the left until it fits.
|
||||
state = nil
|
||||
for len(text) > 0 && textWidth > maxWidth {
|
||||
_, text, state = step(text, state, stepOptionsStyle)
|
||||
textWidth -= state.Width()
|
||||
start += state.GrossLength()
|
||||
style = state.Style()
|
||||
}
|
||||
|
||||
// Handle color tags.
|
||||
for colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
|
||||
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
||||
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
||||
colorPos++
|
||||
x, maxWidth = x+maxWidth-textWidth, textWidth
|
||||
} else if align == AlignCenter {
|
||||
// Chop off characters on the left until it fits.
|
||||
state = nil
|
||||
subtracted := (textWidth - maxWidth) / 2
|
||||
for len(text) > 0 && subtracted > 0 {
|
||||
_, text, state = step(text, state, stepOptionsStyle)
|
||||
subtracted -= state.Width()
|
||||
textWidth -= state.Width()
|
||||
start += state.GrossLength()
|
||||
style = state.Style()
|
||||
}
|
||||
if textWidth < maxWidth {
|
||||
x, maxWidth = x+maxWidth/2-textWidth/2, textWidth
|
||||
}
|
||||
}
|
||||
|
||||
// Handle escape tags.
|
||||
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
|
||||
if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
|
||||
tagOffset++
|
||||
escapePos++
|
||||
// Draw left-aligned text.
|
||||
end = start
|
||||
rightBorder := x + maxWidth
|
||||
state = &stepState{
|
||||
unisegState: -1,
|
||||
style: style,
|
||||
}
|
||||
for len(text) > 0 && x < rightBorder && x < totalWidth {
|
||||
var c string
|
||||
c, text, state = step(text, state, stepOptionsStyle)
|
||||
if c == "" {
|
||||
break // We don't care about the style at the end.
|
||||
}
|
||||
runes := []rune(c)
|
||||
width := state.Width()
|
||||
|
||||
finalStyle := state.Style()
|
||||
if maintainBackground {
|
||||
_, backgroundColor, _ := finalStyle.Decompose()
|
||||
if backgroundColor == tcell.ColorDefault {
|
||||
_, _, existingStyle, _ := screen.GetContent(x, y)
|
||||
_, background, _ := existingStyle.Decompose()
|
||||
finalStyle = finalStyle.Background(background)
|
||||
}
|
||||
}
|
||||
|
||||
// Memorize positions.
|
||||
to = textPos + length
|
||||
|
||||
// Print the rune sequence.
|
||||
finalX := x + drawnWidth
|
||||
finalStyle := style
|
||||
if maintainBackground {
|
||||
_, _, existingStyle, _ := screen.GetContent(finalX, y)
|
||||
_, background, _ := existingStyle.Decompose()
|
||||
finalStyle = finalStyle.Background(background)
|
||||
}
|
||||
finalStyle = overlayStyle(finalStyle, foregroundColor, backgroundColor, attributes)
|
||||
for offset := screenWidth - 1; offset >= 0; offset-- {
|
||||
for offset := width - 1; offset >= 0; offset-- {
|
||||
// To avoid undesired effects, we populate all cells.
|
||||
if offset == 0 {
|
||||
screen.SetContent(finalX+offset, y, main, comb, finalStyle)
|
||||
screen.SetContent(x+offset, y, runes[0], runes[1:], finalStyle)
|
||||
} else {
|
||||
screen.SetContent(finalX+offset, y, ' ', nil, finalStyle)
|
||||
screen.SetContent(x+offset, y, ' ', nil, finalStyle)
|
||||
}
|
||||
}
|
||||
|
||||
// Advance.
|
||||
drawn += length
|
||||
drawnWidth += screenWidth
|
||||
x += width
|
||||
end += state.GrossLength()
|
||||
printedWidth += width
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return drawn + tagOffset + len(escapeIndices), drawnWidth, from, to
|
||||
return
|
||||
}
|
||||
|
||||
// PrintSimple prints white text to the screen at the given position.
|
||||
@ -446,112 +359,75 @@ func PrintSimple(screen tcell.Screen, text string, x, y int) {
|
||||
}
|
||||
|
||||
// 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
|
||||
// screen. The text may contain style tags which are not counted.
|
||||
func TaggedStringWidth(text string) (width int) {
|
||||
var state *stepState
|
||||
for len(text) > 0 {
|
||||
_, text, state = step(text, state, stepOptionsStyle)
|
||||
width += state.Width()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WordWrap splits a text such that each resulting line does not exceed the
|
||||
// given screen width. Possible split points are after any punctuation or
|
||||
// whitespace. Whitespace after split points will be dropped.
|
||||
// given screen width. Split points are determined using the algorithm described
|
||||
// in [Unicode Standard Annex #14] .
|
||||
//
|
||||
// This function considers color tags to have no width.
|
||||
// This function considers style tags to have no width.
|
||||
//
|
||||
// Text is always split at newline characters ('\n').
|
||||
// [Unicode Standard Annex #14]: https://www.unicode.org/reports/tr14/
|
||||
func WordWrap(text string, width int) (lines []string) {
|
||||
colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeString(text, true, false)
|
||||
if width <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Find candidate breakpoints.
|
||||
breakpoints := boundaryPattern.FindAllStringSubmatchIndex(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.
|
||||
|
||||
// Process stripped text one character at a time.
|
||||
var (
|
||||
colorPos, escapePos, breakpointPos, tagOffset int
|
||||
lastBreakpoint, lastContinuation, currentLineStart int
|
||||
lineWidth, overflow int
|
||||
forceBreak bool
|
||||
state *stepState
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth int
|
||||
)
|
||||
unescape := func(substr string, startIndex int) string {
|
||||
// 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 substr
|
||||
}
|
||||
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
|
||||
// Handle tags.
|
||||
for {
|
||||
if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
|
||||
// Colour tags.
|
||||
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
|
||||
colorPos++
|
||||
} else if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
|
||||
// Escape tags.
|
||||
tagOffset++
|
||||
escapePos++
|
||||
str := text
|
||||
for len(str) > 0 {
|
||||
// Parse the next character.
|
||||
var c string
|
||||
c, str, state = step(str, state, stepOptionsStyle)
|
||||
cWidth := state.Width()
|
||||
|
||||
// Would it exceed the line width?
|
||||
if lineWidth+cWidth > width {
|
||||
if lastOptionWidth == 0 {
|
||||
// No split point so far. Just split at the current position.
|
||||
lines = append(lines, text[:lineLength])
|
||||
text = text[lineLength:]
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth = 0, 0, 0, 0
|
||||
} else {
|
||||
break
|
||||
// Split at the last split point.
|
||||
lines = append(lines, text[:lastOption])
|
||||
text = text[lastOption:]
|
||||
lineWidth -= lastOptionWidth
|
||||
lineLength -= lastOption
|
||||
lastOption, lastOptionWidth = 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
// Is this a breakpoint?
|
||||
if breakpointPos < len(breakpoints) && textPos+tagOffset == breakpoints[breakpointPos][0] {
|
||||
// Yes, it is. Set up breakpoint infos depending on its type.
|
||||
lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
|
||||
lastContinuation = breakpoints[breakpointPos][1] + tagOffset
|
||||
overflow = 0
|
||||
forceBreak = main == '\n'
|
||||
if breakpoints[breakpointPos][6] < 0 && !forceBreak {
|
||||
lastBreakpoint++ // Don't skip punctuation.
|
||||
// Move ahead.
|
||||
lineWidth += cWidth
|
||||
lineLength += state.GrossLength()
|
||||
|
||||
// Check for split points.
|
||||
if lineBreak, optional := state.LineBreak(); lineBreak {
|
||||
if optional {
|
||||
// Remember this split point.
|
||||
lastOption = lineLength
|
||||
lastOptionWidth = lineWidth
|
||||
} else if str != "" || c != "" && uniseg.HasTrailingLineBreakInString(c) {
|
||||
// We must split here.
|
||||
lines = append(lines, strings.TrimRight(text[:lineLength], "\n\r"))
|
||||
text = text[lineLength:]
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth = 0, 0, 0, 0
|
||||
}
|
||||
breakpointPos++
|
||||
}
|
||||
|
||||
// Check if a break is warranted.
|
||||
if forceBreak || lineWidth > 0 && lineWidth+screenWidth > width {
|
||||
breakpoint := lastBreakpoint
|
||||
continuation := lastContinuation
|
||||
if forceBreak {
|
||||
breakpoint = textPos + tagOffset
|
||||
continuation = textPos + tagOffset + 1
|
||||
lastBreakpoint = 0
|
||||
overflow = 0
|
||||
} else if lastBreakpoint <= currentLineStart {
|
||||
breakpoint = textPos + tagOffset
|
||||
continuation = textPos + tagOffset
|
||||
overflow = 0
|
||||
}
|
||||
lines = append(lines, unescape(text[currentLineStart:breakpoint], currentLineStart))
|
||||
currentLineStart, lineWidth, forceBreak = continuation, overflow, false
|
||||
}
|
||||
|
||||
// Remember the characters since the last breakpoint.
|
||||
if lastBreakpoint > 0 && lastContinuation <= textPos+tagOffset {
|
||||
overflow += screenWidth
|
||||
}
|
||||
|
||||
// Advance.
|
||||
lineWidth += screenWidth
|
||||
|
||||
// But if we're still inside a breakpoint, skip next character (whitespace).
|
||||
if textPos+tagOffset < currentLineStart {
|
||||
lineWidth -= screenWidth
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
// Flush the rest.
|
||||
if currentLineStart < len(text) {
|
||||
lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
|
||||
}
|
||||
lines = append(lines, text)
|
||||
|
||||
return
|
||||
}
|
||||
@ -602,60 +478,17 @@ func iterateString(text string, callback func(main rune, comb []rune, textPos, t
|
||||
return false
|
||||
}
|
||||
|
||||
// iterateStringReverse iterates through the given string in reverse, starting
|
||||
// from the end of the string, one printed character at a time. For each such
|
||||
// character, the callback function is called with the 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) 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 returns true. This
|
||||
// function returns true if the iteration was stopped before the last character.
|
||||
func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
|
||||
type cluster struct {
|
||||
main rune
|
||||
comb []rune
|
||||
textPos, textWidth, screenPos, screenWidth int
|
||||
}
|
||||
|
||||
// Create the grapheme clusters.
|
||||
var clusters []cluster
|
||||
iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth, boundaries int) bool {
|
||||
clusters = append(clusters, cluster{
|
||||
main: main,
|
||||
comb: comb,
|
||||
textPos: textPos,
|
||||
textWidth: textWidth,
|
||||
screenPos: screenPos,
|
||||
screenWidth: screenWidth,
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
// Iterate in reverse.
|
||||
for index := len(clusters) - 1; index >= 0; index-- {
|
||||
if callback(
|
||||
clusters[index].main,
|
||||
clusters[index].comb,
|
||||
clusters[index].textPos,
|
||||
clusters[index].textWidth,
|
||||
clusters[index].screenPos,
|
||||
clusters[index].screenWidth,
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// stripTags strips colour tags from the given string. (Region tags are not
|
||||
// stripTags strips style tags from the given string. (Region tags are not
|
||||
// stripped.)
|
||||
func stripTags(text string) string {
|
||||
stripped := colorPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||||
if len(match) > 2 {
|
||||
return ""
|
||||
}
|
||||
return match
|
||||
})
|
||||
return escapePattern.ReplaceAllString(stripped, `[$1$2]`)
|
||||
var (
|
||||
str strings.Builder
|
||||
state *stepState
|
||||
)
|
||||
for len(text) > 0 {
|
||||
var c string
|
||||
c, text, state = step(text, state, stepOptionsStyle)
|
||||
str.WriteString(c)
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user