diff --git a/box.go b/box.go index 7057561..baaa839 100644 --- a/box.go +++ b/box.go @@ -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 +} diff --git a/button.go b/button.go index 00f3bd1..1cba3ee 100644 --- a/button.go +++ b/button.go @@ -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() } } diff --git a/demos/basic.go b/demos/basic.go index 5d75982..d782a08 100644 --- a/demos/basic.go +++ b/demos/basic.go @@ -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) diff --git a/flex.go b/flex.go index 3302c05..c763ed6 100644 --- a/flex.go +++ b/flex.go @@ -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. diff --git a/focusable.go b/focusable.go new file mode 100644 index 0000000..99fdaaf --- /dev/null +++ b/focusable.go @@ -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 +} diff --git a/form.go b/form.go index 4f2b131..e47c01a 100644 --- a/form.go +++ b/form.go @@ -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 +} diff --git a/frame.go b/frame.go index ac86c7d..1656c80 100644 --- a/frame.go +++ b/frame.go @@ -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 +} diff --git a/inputfield.go b/inputfield.go index 2ac5bd0..3db8c7d 100644 --- a/inputfield.go +++ b/inputfield.go @@ -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) } } diff --git a/list.go b/list.go new file mode 100644 index 0000000..198d317 --- /dev/null +++ b/list.go @@ -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 + } + } +} diff --git a/util.go b/util.go index 2b0aa96..5d53833 100644 --- a/util.go +++ b/util.go @@ -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) +}