Added list, improved existing primitives, and fixed a bunch of bugs.

This commit is contained in:
Oliver 2017-12-16 22:48:26 +01:00
parent b83a7766a6
commit d5bf1a4ef0
10 changed files with 383 additions and 80 deletions

94
box.go
View File

@ -1,16 +1,24 @@
package tview
import "github.com/gdamore/tcell"
import (
"github.com/gdamore/tcell"
)
// Characters to draw the box border.
const (
BoxVertBar = '\u2500'
BoxHorBar = '\u2502'
BoxTopLeftCorner = '\u250c'
BoxTopRightCorner = '\u2510'
BoxBottomRightCorner = '\u2518'
BoxBottomLeftCorner = '\u2514'
BoxEllipsis = '\u2026'
BoxVertBar = '\u2500'
BoxHorBar = '\u2502'
BoxTopLeftCorner = '\u250c'
BoxTopRightCorner = '\u2510'
BoxBottomRightCorner = '\u2518'
BoxBottomLeftCorner = '\u2514'
BoxDbVertBar = '\u2550'
BoxDbHorBar = '\u2551'
BoxDbTopLeftCorner = '\u2554'
BoxDbTopRightCorner = '\u2557'
BoxDbBottomRightCorner = '\u255d'
BoxDbBottomLeftCorner = '\u255a'
BoxEllipsis = '\u2026'
)
// Box implements Rect with a background and optional elements such as a border
@ -19,9 +27,6 @@ type Box struct {
// The position of the rect.
x, y, width, height int
// Whether or not the box has focus.
hasFocus bool
// The box's background color.
backgroundColor tcell.Color
@ -32,25 +37,30 @@ type Box struct {
// The color of the border.
borderColor tcell.Color
// The color of the border when the box has focus.
focusedBorderColor tcell.Color
// The title. Only visible if there is a border, too.
title string
// The color of the title.
titleColor tcell.Color
// Provides a way to find out if this box has focus. We always go through
// this interface because it may be overriden by implementing classes.
focus Focusable
// Whether or not this box has focus.
hasFocus bool
}
// NewBox returns a Box without a border.
func NewBox() *Box {
return &Box{
width: 15,
height: 10,
borderColor: tcell.ColorWhite,
focusedBorderColor: tcell.ColorYellow,
titleColor: tcell.ColorWhite,
b := &Box{
width: 15,
height: 10,
borderColor: tcell.ColorWhite,
titleColor: tcell.ColorWhite,
}
b.focus = b
return b
}
// Draw draws this primitive onto the screen.
@ -73,21 +83,34 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw border.
if b.border && b.width >= 2 && b.height >= 2 {
border := background.Foreground(b.borderColor)
if b.hasFocus {
border = background.Foreground(b.focusedBorderColor)
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
if b.focus.HasFocus() {
vertical = BoxDbVertBar
horizontal = BoxDbHorBar
topLeft = BoxDbTopLeftCorner
topRight = BoxDbTopRightCorner
bottomLeft = BoxDbBottomLeftCorner
bottomRight = BoxDbBottomRightCorner
} else {
vertical = BoxVertBar
horizontal = BoxHorBar
topLeft = BoxTopLeftCorner
topRight = BoxTopRightCorner
bottomLeft = BoxBottomLeftCorner
bottomRight = BoxBottomRightCorner
}
for x := b.x + 1; x < b.x+b.width-1; x++ {
screen.SetContent(x, b.y, BoxVertBar, nil, border)
screen.SetContent(x, b.y+b.height-1, BoxVertBar, nil, border)
screen.SetContent(x, b.y, vertical, nil, border)
screen.SetContent(x, b.y+b.height-1, vertical, nil, border)
}
for y := b.y + 1; y < b.y+b.height-1; y++ {
screen.SetContent(b.x, y, BoxHorBar, nil, border)
screen.SetContent(b.x+b.width-1, y, BoxHorBar, nil, border)
screen.SetContent(b.x, y, horizontal, nil, border)
screen.SetContent(b.x+b.width-1, y, horizontal, nil, border)
}
screen.SetContent(b.x, b.y, BoxTopLeftCorner, nil, border)
screen.SetContent(b.x+b.width-1, b.y, BoxTopRightCorner, nil, border)
screen.SetContent(b.x, b.y+b.height-1, BoxBottomLeftCorner, nil, border)
screen.SetContent(b.x+b.width-1, b.y+b.height-1, BoxBottomRightCorner, nil, border)
screen.SetContent(b.x, b.y, topLeft, nil, border)
screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border)
screen.SetContent(b.x, b.y+b.height-1, bottomLeft, nil, border)
screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, border)
// Draw title.
if b.title != "" && b.width >= 4 {
@ -145,12 +168,6 @@ func (b *Box) SetBorderColor(color tcell.Color) *Box {
return b
}
// SetFocusedBorderColor sets the box's border color for when the box has focus.
func (b *Box) SetFocusedBorderColor(color tcell.Color) *Box {
b.focusedBorderColor = color
return b
}
// SetTitle sets the box's title.
func (b *Box) SetTitle(title string) *Box {
b.title = title
@ -172,3 +189,8 @@ func (b *Box) Focus(app *Application) {
func (b *Box) Blur() {
b.hasFocus = false
}
// HasFocus returns whether or not this primitive has focus.
func (b *Box) HasFocus() bool {
return b.hasFocus
}

View File

@ -6,7 +6,7 @@ import (
// Button is labeled box that triggers an action when selected.
type Button struct {
Box
*Box
// The text to be displayed before the input area.
label string
@ -32,7 +32,7 @@ type Button struct {
func NewButton(label string) *Button {
box := NewBox().SetBackgroundColor(tcell.ColorBlue)
return &Button{
Box: *box,
Box: box,
label: label,
labelColor: tcell.ColorWhite,
labelColorActivated: tcell.ColorBlue,
@ -93,7 +93,7 @@ func (b *Button) SetBlurFunc(handler func(key tcell.Key)) *Button {
func (b *Button) Draw(screen tcell.Screen) {
// Draw the box.
backgroundColor := b.backgroundColor
if b.hasFocus {
if b.focus.HasFocus() {
b.backgroundColor = b.backgroundColorActivated
}
b.Box.Draw(screen)
@ -107,12 +107,12 @@ func (b *Button) Draw(screen tcell.Screen) {
width -= 2
}
labelColor := b.labelColor
if b.hasFocus {
if b.focus.HasFocus() {
labelColor = b.labelColorActivated
}
Print(screen, b.label, x, y, width, AlignCenter, labelColor)
if b.hasFocus {
if b.focus.HasFocus() {
screen.HideCursor()
}
}

View File

@ -7,25 +7,38 @@ import (
func main() {
app := tview.NewApplication()
var list *tview.List
form := tview.NewFrame(tview.NewForm().
frame := 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)
AddButton("Cancel", nil).
AddButton("Go to list", func() { app.SetFocus(list) })).
AddText("Customer details", true, tview.AlignLeft, tcell.ColorRed).
AddText("Customer details", false, tview.AlignCenter, tcell.ColorRed)
frame.SetBorder(true)
box := tview.NewFlex(tview.FlexColumn, []tview.Primitive{
form,
list = tview.NewList().
AddItem("Edit a form", "You can do whatever you want", 'e').
AddItem("Quit the program", "Do it!", 0).
SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
if shortcut == 'e' {
app.SetFocus(frame)
}
})
list.SetBorder(true)
flex := tview.NewFlex(tview.FlexColumn, []tview.Primitive{
frame,
tview.NewFlex(tview.FlexRow, []tview.Primitive{
tview.NewBox().SetBorder(true).SetTitle("Second"),
list,
tview.NewBox().SetBorder(true).SetTitle("Third"),
}),
tview.NewBox().SetBorder(true).SetTitle("Fourth"),
})
box.AddItem(tview.NewBox().SetBorder(true).SetTitle("Fifth"), 20)
flex.AddItem(tview.NewBox().SetBorder(true).SetTitle("Fifth"), 20)
inputField := tview.NewInputField().
SetLabel("Type something: ").
@ -33,10 +46,10 @@ func main() {
SetAcceptanceFunc(tview.InputFieldFloat)
inputField.SetBorder(true).SetTitle("Type!")
final := tview.NewFlex(tview.FlexRow, []tview.Primitive{box})
final := tview.NewFlex(tview.FlexRow, []tview.Primitive{flex})
final.AddItem(inputField, 3)
app.SetRoot(final, true).SetFocus(form)
app.SetRoot(final, true).SetFocus(list)
if err := app.Run(); err != nil {
panic(err)

27
flex.go
View File

@ -8,8 +8,8 @@ const (
FlexColumn
)
// FlexItem holds layout options for one item.
type FlexItem struct {
// flexItem holds layout options for one item.
type flexItem struct {
Item Primitive // The item to be positioned.
FixedSize int // The item's fixed size which may not be changed, 0 if it has no fixed size.
}
@ -17,8 +17,8 @@ type FlexItem struct {
// Flex is a basic implementation of a flexbox layout.
type Flex struct {
x, y, width, height int // The size and position of this primitive.
Items []FlexItem // The items to be positioned.
Direction int // FlexRow or FlexColumn.
items []flexItem // The items to be positioned.
direction int // FlexRow or FlexColumn.
}
// NewFlex returns a new flexbox layout container with the given primitives.
@ -28,10 +28,10 @@ func NewFlex(direction int, items []Primitive) *Flex {
box := &Flex{
width: 15,
height: 10,
Direction: direction,
direction: direction,
}
for _, item := range items {
box.Items = append(box.Items, FlexItem{Item: item})
box.items = append(box.items, flexItem{Item: item})
}
return box
}
@ -39,7 +39,7 @@ func NewFlex(direction int, items []Primitive) *Flex {
// AddItem adds a new item to the container. fixedSize is a size that may not be
// changed. A value of 0 means that its size may be changed.
func (f *Flex) AddItem(item Primitive, fixedSize int) *Flex {
f.Items = append(f.Items, FlexItem{Item: item, FixedSize: fixedSize})
f.items = append(f.items, flexItem{Item: item, FixedSize: fixedSize})
return f
}
@ -50,10 +50,10 @@ func (f *Flex) Draw(screen tcell.Screen) {
// How much space can we distribute?
var variables int
distSize := f.width
if f.Direction == FlexRow {
if f.direction == FlexRow {
distSize = f.height
}
for _, item := range f.Items {
for _, item := range f.items {
if item.FixedSize > 0 {
distSize -= item.FixedSize
} else {
@ -63,17 +63,17 @@ func (f *Flex) Draw(screen tcell.Screen) {
// Calculate positions and draw items.
pos := f.x
if f.Direction == FlexRow {
if f.direction == FlexRow {
pos = f.y
}
for _, item := range f.Items {
for _, item := range f.items {
size := item.FixedSize
if size <= 0 {
size = distSize / variables
distSize -= size
variables--
}
if f.Direction == FlexColumn {
if f.direction == FlexColumn {
item.Item.SetRect(pos, f.y, size, f.height)
} else {
item.Item.SetRect(f.x, pos, f.width, size)
@ -105,6 +105,9 @@ func (f *Flex) InputHandler() func(event *tcell.EventKey) {
// Focus is called when this primitive receives focus.
func (f *Flex) Focus(app *Application) {
if len(f.items) > 0 {
app.SetFocus(f.items[0].Item)
}
}
// Blur is called when this primitive loses focus.

8
focusable.go Normal file
View File

@ -0,0 +1,8 @@
package tview
// Focusable provides a method which determines if a primitive has focus.
// Composed primitives may be focused based on the focused state of their
// contained primitives.
type Focusable interface {
HasFocus() bool
}

32
form.go
View File

@ -8,7 +8,7 @@ import (
// Form is a Box which contains multiple input fields, one per row.
type Form struct {
Box
*Box
// The items of the form (one row per item).
items []*InputField
@ -35,13 +35,19 @@ type Form struct {
// NewForm returns a new form.
func NewForm() *Form {
return &Form{
Box: *NewBox(),
box := NewBox()
f := &Form{
Box: box,
itemPadding: 1,
labelColor: tcell.ColorYellow,
fieldBackgroundColor: tcell.ColorBlue,
fieldTextColor: tcell.ColorWhite,
}
f.focus = f
return f
}
// SetItemPadding sets the number of empty rows between form items.
@ -156,8 +162,6 @@ func (f *Form) Draw(screen tcell.Screen) {
// 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)+len(f.buttons) == 0 {
return
}
@ -166,10 +170,9 @@ func (f *Form) Focus(app *Application) {
if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) {
f.focusedElement = 0
}
f.hasFocus = false
handler := func(key tcell.Key) {
switch key {
case tcell.KeyTab:
case tcell.KeyTab, tcell.KeyEnter:
f.focusedElement++
case tcell.KeyBacktab:
f.focusedElement--
@ -198,3 +201,18 @@ func (f *Form) Focus(app *Application) {
func (f *Form) InputHandler() func(event *tcell.EventKey) {
return func(event *tcell.EventKey) {}
}
// HasFocus returns whether or not this primitive has focus.
func (f *Form) HasFocus() bool {
for _, item := range f.items {
if item.focus.HasFocus() {
return true
}
}
for _, button := range f.buttons {
if button.focus.HasFocus() {
return true
}
}
return false
}

View File

@ -12,10 +12,10 @@ type frameText struct {
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.
// Frame is a wrapper which adds a border around another box. The top area
// (header) and the bottom area (footer) may also contain text.
type Frame struct {
Box
*Box
// The contained primitive.
primitive Primitive
@ -30,8 +30,10 @@ type Frame struct {
// 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(),
box := NewBox()
f := &Frame{
Box: box,
primitive: primitive,
top: 1,
bottom: 1,
@ -40,6 +42,10 @@ func NewFrame(primitive Primitive) *Frame {
left: 1,
right: 1,
}
f.focus = f
return f
}
// AddText adds text to the frame. Set "header" to true if the text is to appear
@ -129,7 +135,7 @@ func (f *Frame) Draw(screen tcell.Screen) {
// Set the size of the contained primitive.
if topMax > top {
top = topMax + 1 + f.header
top = topMax + f.header
}
if bottomMin < bottom {
bottom = bottomMin - f.footer
@ -153,3 +159,12 @@ func (f *Frame) InputHandler() func(event *tcell.EventKey) {
return func(event *tcell.EventKey) {
}
}
// HasFocus returns whether or not this primitive has focus.
func (f *Frame) HasFocus() bool {
focusable, ok := f.primitive.(Focusable)
if ok {
return focusable.HasFocus()
}
return false
}

View File

@ -52,7 +52,7 @@ func init() {
// InputField is a one-line box (three lines if there is a title) where the
// user can enter text.
type InputField struct {
Box
*Box
// The text that was entered.
text string
@ -85,7 +85,7 @@ type InputField struct {
// NewInputField returns a new input field.
func NewInputField() *InputField {
return &InputField{
Box: *NewBox(),
Box: NewBox(),
labelColor: tcell.ColorYellow,
fieldBackgroundColor: tcell.ColorBlue,
fieldTextColor: tcell.ColorWhite,
@ -206,7 +206,7 @@ func (i *InputField) Draw(screen tcell.Screen) {
}
// Set cursor.
if i.hasFocus {
if i.focus.HasFocus() {
i.setCursor(screen)
}
}

215
list.go Normal file
View File

@ -0,0 +1,215 @@
package tview
import (
"fmt"
"github.com/gdamore/tcell"
)
// listItem represents one item in a List.
type listItem struct {
MainText string // The main text of the list item.
SecondaryText string // A secondary text to be shown underneath the main text.
Shortcut rune // The key to select the list item directly, 0 if there is no shortcut.
}
// List displays rows of items, each of which can be selected.
type List struct {
*Box
// The items of the list.
items []*listItem
// The index of the currently selected item.
currentItem int
// Whether or not to show the secondary item texts.
showSecondaryText bool
// The item main text color. Selected items have their background and text
// color switched.
mainTextColor tcell.Color
// The item secondary text color.
secondaryTextColor tcell.Color
// The item shortcut text color.
shortcutColor tcell.Color
// An optional function which is called when a list item was selected.
selected func(index int, mainText, secondaryText string, shortcut rune)
}
// NewList returns a new form.
func NewList() *List {
return &List{
Box: NewBox(),
showSecondaryText: true,
mainTextColor: tcell.ColorWhite,
secondaryTextColor: tcell.ColorGreen,
shortcutColor: tcell.ColorYellow,
}
}
// SetMainTextColorColor sets the color of the items' main text.
func (l *List) SetMainTextColorColor(color tcell.Color) *List {
l.mainTextColor = color
return l
}
// SetSecondaryTextColorColor sets the color of the items' secondary text.
func (l *List) SetSecondaryTextColorColor(color tcell.Color) *List {
l.secondaryTextColor = color
return l
}
// SetShortcutColor sets the color of the items' shortcut.
func (l *List) SetShortcutColor(color tcell.Color) *List {
l.shortcutColor = color
return l
}
// ShowSecondaryText determines whether or not to show secondary item texts.
func (l *List) ShowSecondaryText(show bool) *List {
l.showSecondaryText = show
return l
}
// SetSelectedFunc sets the function which is called when the user selects a
// list item. The function receives the item's index in the list of items
// (starting with 0), its main text, secondary text, and its shortcut rune.
func (l *List) SetSelectedFunc(handler func(int, string, string, rune)) *List {
l.selected = handler
return l
}
// AddItem adds a new item to the list. An item has a main text which will be
// highlighted when selected. It also has a secondary text which is shown
// underneath the main text (if it is set to visible) but which may remain
// empty.
//
// The shortcut is a key binding. If the specified rune is entered, the item
// is selected immediately. Set to 0 for no binding.
func (l *List) AddItem(mainText, secondaryText string, shortcut rune) *List {
l.items = append(l.items, &listItem{
MainText: mainText,
SecondaryText: secondaryText,
Shortcut: shortcut,
})
return l
}
// Draw draws this primitive onto the screen.
func (l *List) Draw(screen tcell.Screen) {
l.Box.Draw(screen)
// Determine the dimensions.
x := l.x
y := l.y
width := l.width
bottomLimit := l.y + l.height
if l.border {
x++
y++
width -= 2
bottomLimit -= 2
}
// Do we show any shortcuts?
var showShortcuts bool
for _, item := range l.items {
if item.Shortcut != 0 {
showShortcuts = true
x += 4
width -= 4
break
}
}
// Draw the list items.
for index, item := range l.items {
if y >= bottomLimit {
break
}
// Shortcuts.
if showShortcuts && item.Shortcut != 0 {
Print(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-2, y, width+4, AlignRight, l.shortcutColor)
}
// Main text.
color := l.mainTextColor
if l.focus.HasFocus() && index == l.currentItem {
textLength := len([]rune(item.MainText))
style := tcell.StyleDefault.Background(l.mainTextColor)
for bx := 0; bx < textLength && bx < width; bx++ {
screen.SetContent(x+bx, y, ' ', nil, style)
}
color = l.backgroundColor
}
Print(screen, item.MainText, x, y, width, AlignLeft, color)
y++
if y >= bottomLimit {
break
}
// Secondary text.
if l.showSecondaryText {
Print(screen, item.SecondaryText, x, y, width, AlignLeft, l.secondaryTextColor)
y++
}
}
}
// InputHandler returns the handler for this primitive.
func (l *List) InputHandler() func(event *tcell.EventKey) {
return func(event *tcell.EventKey) {
switch key := event.Key(); key {
case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight:
l.currentItem++
case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft:
l.currentItem--
case tcell.KeyHome:
l.currentItem = 0
case tcell.KeyEnd:
l.currentItem = len(l.items) - 1
case tcell.KeyPgDn:
l.currentItem += 5
case tcell.KeyPgUp:
l.currentItem -= 5
case tcell.KeyEnter:
if l.selected != nil {
item := l.items[l.currentItem]
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
}
case tcell.KeyRune:
ch := event.Rune()
if ch != ' ' {
// It's not a space bar. Is it a shortcut?
var found bool
for index, item := range l.items {
if item.Shortcut == ch {
// We have a shortcut.
found = true
l.currentItem = index
break
}
}
if !found {
break
}
}
if l.selected != nil {
item := l.items[l.currentItem]
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
}
}
if l.currentItem < 0 {
l.currentItem = len(l.items) - 1
} else if l.currentItem >= len(l.items) {
l.currentItem = 0
}
}
}

11
util.go
View File

@ -1,6 +1,10 @@
package tview
import "github.com/gdamore/tcell"
import (
"math"
"github.com/gdamore/tcell"
)
// Text alignment within a box.
const (
@ -52,3 +56,8 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc
return len(runes)
}
// PrintSimple prints white text to the screen at the given position.
func PrintSimple(screen tcell.Screen, text string, x, y int) {
Print(screen, text, x, y, math.MaxInt64, AlignLeft, tcell.ColorWhite)
}