diff --git a/button.go b/button.go new file mode 100644 index 0000000..00f3bd1 --- /dev/null +++ b/button.go @@ -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) + } + } + } +} diff --git a/demos/basic.go b/demos/basic.go index 7ea6e28..5d75982 100644 --- a/demos/basic.go +++ b/demos/basic.go @@ -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 { diff --git a/form.go b/form.go index b362636..4f2b131 100644 --- a/form.go +++ b/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) {} } diff --git a/frame.go b/frame.go new file mode 100644 index 0000000..ac86c7d --- /dev/null +++ b/frame.go @@ -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) { + } +} diff --git a/inputfield.go b/inputfield.go index cb10945..2ac5bd0 100644 --- a/inputfield.go +++ b/inputfield.go @@ -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) } diff --git a/util.go b/util.go new file mode 100644 index 0000000..2b0aa96 --- /dev/null +++ b/util.go @@ -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) +}