215 lines
4.7 KiB
Go
215 lines
4.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"unicode/utf8"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
)
|
|
|
|
// An InputField is a single-line input box.
|
|
type InputField struct {
|
|
Buffer []byte
|
|
|
|
cursorPos int
|
|
scrollPos int
|
|
x, y int
|
|
width, height int
|
|
focused bool
|
|
screen *tcell.Screen
|
|
style tcell.Style
|
|
}
|
|
|
|
func NewInputField(screen *tcell.Screen, placeholder []byte, style tcell.Style) *InputField {
|
|
return &InputField{
|
|
Buffer: append(make([]byte, 0, Max(len(placeholder), 32)), placeholder...),
|
|
screen: screen,
|
|
style: style,
|
|
}
|
|
}
|
|
|
|
func (f *InputField) String() string {
|
|
return string(f.Buffer)
|
|
}
|
|
|
|
func (f *InputField) GetCursorPos() int {
|
|
return f.cursorPos
|
|
}
|
|
|
|
// SetCursorPos sets the cursor position offset. Offset is clamped to possible values.
|
|
// The InputField is scrolled to show the new cursor position. The offset is in runes.
|
|
func (f *InputField) SetCursorPos(offset int) {
|
|
// Clamping
|
|
if offset < 0 {
|
|
offset = 0
|
|
} else if runes := utf8.RuneCount(f.Buffer); offset > runes {
|
|
offset = runes
|
|
}
|
|
|
|
// Scrolling
|
|
if offset >= f.scrollPos+f.width-2 { // If cursor position is out of view to the right...
|
|
f.scrollPos = offset - f.width + 2 // Scroll just enough to view that column
|
|
} else if offset < f.scrollPos { // If cursor position is out of view to the left...
|
|
f.scrollPos = offset
|
|
}
|
|
|
|
f.cursorPos = offset
|
|
if f.focused {
|
|
(*f.screen).ShowCursor(f.x+offset-f.scrollPos+1, f.y)
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if forward {
|
|
if f.cursorPos < utf8.RuneCount(f.Buffer) { // If the cursor is not at the end...
|
|
f.Buffer = f.delete(f.Buffer, f.runeIdxToByteIdx(f.cursorPos))
|
|
}
|
|
} else {
|
|
if f.cursorPos > 0 { // If the cursor is not at the beginning...
|
|
f.SetCursorPos(f.cursorPos - 1)
|
|
f.Buffer = f.delete(f.Buffer, f.runeIdxToByteIdx(f.cursorPos))
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
s.SetContent(f.x, f.y, '[', nil, f.style)
|
|
s.SetContent(f.x+f.width-1, f.y, ']', nil, f.style)
|
|
|
|
fg, bg, attr := f.style.Decompose()
|
|
invertedStyle := tcell.Style{}.Foreground(bg).Background(fg).Attributes(attr)
|
|
|
|
var byteIdx int
|
|
var runeIdx int
|
|
|
|
// 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
|
|
f.SetCursorPos(f.cursorPos)
|
|
}
|
|
|
|
func (f *InputField) SetFocused(v bool) {
|
|
f.focused = v
|
|
if v {
|
|
f.SetCursorPos(f.cursorPos)
|
|
} else {
|
|
(*f.screen).HideCursor()
|
|
}
|
|
}
|
|
|
|
func (f *InputField) SetStyle(style tcell.Style) {
|
|
f.style = style
|
|
}
|
|
|
|
func (f *InputField) SetTheme(theme *Theme) {}
|
|
|
|
func (f *InputField) GetPos() (int, int) {
|
|
return f.x, f.y
|
|
}
|
|
|
|
func (f *InputField) SetPos(x, y int) {
|
|
f.x, f.y = x, y
|
|
}
|
|
|
|
func (f *InputField) GetMinSize() (int, int) {
|
|
return 0, 0
|
|
}
|
|
|
|
func (f *InputField) GetSize() (int, int) {
|
|
return f.width, f.height
|
|
}
|
|
|
|
func (f *InputField) SetSize(width, height int) {
|
|
f.width, f.height = width, height
|
|
}
|
|
|
|
func (f *InputField) HandleEvent(event tcell.Event) bool {
|
|
switch ev := event.(type) {
|
|
case *tcell.EventKey:
|
|
switch ev.Key() {
|
|
// Cursor movement
|
|
case tcell.KeyLeft:
|
|
f.SetCursorPos(f.cursorPos - 1)
|
|
case tcell.KeyRight:
|
|
f.SetCursorPos(f.cursorPos + 1)
|
|
|
|
// Deleting
|
|
case tcell.KeyBackspace:
|
|
fallthrough
|
|
case tcell.KeyBackspace2:
|
|
f.Delete(false)
|
|
case tcell.KeyDelete:
|
|
f.Delete(true)
|
|
|
|
// Inserting
|
|
case tcell.KeyRune:
|
|
ch := ev.Rune()
|
|
if bytesLen := utf8.RuneLen(ch); bytesLen > 0 {
|
|
bytes := make([]byte, bytesLen)
|
|
utf8.EncodeRune(bytes, ch)
|
|
f.Insert(bytes)
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|