Added frames (with headers/footers) and buttons. Extended form.
This commit is contained in:
parent
f9f139caaf
commit
b83a7766a6
6 changed files with 440 additions and 42 deletions
135
button.go
Normal file
135
button.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Button is labeled box that triggers an action when selected.
|
||||
type Button struct {
|
||||
Box
|
||||
|
||||
// The text to be displayed before the input area.
|
||||
label string
|
||||
|
||||
// The label color.
|
||||
labelColor tcell.Color
|
||||
|
||||
// The label color when the button is in focus.
|
||||
labelColorActivated tcell.Color
|
||||
|
||||
// The background color when the button is in focus.
|
||||
backgroundColorActivated tcell.Color
|
||||
|
||||
// An optional function which is called when the button was selected.
|
||||
selected func()
|
||||
|
||||
// An optional function which is called when the user leaves the button. A
|
||||
// key is provided indicating which key was pressed to leave (tab or backtab).
|
||||
blur func(tcell.Key)
|
||||
}
|
||||
|
||||
// NewButton returns a new input field.
|
||||
func NewButton(label string) *Button {
|
||||
box := NewBox().SetBackgroundColor(tcell.ColorBlue)
|
||||
return &Button{
|
||||
Box: *box,
|
||||
label: label,
|
||||
labelColor: tcell.ColorWhite,
|
||||
labelColorActivated: tcell.ColorBlue,
|
||||
backgroundColorActivated: tcell.ColorWhite,
|
||||
}
|
||||
}
|
||||
|
||||
// SetLabel sets the button text.
|
||||
func (b *Button) SetLabel(label string) *Button {
|
||||
b.label = label
|
||||
return b
|
||||
}
|
||||
|
||||
// GetLabel returns the button text.
|
||||
func (b *Button) GetLabel() string {
|
||||
return b.label
|
||||
}
|
||||
|
||||
// SetLabelColor sets the color of the button text.
|
||||
func (b *Button) SetLabelColor(color tcell.Color) *Button {
|
||||
b.labelColor = color
|
||||
return b
|
||||
}
|
||||
|
||||
// SetLabelColorActivated sets the color of the button text when the button is
|
||||
// in focus.
|
||||
func (b *Button) SetLabelColorActivated(color tcell.Color) *Button {
|
||||
b.labelColorActivated = color
|
||||
return b
|
||||
}
|
||||
|
||||
// SetBackgroundColorActivated sets the background color of the button text when
|
||||
// the button is in focus.
|
||||
func (b *Button) SetBackgroundColorActivated(color tcell.Color) *Button {
|
||||
b.backgroundColorActivated = color
|
||||
return b
|
||||
}
|
||||
|
||||
// SetSelectedFunc sets a handler which is called when the button was selected.
|
||||
func (b *Button) SetSelectedFunc(handler func()) *Button {
|
||||
b.selected = handler
|
||||
return b
|
||||
}
|
||||
|
||||
// SetBlurFunc sets a handler which is called when the user leaves the button.
|
||||
// The callback function is provided with the key that was pressed, which is one
|
||||
// of the following:
|
||||
//
|
||||
// - KeyEscape: Leaving the button with no specific direction.
|
||||
// - KeyTab: Move to the next field.
|
||||
// - KeyBacktab: Move to the previous field.
|
||||
func (b *Button) SetBlurFunc(handler func(key tcell.Key)) *Button {
|
||||
b.blur = handler
|
||||
return b
|
||||
}
|
||||
|
||||
// Draw draws this primitive onto the screen.
|
||||
func (b *Button) Draw(screen tcell.Screen) {
|
||||
// Draw the box.
|
||||
backgroundColor := b.backgroundColor
|
||||
if b.hasFocus {
|
||||
b.backgroundColor = b.backgroundColorActivated
|
||||
}
|
||||
b.Box.Draw(screen)
|
||||
b.backgroundColor = backgroundColor
|
||||
|
||||
// Draw label.
|
||||
x := b.x + b.width/2
|
||||
y := b.y + b.height/2
|
||||
width := b.width
|
||||
if b.border {
|
||||
width -= 2
|
||||
}
|
||||
labelColor := b.labelColor
|
||||
if b.hasFocus {
|
||||
labelColor = b.labelColorActivated
|
||||
}
|
||||
Print(screen, b.label, x, y, width, AlignCenter, labelColor)
|
||||
|
||||
if b.hasFocus {
|
||||
screen.HideCursor()
|
||||
}
|
||||
}
|
||||
|
||||
// InputHandler returns the handler for this primitive.
|
||||
func (b *Button) InputHandler() func(event *tcell.EventKey) {
|
||||
return func(event *tcell.EventKey) {
|
||||
// Process key event.
|
||||
switch key := event.Key(); key {
|
||||
case tcell.KeyEnter: // Selected.
|
||||
if b.selected != nil {
|
||||
b.selected()
|
||||
}
|
||||
case tcell.KeyBacktab, tcell.KeyTab, tcell.KeyEscape: // Leave. No action.
|
||||
if b.blur != nil {
|
||||
b.blur(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,20 @@
|
|||
package main
|
||||
|
||||
import "github.com/rivo/tview"
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func main() {
|
||||
form := tview.NewForm().AddItem("First name", "", 20, nil).AddItem("Last name", "", 20, nil).AddItem("Age", "", 4, nil)
|
||||
app := tview.NewApplication()
|
||||
|
||||
form := tview.NewFrame(tview.NewForm().
|
||||
AddItem("First name", "", 20, nil).
|
||||
AddItem("Last name", "", 20, nil).
|
||||
AddItem("Age", "", 4, nil).
|
||||
AddButton("Save", func() { app.Stop() }).
|
||||
AddButton("Cancel", nil)).
|
||||
AddText("Customer details", true, tview.AlignLeft, tcell.ColorRed)
|
||||
form.SetBorder(true)
|
||||
|
||||
box := tview.NewFlex(tview.FlexColumn, []tview.Primitive{
|
||||
|
@ -25,7 +36,6 @@ func main() {
|
|||
final := tview.NewFlex(tview.FlexRow, []tview.Primitive{box})
|
||||
final.AddItem(inputField, 3)
|
||||
|
||||
app := tview.NewApplication()
|
||||
app.SetRoot(final, true).SetFocus(form)
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
|
|
79
form.go
79
form.go
|
@ -13,11 +13,15 @@ type Form struct {
|
|||
// The items of the form (one row per item).
|
||||
items []*InputField
|
||||
|
||||
// The buttons of the form.
|
||||
buttons []*Button
|
||||
|
||||
// The number of empty rows between items.
|
||||
itemPadding int
|
||||
|
||||
// The index of the item which has focus.
|
||||
focusedItem int
|
||||
// The index of the item or button which has focus. (Items are counted first,
|
||||
// buttons are counted last.)
|
||||
focusedElement int
|
||||
|
||||
// The label color.
|
||||
labelColor tcell.Color
|
||||
|
@ -77,6 +81,13 @@ func (f *Form) AddItem(label, value string, fieldLength int, accept func(textToC
|
|||
return f
|
||||
}
|
||||
|
||||
// AddButton adds a new button to the form. The "selected" function is called
|
||||
// when the user selects this button. It may be nil.
|
||||
func (f *Form) AddButton(label string, selected func()) *Form {
|
||||
f.buttons = append(f.buttons, NewButton(label).SetSelectedFunc(selected))
|
||||
return f
|
||||
}
|
||||
|
||||
// Draw draws this primitive onto the screen.
|
||||
func (f *Form) Draw(screen tcell.Screen) {
|
||||
f.Box.Draw(screen)
|
||||
|
@ -92,6 +103,7 @@ func (f *Form) Draw(screen tcell.Screen) {
|
|||
width -= 2
|
||||
bottomLimit -= 2
|
||||
}
|
||||
rightLimit := x + width
|
||||
|
||||
// Find the longest label.
|
||||
var labelLength int
|
||||
|
@ -106,7 +118,7 @@ func (f *Form) Draw(screen tcell.Screen) {
|
|||
// Set up and draw the input fields.
|
||||
for _, inputField := range f.items {
|
||||
if y >= bottomLimit {
|
||||
break
|
||||
return // Stop here.
|
||||
}
|
||||
label := strings.TrimSpace(inputField.GetLabel())
|
||||
inputField.SetLabelColor(f.labelColor).
|
||||
|
@ -118,34 +130,71 @@ func (f *Form) Draw(screen tcell.Screen) {
|
|||
inputField.Draw(screen)
|
||||
y += 1 + f.itemPadding
|
||||
}
|
||||
|
||||
// Draw the buttons.
|
||||
if f.itemPadding == 0 {
|
||||
y++
|
||||
}
|
||||
if y >= bottomLimit {
|
||||
return // Stop here.
|
||||
}
|
||||
for _, button := range f.buttons {
|
||||
space := rightLimit - x
|
||||
if space < 1 {
|
||||
return // No space for this button anymore.
|
||||
}
|
||||
buttonWidth := len([]rune(button.GetLabel())) + 4
|
||||
if buttonWidth > space {
|
||||
buttonWidth = space
|
||||
}
|
||||
button.SetRect(x, y, buttonWidth, 1)
|
||||
button.Draw(screen)
|
||||
|
||||
x += buttonWidth + 2
|
||||
}
|
||||
}
|
||||
|
||||
// Focus is called by the application when the primitive receives focus.
|
||||
func (f *Form) Focus(app *Application) {
|
||||
f.Box.Focus(app)
|
||||
|
||||
if len(f.items) == 0 {
|
||||
if len(f.items)+len(f.buttons) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Hand on the focus to one of our items.
|
||||
if f.focusedItem < 0 || f.focusedItem >= len(f.items) {
|
||||
f.focusedItem = 0
|
||||
// Hand on the focus to one of our child elements.
|
||||
if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) {
|
||||
f.focusedElement = 0
|
||||
}
|
||||
f.hasFocus = false
|
||||
inputField := f.items[f.focusedItem]
|
||||
inputField.SetDoneFunc(func(key tcell.Key) {
|
||||
handler := func(key tcell.Key) {
|
||||
switch key {
|
||||
case tcell.KeyTab:
|
||||
f.focusedItem++
|
||||
f.Focus(app)
|
||||
f.focusedElement++
|
||||
case tcell.KeyBacktab:
|
||||
f.focusedElement--
|
||||
if f.focusedElement < 0 {
|
||||
f.focusedElement = len(f.items) + len(f.buttons) - 1
|
||||
}
|
||||
case tcell.KeyEscape:
|
||||
f.focusedElement = 0
|
||||
}
|
||||
})
|
||||
app.SetFocus(inputField)
|
||||
f.Focus(app)
|
||||
}
|
||||
if f.focusedElement < len(f.items) {
|
||||
// We're selecting an item.
|
||||
inputField := f.items[f.focusedElement]
|
||||
inputField.SetDoneFunc(handler)
|
||||
app.SetFocus(inputField)
|
||||
} else {
|
||||
// We're selecting a button.
|
||||
button := f.buttons[f.focusedElement-len(f.items)]
|
||||
button.SetBlurFunc(handler)
|
||||
app.SetFocus(button)
|
||||
}
|
||||
}
|
||||
|
||||
// InputHandler returns the handler for this primitive.
|
||||
func (f *Form) InputHandler() func(event *tcell.EventKey) {
|
||||
return func(event *tcell.EventKey) {
|
||||
}
|
||||
return func(event *tcell.EventKey) {}
|
||||
}
|
||||
|
|
155
frame.go
Normal file
155
frame.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// frameText holds information about a line of text shown in the frame.
|
||||
type frameText struct {
|
||||
Text string // The text to be displayed.
|
||||
Header bool // true = place in header, false = place in footer.
|
||||
Align int // One of the Align constants.
|
||||
Color tcell.Color // The text color.
|
||||
}
|
||||
|
||||
// Frame is a wrapper which adds a border around another primitive. The top and
|
||||
// the bottom border may also contain text.
|
||||
type Frame struct {
|
||||
Box
|
||||
|
||||
// The contained primitive.
|
||||
primitive Primitive
|
||||
|
||||
// The lines of text to be displayed.
|
||||
text []*frameText
|
||||
|
||||
// Border spacing.
|
||||
top, bottom, header, footer, left, right int
|
||||
}
|
||||
|
||||
// NewFrame returns a new frame around the given primitive. The primitive's
|
||||
// size will be changed to fit within this frame.
|
||||
func NewFrame(primitive Primitive) *Frame {
|
||||
return &Frame{
|
||||
Box: *NewBox(),
|
||||
primitive: primitive,
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
header: 1,
|
||||
footer: 1,
|
||||
left: 1,
|
||||
right: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// AddText adds text to the frame. Set "header" to true if the text is to appear
|
||||
// in the header, above the contained primitive. Set it to false for it to
|
||||
// appear in the footer, below the contained primitive. "align" must be one of
|
||||
// the Align constants. Rows in the header are printed top to bottom, rows in
|
||||
// the footer are printed bottom to top. Note that long text can overlap as
|
||||
// different alignments will be placed on the same row.
|
||||
func (f *Frame) AddText(text string, header bool, align int, color tcell.Color) *Frame {
|
||||
f.text = append(f.text, &frameText{
|
||||
Text: text,
|
||||
Header: header,
|
||||
Align: align,
|
||||
Color: color,
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
// SetBorders sets the width of the frame borders as well as "header" and
|
||||
// "footer", the vertical space between the header and footer text and the
|
||||
// contained primitive (does not apply if there is no text).
|
||||
func (f *Frame) SetBorders(top, bottom, header, footer, left, right int) *Frame {
|
||||
f.top, f.bottom, f.header, f.footer, f.left, f.right = top, bottom, header, footer, left, right
|
||||
return f
|
||||
}
|
||||
|
||||
// Draw draws this primitive onto the screen.
|
||||
func (f *Frame) Draw(screen tcell.Screen) {
|
||||
f.Box.Draw(screen)
|
||||
|
||||
// Calculate start positions.
|
||||
left := f.x
|
||||
right := f.x + f.width - 1
|
||||
top := f.y
|
||||
bottom := f.y + f.height - 1
|
||||
if f.border {
|
||||
left++
|
||||
right--
|
||||
top++
|
||||
bottom--
|
||||
}
|
||||
left += f.left
|
||||
right -= f.right
|
||||
top += f.top
|
||||
bottom -= f.bottom
|
||||
center := (left + right) / 2
|
||||
if left >= right || top >= bottom {
|
||||
return // No space left.
|
||||
}
|
||||
|
||||
// Draw text.
|
||||
var rows [6]int // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right.
|
||||
topMax := top
|
||||
bottomMin := bottom
|
||||
for _, text := range f.text {
|
||||
// Where do we place this text?
|
||||
var y int
|
||||
if text.Header {
|
||||
y = top + rows[text.Align]
|
||||
rows[text.Align]++
|
||||
if y >= bottomMin {
|
||||
continue
|
||||
}
|
||||
if y+1 > topMax {
|
||||
topMax = y + 1
|
||||
}
|
||||
} else {
|
||||
y = bottom - rows[3+text.Align]
|
||||
rows[3+text.Align]++
|
||||
if y <= topMax {
|
||||
continue
|
||||
}
|
||||
if y-1 < bottomMin {
|
||||
bottomMin = y - 1
|
||||
}
|
||||
}
|
||||
x := left
|
||||
if text.Align == AlignCenter {
|
||||
x = center
|
||||
} else if text.Align == AlignRight {
|
||||
x = right
|
||||
}
|
||||
|
||||
// Draw text.
|
||||
Print(screen, text.Text, x, y, right-left+1, text.Align, text.Color)
|
||||
}
|
||||
|
||||
// Set the size of the contained primitive.
|
||||
if topMax > top {
|
||||
top = topMax + 1 + f.header
|
||||
}
|
||||
if bottomMin < bottom {
|
||||
bottom = bottomMin - f.footer
|
||||
}
|
||||
if top >= bottom {
|
||||
return // No space for the primitive.
|
||||
}
|
||||
f.primitive.SetRect(left, top, right+1-left, bottom-top)
|
||||
|
||||
// Finally, draw the contained primitive.
|
||||
f.primitive.Draw(screen)
|
||||
}
|
||||
|
||||
// Focus is called when this primitive receives focus.
|
||||
func (f *Frame) Focus(app *Application) {
|
||||
app.SetFocus(f.primitive)
|
||||
}
|
||||
|
||||
// InputHandler returns the handler for this primitive.
|
||||
func (f *Frame) InputHandler() func(event *tcell.EventKey) {
|
||||
return func(event *tcell.EventKey) {
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package tview
|
|||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
|
@ -76,7 +77,8 @@ type InputField struct {
|
|||
accept func(text string, ch rune) bool
|
||||
|
||||
// An optional function which is called when the user indicated that they
|
||||
// are done entering text. The key which was pressed is provided.
|
||||
// are done entering text. The key which was pressed is provided (tab,
|
||||
// shift-tab, enter, or escape).
|
||||
done func(tcell.Key)
|
||||
}
|
||||
|
||||
|
@ -154,6 +156,7 @@ func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar
|
|||
// - KeyEnter: Done entering text.
|
||||
// - KeyEscape: Abort text input.
|
||||
// - KeyTab: Move to the next field.
|
||||
// - KeyBacktab: Move to the previous field.
|
||||
func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) *InputField {
|
||||
i.done = handler
|
||||
return i
|
||||
|
@ -179,17 +182,9 @@ func (i *InputField) Draw(screen tcell.Screen) {
|
|||
}
|
||||
|
||||
// Draw label.
|
||||
labelStyle := tcell.StyleDefault.Background(i.backgroundColor).Foreground(i.labelColor)
|
||||
for _, ch := range i.label {
|
||||
if x >= rightLimit {
|
||||
return
|
||||
}
|
||||
screen.SetContent(x, y, ch, nil, labelStyle)
|
||||
x++
|
||||
}
|
||||
x += Print(screen, i.label, x, y, rightLimit-x, AlignLeft, i.labelColor)
|
||||
|
||||
// Draw input area.
|
||||
inputStyle := tcell.StyleDefault.Background(i.fieldBackgroundColor).Foreground(i.fieldTextColor)
|
||||
fieldLength := i.fieldLength
|
||||
if fieldLength == 0 {
|
||||
fieldLength = math.MaxInt64
|
||||
|
@ -197,20 +192,17 @@ func (i *InputField) Draw(screen tcell.Screen) {
|
|||
if rightLimit-x < fieldLength {
|
||||
fieldLength = rightLimit - x
|
||||
}
|
||||
text := []rune(i.text)
|
||||
index := 0
|
||||
if fieldLength-1 < len(text) {
|
||||
index = len(text) - fieldLength + 1
|
||||
fieldStyle := tcell.StyleDefault.Background(i.fieldBackgroundColor)
|
||||
for index := 0; index < fieldLength; index++ {
|
||||
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
|
||||
}
|
||||
for fieldLength > 0 {
|
||||
ch := ' '
|
||||
if index < len(text) {
|
||||
ch = text[index]
|
||||
}
|
||||
screen.SetContent(x, y, ch, nil, inputStyle)
|
||||
x++
|
||||
index++
|
||||
fieldLength--
|
||||
|
||||
// Draw entered text.
|
||||
fieldLength-- // We need one cell for the cursor.
|
||||
if fieldLength < len([]rune(i.text)) {
|
||||
Print(screen, i.text, x+fieldLength-1, y, fieldLength, AlignRight, i.fieldTextColor)
|
||||
} else {
|
||||
Print(screen, i.text, x, y, fieldLength, AlignLeft, i.fieldTextColor)
|
||||
}
|
||||
|
||||
// Set cursor.
|
||||
|
@ -255,12 +247,15 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey) {
|
|||
i.text = newText
|
||||
case tcell.KeyCtrlU: // Delete all.
|
||||
i.text = ""
|
||||
case tcell.KeyCtrlW: // Delete last word.
|
||||
lastWord := regexp.MustCompile(`\s*\S+\s*$`)
|
||||
i.text = lastWord.ReplaceAllString(i.text, "")
|
||||
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character.
|
||||
if len([]rune(i.text)) == 0 {
|
||||
break
|
||||
}
|
||||
i.text = i.text[:len([]rune(i.text))-1]
|
||||
case tcell.KeyEnter, tcell.KeyTab, tcell.KeyEscape: // We're done.
|
||||
case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
|
||||
if i.done != nil {
|
||||
i.done(key)
|
||||
}
|
||||
|
|
54
util.go
Normal file
54
util.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package tview
|
||||
|
||||
import "github.com/gdamore/tcell"
|
||||
|
||||
// Text alignment within a box.
|
||||
const (
|
||||
AlignLeft = iota
|
||||
AlignCenter
|
||||
AlignRight
|
||||
)
|
||||
|
||||
// Print prints text onto the screen at position (x,y). "align" is one of the
|
||||
// Align constants and will affect the direction starting at (x,y) into which
|
||||
// the text is printed. The screen's background color will be maintained. The
|
||||
// number of runes printed will not exceed "maxWidth".
|
||||
//
|
||||
// Returns the number of runes printed.
|
||||
func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) int {
|
||||
// We deal with runes, not with bytes.
|
||||
runes := []rune(text)
|
||||
if maxWidth < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Shorten text if it's too long.
|
||||
if len(runes) > maxWidth {
|
||||
switch align {
|
||||
case AlignCenter:
|
||||
trim := (len(runes) - maxWidth) / 2
|
||||
runes = runes[trim : maxWidth+trim]
|
||||
case AlignRight:
|
||||
runes = runes[len(runes)-maxWidth:]
|
||||
default: // AlignLeft.
|
||||
runes = runes[:maxWidth]
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust x-position.
|
||||
if align == AlignCenter {
|
||||
x -= len(runes) / 2
|
||||
} else if align == AlignRight {
|
||||
x -= len(runes) - 1
|
||||
}
|
||||
|
||||
// Draw text.
|
||||
for _, ch := range runes {
|
||||
_, _, style, _ := screen.GetContent(x, y)
|
||||
style = style.Foreground(color)
|
||||
screen.SetContent(x, y, ch, nil, style)
|
||||
x++
|
||||
}
|
||||
|
||||
return len(runes)
|
||||
}
|
Loading…
Reference in a new issue