InputField: replace buffer with byte buffer and update appearance
This commit is contained in:
parent
1b50aff0c6
commit
28e9af70eb
@ -34,7 +34,7 @@ func NewFileSelectorDialog(screen *tcell.Screen, title string, mustExist bool, t
|
|||||||
title: title,
|
title: title,
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.inputField = NewInputField(screen, "", theme)
|
dialog.inputField = NewInputField(screen, []byte{}, theme.GetOrDefault("Window")) // Use window's theme for InputField
|
||||||
dialog.confirmButton = NewButton("Confirm", theme, dialog.onConfirm)
|
dialog.confirmButton = NewButton("Confirm", theme, dialog.onConfirm)
|
||||||
dialog.cancelButton = NewButton("Cancel", theme, cancelCallback)
|
dialog.cancelButton = NewButton("Cancel", theme, cancelCallback)
|
||||||
dialog.tabOrder = []Component{dialog.inputField, dialog.cancelButton, dialog.confirmButton}
|
dialog.tabOrder = []Component{dialog.inputField, dialog.cancelButton, dialog.confirmButton}
|
||||||
@ -45,7 +45,7 @@ func NewFileSelectorDialog(screen *tcell.Screen, title string, mustExist bool, t
|
|||||||
// onConfirm is a callback called by the confirm button.
|
// onConfirm is a callback called by the confirm button.
|
||||||
func (d *FileSelectorDialog) onConfirm() {
|
func (d *FileSelectorDialog) onConfirm() {
|
||||||
if d.FilesChosenCallback != nil {
|
if d.FilesChosenCallback != nil {
|
||||||
files := strings.Split(d.inputField.Text, ",") // Split input by commas
|
files := strings.Split(string(d.inputField.Buffer), ",") // Split input by commas
|
||||||
for i := range files {
|
for i := range files {
|
||||||
files[i] = strings.TrimSpace(files[i]) // Trim all strings in slice
|
files[i] = strings.TrimSpace(files[i]) // Trim all strings in slice
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ func (d *FileSelectorDialog) SetFocused(v bool) {
|
|||||||
|
|
||||||
func (d *FileSelectorDialog) SetTheme(theme *Theme) {
|
func (d *FileSelectorDialog) SetTheme(theme *Theme) {
|
||||||
d.Theme = theme
|
d.Theme = theme
|
||||||
d.inputField.SetTheme(theme)
|
d.inputField.SetStyle(theme.GetOrDefault("Window"))
|
||||||
d.confirmButton.SetTheme(theme)
|
d.confirmButton.SetTheme(theme)
|
||||||
d.cancelButton.SetTheme(theme)
|
d.cancelButton.SetTheme(theme)
|
||||||
}
|
}
|
||||||
|
126
ui/inputfield.go
126
ui/inputfield.go
@ -1,10 +1,14 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "github.com/gdamore/tcell/v2"
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
)
|
||||||
|
|
||||||
// An InputField is a single-line input box.
|
// An InputField is a single-line input box.
|
||||||
type InputField struct {
|
type InputField struct {
|
||||||
Text string
|
Buffer []byte
|
||||||
|
|
||||||
cursorPos int
|
cursorPos int
|
||||||
scrollPos int
|
scrollPos int
|
||||||
@ -12,30 +16,33 @@ type InputField struct {
|
|||||||
width, height int
|
width, height int
|
||||||
focused bool
|
focused bool
|
||||||
screen *tcell.Screen
|
screen *tcell.Screen
|
||||||
|
style tcell.Style
|
||||||
Theme *Theme
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInputField(screen *tcell.Screen, placeholder string, theme *Theme) *InputField {
|
func NewInputField(screen *tcell.Screen, placeholder []byte, style tcell.Style) *InputField {
|
||||||
return &InputField{
|
return &InputField{
|
||||||
Text: placeholder,
|
Buffer: append(make([]byte, 0, Max(len(placeholder), 32)), placeholder...),
|
||||||
screen: screen,
|
screen: screen,
|
||||||
Theme: theme,
|
style: style,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *InputField) String() string {
|
||||||
|
return string(f.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *InputField) GetCursorPos() int {
|
func (f *InputField) GetCursorPos() int {
|
||||||
return f.cursorPos
|
return f.cursorPos
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCursorPos sets the cursor position offset. Offset is clamped to possible values.
|
// SetCursorPos sets the cursor position offset. Offset is clamped to possible values.
|
||||||
// The InputField is scrolled to show the new cursor position.
|
// The InputField is scrolled to show the new cursor position. The offset is in runes.
|
||||||
func (f *InputField) SetCursorPos(offset int) {
|
func (f *InputField) SetCursorPos(offset int) {
|
||||||
// Clamping
|
// Clamping
|
||||||
if offset < 0 {
|
if offset < 0 {
|
||||||
offset = 0
|
offset = 0
|
||||||
} else if offset > len(f.Text) {
|
} else if runes := utf8.RuneCount(f.Buffer); offset > runes {
|
||||||
offset = len(f.Text)
|
offset = runes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scrolling
|
// Scrolling
|
||||||
@ -51,36 +58,86 @@ func (f *InputField) SetCursorPos(offset int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *InputField) runeIdxToByteIdx(idx int) int {
|
||||||
|
var i int
|
||||||
|
for idx > 0 {
|
||||||
|
_, size := utf8.DecodeRune(f.Buffer[i:])
|
||||||
|
i += size
|
||||||
|
idx--
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *InputField) Insert(contents []byte) {
|
||||||
|
f.Buffer = f.insert(f.Buffer, f.runeIdxToByteIdx(f.cursorPos), contents...)
|
||||||
|
f.SetCursorPos(f.cursorPos + utf8.RuneCount(contents))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Efficient slice inserting from Slice Tricks.
|
||||||
|
func (f *InputField) insert(dst []byte, at int, src ...byte) []byte {
|
||||||
|
if n := len(dst) + len(src); n <= cap(dst) {
|
||||||
|
dstn := dst[:n]
|
||||||
|
copy(dstn[at+len(src):], dst[at:])
|
||||||
|
copy(dstn[at:], src)
|
||||||
|
return dstn
|
||||||
|
}
|
||||||
|
dstn := make([]byte, len(dst) + len(src))
|
||||||
|
copy(dstn, dst[:at])
|
||||||
|
copy(dstn[at:], src)
|
||||||
|
copy(dstn[at+len(src):], dst[at:])
|
||||||
|
return dstn
|
||||||
|
}
|
||||||
|
|
||||||
func (f *InputField) Delete(forward bool) {
|
func (f *InputField) Delete(forward bool) {
|
||||||
if forward {
|
if forward {
|
||||||
if f.cursorPos < len(f.Text) { // If the cursor is not at the very end (past text)...
|
if f.cursorPos < utf8.RuneCount(f.Buffer) { // If the cursor is not at the end...
|
||||||
lineRunes := []rune(f.Text)
|
f.Buffer = f.delete(f.Buffer, f.runeIdxToByteIdx(f.cursorPos))
|
||||||
copy(lineRunes[f.cursorPos:], lineRunes[f.cursorPos+1:]) // Shift characters after cursor left
|
|
||||||
lineRunes = lineRunes[:len(lineRunes)-1] // Shrink line
|
|
||||||
f.Text = string(lineRunes) // Update line with new runes
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if f.cursorPos > 0 { // If the cursor is not at the beginning...
|
if f.cursorPos > 0 { // If the cursor is not at the beginning...
|
||||||
lineRunes := []rune(f.Text)
|
f.SetCursorPos(f.cursorPos - 1)
|
||||||
copy(lineRunes[f.cursorPos-1:], lineRunes[f.cursorPos:]) // Shift characters at cursor left
|
f.Buffer = f.delete(f.Buffer, f.runeIdxToByteIdx(f.cursorPos))
|
||||||
lineRunes = lineRunes[:len(lineRunes)-1] // Shrink line length
|
|
||||||
f.Text = string(lineRunes) // Update line with new runes
|
|
||||||
|
|
||||||
f.SetCursorPos(f.cursorPos - 1) // Move cursor back
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *InputField) delete(dst []byte, at int) []byte {
|
||||||
|
copy(dst[at:], dst[at+1:])
|
||||||
|
dst[len(dst)-1] = 0
|
||||||
|
dst = dst[:len(dst)-1]
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
func (f *InputField) Draw(s tcell.Screen) {
|
func (f *InputField) Draw(s tcell.Screen) {
|
||||||
style := f.Theme.GetOrDefault("InputField")
|
s.SetContent(f.x, f.y, '[', nil, f.style)
|
||||||
|
s.SetContent(f.x+f.width-1, f.y, ']', nil, f.style)
|
||||||
|
|
||||||
DrawRect(s, f.x, f.y, f.width, f.height, ' ', style) // Draw background
|
fg, bg, attr := f.style.Decompose()
|
||||||
s.SetContent(f.x, f.y, '[', nil, style)
|
invertedStyle := tcell.Style{}.Foreground(bg).Background(fg).Attributes(attr)
|
||||||
s.SetContent(f.x+f.width-1, f.y, ']', nil, style)
|
|
||||||
|
|
||||||
if len(f.Text) > 0 {
|
var byteIdx int
|
||||||
endPos := f.scrollPos + Min(len(f.Text)-f.scrollPos, f.width-2)
|
var runeIdx int
|
||||||
DrawStr(s, f.x+1, f.y, f.Text[f.scrollPos:endPos], style) // Draw text
|
|
||||||
|
// Scrolling
|
||||||
|
for byteIdx < len(f.Buffer) && runeIdx < f.scrollPos {
|
||||||
|
_, size := utf8.DecodeRune(f.Buffer[byteIdx:])
|
||||||
|
byteIdx += size
|
||||||
|
runeIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < f.width-2; i++ { // For each column between [ and ]
|
||||||
|
if byteIdx < len(f.Buffer) {
|
||||||
|
// Draw the rune
|
||||||
|
r, size := utf8.DecodeRune(f.Buffer[byteIdx:])
|
||||||
|
|
||||||
|
s.SetContent(f.x+1+i, f.y, r, nil, invertedStyle)
|
||||||
|
|
||||||
|
byteIdx += size
|
||||||
|
runeIdx++
|
||||||
|
} else {
|
||||||
|
// Draw a '.'
|
||||||
|
s.SetContent(f.x+1+i, f.y, '.', nil, f.style)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cursor
|
// Update cursor
|
||||||
@ -96,10 +153,12 @@ func (f *InputField) SetFocused(v bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *InputField) SetTheme(theme *Theme) {
|
func (f *InputField) SetStyle(style tcell.Style) {
|
||||||
f.Theme = theme
|
f.style = style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *InputField) SetTheme(theme *Theme) {}
|
||||||
|
|
||||||
func (f *InputField) GetPos() (int, int) {
|
func (f *InputField) GetPos() (int, int) {
|
||||||
return f.x, f.y
|
return f.x, f.y
|
||||||
}
|
}
|
||||||
@ -141,8 +200,11 @@ func (f *InputField) HandleEvent(event tcell.Event) bool {
|
|||||||
// Inserting
|
// Inserting
|
||||||
case tcell.KeyRune:
|
case tcell.KeyRune:
|
||||||
ch := ev.Rune()
|
ch := ev.Rune()
|
||||||
f.Text += string(ch)
|
if bytesLen := utf8.RuneLen(ch); bytesLen > 0 {
|
||||||
f.SetCursorPos(f.cursorPos + 1)
|
bytes := make([]byte, bytesLen)
|
||||||
|
utf8.EncodeRune(bytes, ch)
|
||||||
|
f.Insert(bytes)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ func (theme *Theme) GetOrDefault(key string) tcell.Style {
|
|||||||
var DefaultTheme = Theme{
|
var DefaultTheme = Theme{
|
||||||
"Normal": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"Normal": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||||
"Button": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite),
|
"Button": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite),
|
||||||
"InputField": tcell.Style{}.Foreground(tcell.ColorWhite).Background(tcell.ColorBlack),
|
"InputField": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||||
"MenuBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
"MenuBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||||
"MenuBarSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"MenuBarSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||||
"Menu": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
"Menu": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user