Added frames (with headers/footers) and buttons. Extended form.

This commit is contained in:
Oliver 2017-12-15 23:03:01 +01:00
parent f9f139caaf
commit b83a7766a6
6 changed files with 440 additions and 42 deletions

135
button.go Normal file
View 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)
}
}
}
}

View File

@ -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
View File

@ -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
View 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) {
}
}

View File

@ -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
View 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)
}