2015-01-03 07:50:37 +00:00
|
|
|
// Standard components
|
|
|
|
|
|
|
|
|
|
package ui
|
|
|
|
|
|
2018-09-01 01:27:34 +00:00
|
|
|
import (
|
|
|
|
|
"unicode"
|
2020-09-29 23:08:57 +00:00
|
|
|
)
|
2015-01-03 07:50:37 +00:00
|
|
|
|
2015-01-06 11:34:06 +00:00
|
|
|
// A text component. This simply renders a text string.
|
|
|
|
|
type TextView struct {
|
|
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
// The string to render
|
|
|
|
|
Text string
|
2015-01-06 11:34:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Minimum dimensions
|
|
|
|
|
func (tv *TextView) Remeasure(w, h int) (int, int) {
|
2017-08-25 23:48:22 +00:00
|
|
|
return w, 1
|
2015-01-06 11:34:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Status bar redraw
|
|
|
|
|
func (tv *TextView) Redraw(context *DrawContext) {
|
2017-08-25 23:48:22 +00:00
|
|
|
context.SetFgAttr(0)
|
|
|
|
|
context.SetBgAttr(0)
|
2015-01-06 11:34:06 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
context.HorizRule(0, ' ')
|
|
|
|
|
context.Print(0, 0, tv.Text)
|
|
|
|
|
context.HideCursor()
|
2015-01-06 11:34:06 +00:00
|
|
|
}
|
|
|
|
|
|
2015-01-03 07:50:37 +00:00
|
|
|
// Status bar component. This component displays text on the left and right of it's
|
|
|
|
|
// allocated space.
|
|
|
|
|
type StatusBar struct {
|
2017-08-25 23:48:22 +00:00
|
|
|
Left string // Left aligned string
|
|
|
|
|
Right string // Right aligned string
|
2015-01-03 07:50:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Minimum dimensions
|
|
|
|
|
func (sbar *StatusBar) Remeasure(w, h int) (int, int) {
|
2017-08-25 23:48:22 +00:00
|
|
|
return w, 1
|
2015-01-03 07:50:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Status bar redraw
|
|
|
|
|
func (sbar *StatusBar) Redraw(context *DrawContext) {
|
2017-08-25 23:48:22 +00:00
|
|
|
context.SetFgAttr(AttrReverse)
|
|
|
|
|
context.SetBgAttr(AttrReverse)
|
2015-01-03 07:50:37 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
context.HorizRule(0, ' ')
|
|
|
|
|
context.Print(0, 0, sbar.Left)
|
|
|
|
|
context.PrintRight(context.W, 0, sbar.Right)
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A single-text entry component.
|
|
|
|
|
type TextEntry struct {
|
2017-08-25 23:48:22 +00:00
|
|
|
Prompt string
|
2015-01-05 11:50:02 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
value string
|
|
|
|
|
cursorOffset int
|
|
|
|
|
displayOffset int
|
2020-10-21 03:35:52 +00:00
|
|
|
isDirty bool
|
|
|
|
|
|
|
|
|
|
// CancelOnEmptyBackspace will cancel the text entry prompt if no other
|
|
|
|
|
// key was pressed and the prompt was empty.
|
|
|
|
|
CancelOnEmptyBackspace bool
|
2017-08-25 23:48:22 +00:00
|
|
|
|
|
|
|
|
// Called when the user presses Enter
|
|
|
|
|
OnEntry func(val string)
|
2018-09-01 01:27:34 +00:00
|
|
|
|
|
|
|
|
// Called when the user presses Esc or CtrlC
|
|
|
|
|
OnCancel func()
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
2020-10-21 03:35:52 +00:00
|
|
|
func (te *TextEntry) Reset() {
|
|
|
|
|
te.isDirty = false
|
|
|
|
|
}
|
|
|
|
|
|
2015-01-05 11:50:02 +00:00
|
|
|
func (te *TextEntry) Remeasure(w, h int) (int, int) {
|
2017-08-25 23:48:22 +00:00
|
|
|
return w, 1
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (te *TextEntry) Redraw(context *DrawContext) {
|
2017-08-25 23:48:22 +00:00
|
|
|
context.HorizRule(0, ' ')
|
|
|
|
|
valueOffsetX := 0
|
|
|
|
|
displayOffsetX := te.calculateDisplayOffset(context.W)
|
2015-01-05 11:50:02 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
if te.Prompt != "" {
|
|
|
|
|
context.SetFgAttr(ColorDefault | AttrBold)
|
|
|
|
|
context.Print(0, 0, te.Prompt)
|
|
|
|
|
context.SetFgAttr(ColorDefault)
|
2015-01-05 11:50:02 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
valueOffsetX = len(te.Prompt)
|
|
|
|
|
}
|
2015-01-05 11:50:02 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
context.Print(valueOffsetX, 0, te.value[displayOffsetX:intMin(displayOffsetX+context.W, len(te.value))])
|
|
|
|
|
context.SetCursorPosition(te.cursorOffset+valueOffsetX-displayOffsetX, 0)
|
2015-01-05 11:50:02 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
//context.Print(0, 0, fmt.Sprintf("%d,%d", te.cursorOffset, displayOffsetX))
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (te *TextEntry) calculateDisplayOffset(displayWidth int) int {
|
2017-08-25 23:48:22 +00:00
|
|
|
if te.Prompt != "" {
|
|
|
|
|
displayWidth -= len(te.Prompt)
|
|
|
|
|
}
|
|
|
|
|
virtualCursorOffset := te.cursorOffset - te.displayOffset
|
|
|
|
|
|
|
|
|
|
if virtualCursorOffset >= displayWidth {
|
|
|
|
|
te.displayOffset = te.cursorOffset - displayWidth + 10
|
|
|
|
|
} else if virtualCursorOffset < 0 {
|
|
|
|
|
te.displayOffset = intMax(te.cursorOffset-displayWidth+1, 0)
|
|
|
|
|
}
|
2015-01-05 11:50:02 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
return te.displayOffset
|
|
|
|
|
}
|
2015-01-05 11:50:02 +00:00
|
|
|
|
2017-08-25 23:48:22 +00:00
|
|
|
// SetValue sets the value of the text entry
|
|
|
|
|
func (te *TextEntry) SetValue(val string) {
|
|
|
|
|
te.value = val
|
|
|
|
|
te.cursorOffset = len(val)
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (te *TextEntry) KeyPressed(key rune, mod int) {
|
2017-08-25 23:48:22 +00:00
|
|
|
if (key >= ' ') && (key <= '~') {
|
|
|
|
|
te.insertRune(key)
|
|
|
|
|
} else if key == KeyArrowLeft {
|
|
|
|
|
te.moveCursorBy(-1)
|
|
|
|
|
} else if key == KeyArrowRight {
|
|
|
|
|
te.moveCursorBy(1)
|
2020-10-21 03:35:52 +00:00
|
|
|
} else if (key == KeyHome) || (key == KeyCtrlA) {
|
2017-08-25 23:48:22 +00:00
|
|
|
te.moveCursorTo(0)
|
2020-10-21 03:35:52 +00:00
|
|
|
} else if (key == KeyEnd) || (key == KeyCtrlE) {
|
2017-08-25 23:48:22 +00:00
|
|
|
te.moveCursorTo(len(te.value))
|
|
|
|
|
} else if (key == KeyBackspace) || (key == KeyBackspace2) {
|
|
|
|
|
if mod&ModKeyAlt != 0 {
|
|
|
|
|
te.backspaceWhile(unicode.IsSpace)
|
|
|
|
|
te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) })
|
2020-09-29 23:08:57 +00:00
|
|
|
} else if te.cursorOffset == 0 {
|
2020-10-21 03:35:52 +00:00
|
|
|
if te.CancelOnEmptyBackspace && !te.isDirty {
|
|
|
|
|
te.cancelAndExit()
|
|
|
|
|
}
|
2017-08-25 23:48:22 +00:00
|
|
|
} else {
|
|
|
|
|
te.backspace()
|
|
|
|
|
}
|
|
|
|
|
} else if key == KeyCtrlK {
|
|
|
|
|
te.killLine()
|
|
|
|
|
} else if key == KeyDelete {
|
|
|
|
|
te.removeCharAtPos(te.cursorOffset)
|
|
|
|
|
} else if key == KeyEnter {
|
|
|
|
|
if te.OnEntry != nil {
|
|
|
|
|
te.OnEntry(te.value)
|
|
|
|
|
}
|
2018-09-01 01:27:34 +00:00
|
|
|
} else if key == KeyCtrlC {
|
2020-09-29 23:08:57 +00:00
|
|
|
te.cancelAndExit()
|
2017-08-25 23:48:22 +00:00
|
|
|
}
|
2018-09-01 01:27:34 +00:00
|
|
|
|
|
|
|
|
//panic(fmt.Sprintf("Entered key: '%x', mod: '%x'", key, mod))
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
2020-09-29 23:08:57 +00:00
|
|
|
func (te *TextEntry) cancelAndExit() {
|
|
|
|
|
if te.OnCancel != nil {
|
|
|
|
|
te.OnCancel()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-01-05 11:50:02 +00:00
|
|
|
// Backspace
|
|
|
|
|
func (te *TextEntry) backspace() {
|
2017-08-25 23:48:22 +00:00
|
|
|
te.removeCharAtPos(te.cursorOffset - 1)
|
|
|
|
|
te.moveCursorBy(-1)
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Backspace while the character underneith the cursor matches the guard
|
|
|
|
|
func (te *TextEntry) backspaceWhile(guard func(r rune) bool) {
|
2017-08-25 23:48:22 +00:00
|
|
|
for te.cursorOffset > 0 {
|
|
|
|
|
ch := rune(te.value[te.cursorOffset-1])
|
|
|
|
|
if guard(ch) {
|
|
|
|
|
te.backspace()
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-01-03 12:09:35 +00:00
|
|
|
}
|
2015-01-05 11:50:02 +00:00
|
|
|
|
|
|
|
|
// Kill the line. If the cursor is at the end of the line, kill to the start.
|
|
|
|
|
// Otherwise, trim the line.
|
|
|
|
|
func (te *TextEntry) killLine() {
|
2020-10-21 03:35:52 +00:00
|
|
|
te.isDirty = true
|
2017-08-25 23:48:22 +00:00
|
|
|
if te.cursorOffset < len(te.value) {
|
|
|
|
|
te.value = te.value[:te.cursorOffset]
|
|
|
|
|
} else {
|
|
|
|
|
te.value = ""
|
|
|
|
|
te.cursorOffset = 0
|
|
|
|
|
}
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inserts a rune at the cursor position
|
|
|
|
|
func (te *TextEntry) insertRune(key rune) {
|
2020-10-21 03:35:52 +00:00
|
|
|
te.isDirty = true
|
2017-08-25 23:48:22 +00:00
|
|
|
if te.cursorOffset >= len(te.value) {
|
|
|
|
|
te.value += string(key)
|
|
|
|
|
} else {
|
|
|
|
|
te.value = te.value[:te.cursorOffset] + string(key) + te.value[te.cursorOffset:]
|
|
|
|
|
}
|
|
|
|
|
te.moveCursorBy(1)
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove the character at a specific position
|
|
|
|
|
func (te *TextEntry) removeCharAtPos(pos int) {
|
2020-10-21 03:35:52 +00:00
|
|
|
te.isDirty = true
|
2017-08-25 23:48:22 +00:00
|
|
|
if (pos >= 0) && (pos < len(te.value)) {
|
|
|
|
|
te.value = te.value[:pos] + te.value[pos+1:]
|
|
|
|
|
}
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Move the cursor
|
|
|
|
|
func (te *TextEntry) moveCursorBy(byX int) {
|
2017-08-25 23:48:22 +00:00
|
|
|
te.moveCursorTo(te.cursorOffset + byX)
|
2015-01-05 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (te *TextEntry) moveCursorTo(toX int) {
|
2017-08-25 23:48:22 +00:00
|
|
|
te.cursorOffset = intMinMax(toX, 0, len(te.value))
|
|
|
|
|
}
|