Added Checkbox, Modal, and Pages.

This commit is contained in:
Oliver 2017-12-20 20:54:49 +01:00
parent 2bd80aa513
commit dad7891c89
14 changed files with 772 additions and 85 deletions

View File

@ -168,3 +168,9 @@ func (a *Application) SetFocus(p Primitive) *Application {
return a
}
// GetFocus returns the primitive which has the current focus. If none has it,
// nil is returned.
func (a *Application) GetFocus() Primitive {
return a.focus
}

10
box.go
View File

@ -21,8 +21,9 @@ const (
BoxEllipsis = '\u2026'
)
// Box implements Rect with a background and optional elements such as a border
// and a title.
// Box implements Primitive with a background and optional elements such as a
// border and a title. Most subclasses keep their content contained in the box
// but don't necessarily have to.
type Box struct {
// The position of the rect.
x, y, width, height int
@ -194,3 +195,8 @@ func (b *Box) Blur() {
func (b *Box) HasFocus() bool {
return b.hasFocus
}
// GetFocusable returns the item's Focusable.
func (b *Box) GetFocusable() Focusable {
return b.focus
}

175
checkbox.go Normal file
View File

@ -0,0 +1,175 @@
package tview
import (
"github.com/gdamore/tcell"
)
// Checkbox is a one-line box (three lines if there is a title) where the
// user can enter text.
type Checkbox struct {
*Box
// Whether or not this box is checked.
checked bool
// The text to be displayed before the input area.
label string
// The label color.
labelColor tcell.Color
// The background color of the input area.
fieldBackgroundColor tcell.Color
// The text color of the input area.
fieldTextColor tcell.Color
// An optional function which is called when the user changes the checked
// state of this checkbox.
changed func(checked bool)
// An optional function which is called when the user indicated that they
// are done entering text. The key which was pressed is provided (tab,
// shift-tab, or escape).
done func(tcell.Key)
}
// NewCheckbox returns a new input field.
func NewCheckbox() *Checkbox {
return &Checkbox{
Box: NewBox(),
labelColor: tcell.ColorYellow,
fieldBackgroundColor: tcell.ColorBlue,
fieldTextColor: tcell.ColorWhite,
}
}
// SetChecked sets the state of the checkbox.
func (c *Checkbox) SetChecked(checked bool) *Checkbox {
c.checked = checked
return c
}
// SetLabel sets the text to be displayed before the input area.
func (c *Checkbox) SetLabel(label string) *Checkbox {
c.label = label
return c
}
// GetLabel returns the text to be displayed before the input area.
func (c *Checkbox) GetLabel() string {
return c.label
}
// SetLabelColor sets the color of the label.
func (c *Checkbox) SetLabelColor(color tcell.Color) *Checkbox {
c.labelColor = color
return c
}
// SetFieldBackgroundColor sets the background color of the input area.
func (c *Checkbox) SetFieldBackgroundColor(color tcell.Color) *Checkbox {
c.fieldBackgroundColor = color
return c
}
// SetFieldTextColor sets the text color of the input area.
func (c *Checkbox) SetFieldTextColor(color tcell.Color) *Checkbox {
c.fieldTextColor = color
return c
}
// SetFormAttributes sets attributes shared by all form items.
func (c *Checkbox) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
c.label = label
c.labelColor = labelColor
c.backgroundColor = bgColor
c.fieldTextColor = fieldTextColor
c.fieldBackgroundColor = fieldBgColor
return c
}
// SetChangedFunc sets a handler which is called when the checked state of this
// checkbox was changed by the user. The handler function receives the new
// state.
func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
c.changed = handler
return c
}
// SetDoneFunc sets a handler which is called when the user is done entering
// text. The callback function is provided with the key that was pressed, which
// is one of the following:
//
// - KeyEscape: Abort text input.
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (c *Checkbox) SetDoneFunc(handler func(key tcell.Key)) *Checkbox {
c.done = handler
return c
}
// SetFinishedFunc calls SetDoneFunc().
func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
return c.SetDoneFunc(handler)
}
// Draw draws this primitive onto the screen.
func (c *Checkbox) Draw(screen tcell.Screen) {
c.Box.Draw(screen)
// Prepare
x := c.x
y := c.y
rightLimit := x + c.width
height := c.height
if c.border {
x++
y++
rightLimit -= 2
height -= 2
}
if height < 1 || rightLimit <= x {
return
}
// Draw label.
x += Print(screen, c.label, x, y, rightLimit-x, AlignLeft, c.labelColor)
// Draw checkbox.
fieldStyle := tcell.StyleDefault.Background(c.fieldBackgroundColor).Foreground(c.fieldTextColor)
if c.focus.HasFocus() {
fieldStyle = fieldStyle.Background(c.fieldTextColor).Foreground(c.fieldBackgroundColor)
}
checkedRune := 'X'
if !c.checked {
checkedRune = ' '
}
screen.SetContent(x, y, checkedRune, nil, fieldStyle)
// Hide cursor.
if c.focus.HasFocus() {
screen.HideCursor()
}
}
// InputHandler returns the handler for this primitive.
func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return func(event *tcell.EventKey, setFocus func(p Primitive)) {
// Process key event.
switch key := event.Key(); key {
case tcell.KeyRune, tcell.KeyEnter: // Check.
if key == tcell.KeyRune && event.Rune() != ' ' {
break
}
c.checked = !c.checked
if c.changed != nil {
c.changed(c.checked)
}
case tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
if c.done != nil {
c.done(key)
}
}
}
}

View File

@ -7,9 +7,10 @@ import (
func main() {
app := tview.NewApplication()
pages := tview.NewPages()
var list *tview.List
frame := tview.NewFrame(tview.NewForm().
form := tview.NewForm().
AddInputField("First name", "", 20, nil).
AddInputField("Last name", "", 20, nil).
AddInputField("Age", "", 4, nil).
@ -18,27 +19,43 @@ func main() {
app.Stop()
}
}).
AddButton("Save", func() { app.Stop() }).
AddCheckbox("Check", false, nil).
AddButton("Save", func() {
previous := app.GetFocus()
modal := tview.NewModal().
SetText("Would you really like to save this customer to the database?").
AddButtons([]string{"Save", "Cancel"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
pages.RemovePage("confirm")
app.SetFocus(previous)
app.Draw()
})
pages.AddPage("confirm", modal, true)
app.SetFocus(modal)
app.Draw()
}).
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).SetTitle("Customers")
AddButton("Go to list", func() { app.SetFocus(list) }).
SetCancelFunc(func() {
app.Stop()
})
form.SetTitle("Customer").SetBorder(true)
list = tview.NewList().
AddItem("Edit a form", "You can do whatever you want", 'e', func() { app.SetFocus(frame) }).
AddItem("Edit a form", "You can do whatever you want", 'e', func() { app.SetFocus(form) }).
AddItem("Quit the program", "Do it!", 0, func() { app.Stop() })
list.SetBorder(true)
flex := tview.NewFlex(tview.FlexColumn, []tview.Primitive{
frame,
tview.NewFlex(tview.FlexRow, []tview.Primitive{
list,
tview.NewBox().SetBorder(true).SetTitle("Third"),
}),
tview.NewBox().SetBorder(true).SetTitle("Fourth"),
})
flex.AddItem(tview.NewBox().SetBorder(true).SetTitle("Fifth"), 20)
frame := tview.NewFrame(list).AddText("Choose!", true, tview.AlignCenter, tcell.ColorRed)
frame.SetBorder(true)
flex := tview.NewFlex().
AddItem(form, 0).
AddItem(tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(frame, 0).
AddItem(tview.NewBox().SetBorder(true).SetTitle("Third"), 0), 0).
AddItem(tview.NewBox().SetBorder(true).SetTitle("Fourth"), 0).
AddItem(tview.NewBox().SetBorder(true).SetTitle("Fifth"), 20)
inputField := tview.NewInputField().
SetLabel("Type something: ").
@ -46,10 +63,15 @@ func main() {
SetAcceptanceFunc(tview.InputFieldFloat)
inputField.SetBorder(true).SetTitle("Type!")
final := tview.NewFlex(tview.FlexRow, []tview.Primitive{flex})
final.AddItem(inputField, 3)
final := tview.NewFlex().
SetFullScreen(true).
SetDirection(tview.FlexRow).
AddItem(flex, 0).
AddItem(inputField, 3)
app.SetRoot(final, true).SetFocus(list)
pages.AddPage("flex", final, true)
app.SetRoot(pages, false).SetFocus(list)
if err := app.Run(); err != nil {
panic(err)

2
doc.go
View File

@ -1,5 +1,7 @@
/*
Package tview implements primitives for terminal based applications. It uses
github.com/gdamore/tcell.
No mouse input (yet).
*/
package tview

View File

@ -169,11 +169,6 @@ func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
return d.SetDoneFunc(handler)
}
// GetFocusable returns the item's Focusable.
func (d *DropDown) GetFocusable() Focusable {
return d.focus
}
// Draw draws this primitive onto the screen.
func (d *DropDown) Draw(screen tcell.Screen) {
d.Box.Draw(screen)

87
flex.go
View File

@ -16,24 +16,43 @@ 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.
*Box
// The items to be positioned.
items []flexItem
// FlexRow or FlexColumn.
direction int
// If set to true, will use the entire screen as its available space instead
// its box dimensions.
fullScreen bool
}
// NewFlex returns a new flexbox layout container with the given primitives.
// The items all have no fixed size. If more control is needed, call AddItem().
// The direction argument must be FlexRow or FlexColumn.
func NewFlex(direction int, items []Primitive) *Flex {
box := &Flex{
width: 15,
height: 10,
direction: direction,
func NewFlex() *Flex {
f := &Flex{
Box: NewBox(),
direction: FlexColumn,
}
for _, item := range items {
box.items = append(box.items, flexItem{Item: item})
}
return box
f.focus = f
return f
}
// SetDirection sets the direction in which the contained primitives are
// distributed. This can be either FlexColumn (default) or FlexRow.
func (f *Flex) SetDirection(direction int) *Flex {
f.direction = direction
return f
}
// SetFullScreen sets the flag which, when true, causes the flex layout to use
// the entire screen space instead of whatever size it is currently assigned to.
func (f *Flex) SetFullScreen(fullScreen bool) *Flex {
f.fullScreen = fullScreen
return f
}
// AddItem adds a new item to the container. fixedSize is a size that may not be
@ -47,6 +66,15 @@ func (f *Flex) AddItem(item Primitive, fixedSize int) *Flex {
func (f *Flex) Draw(screen tcell.Screen) {
// Calculate size and position of the items.
// Do we use the entire screen?
if f.fullScreen {
f.x = 0
f.y = 0
width, height := screen.Size()
f.width = width
f.height = height
}
// How much space can we distribute?
var variables int
distSize := f.width
@ -80,29 +108,14 @@ func (f *Flex) Draw(screen tcell.Screen) {
}
pos += size
item.Item.Draw(screen)
if item.Item.GetFocusable().HasFocus() {
defer item.Item.Draw(screen)
} else {
item.Item.Draw(screen)
}
}
}
// GetRect returns the current position of the primitive, x, y, width, and
// height.
func (f *Flex) GetRect() (int, int, int, int) {
return f.x, f.y, f.width, f.height
}
// SetRect sets a new position of the primitive.
func (f *Flex) SetRect(x, y, width, height int) {
f.x = x
f.y = y
f.width = width
f.height = height
}
// InputHandler returns nil.
func (f *Flex) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return nil
}
// Focus is called when this primitive receives focus.
func (f *Flex) Focus(delegate func(p Primitive)) {
if len(f.items) > 0 {
@ -110,6 +123,12 @@ func (f *Flex) Focus(delegate func(p Primitive)) {
}
}
// Blur is called when this primitive loses focus.
func (f *Flex) Blur() {
// HasFocus returns whether or not this primitive has focus.
func (f *Flex) HasFocus() bool {
for _, item := range f.items {
if item.Item.GetFocusable().HasFocus() {
return true
}
}
return false
}

129
form.go
View File

@ -22,9 +22,6 @@ type FormItem interface {
// Enter key (we're done), the Escape key (cancel input), the Tab key (move to
// next field), and the Backtab key (move to previous field).
SetFinishedFunc(handler func(key tcell.Key)) FormItem
// GetFocusable returns the item's Focusable.
GetFocusable() Focusable
}
// Form is a Box which contains multiple input fields, one per row.
@ -37,6 +34,12 @@ type Form struct {
// The buttons of the form.
buttons []*Button
// The alignment of the buttons.
buttonsAlign int
// Border padding.
paddingTop, paddingBottom, paddingLeft, paddingRight int
// The number of empty rows between items.
itemPadding int
@ -52,6 +55,15 @@ type Form struct {
// The text color of the input area.
fieldTextColor tcell.Color
// The background color of the buttons.
buttonBackgroundColor tcell.Color
// The color of the button text.
buttonTextColor tcell.Color
// An optional function which is called when the user hits Escape.
cancel func()
}
// NewForm returns a new form.
@ -59,11 +71,17 @@ func NewForm() *Form {
box := NewBox()
f := &Form{
Box: box,
itemPadding: 1,
labelColor: tcell.ColorYellow,
fieldBackgroundColor: tcell.ColorBlue,
fieldTextColor: tcell.ColorWhite,
Box: box,
itemPadding: 1,
paddingTop: 1,
paddingBottom: 1,
paddingLeft: 1,
paddingRight: 1,
labelColor: tcell.ColorYellow,
fieldBackgroundColor: tcell.ColorBlue,
fieldTextColor: tcell.ColorWhite,
buttonBackgroundColor: tcell.ColorBlue,
buttonTextColor: tcell.ColorWhite,
}
f.focus = f
@ -71,6 +89,12 @@ func NewForm() *Form {
return f
}
// SetPadding sets the size of the borders around the form items.
func (f *Form) SetPadding(top, bottom, left, right int) *Form {
f.paddingTop, f.paddingBottom, f.paddingLeft, f.paddingRight = top, bottom, left, right
return f
}
// SetItemPadding sets the number of empty rows between form items.
func (f *Form) SetItemPadding(padding int) *Form {
f.itemPadding = padding
@ -95,6 +119,25 @@ func (f *Form) SetFieldTextColor(color tcell.Color) *Form {
return f
}
// SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft
// (the default), AlignCenter, and AlignRight.
func (f *Form) SetButtonsAlign(align int) *Form {
f.buttonsAlign = align
return f
}
// SetButtonBackgroundColor sets the background color of the buttons.
func (f *Form) SetButtonBackgroundColor(color tcell.Color) *Form {
f.buttonBackgroundColor = color
return f
}
// SetButtonTextColor sets the color of the button texts.
func (f *Form) SetButtonTextColor(color tcell.Color) *Form {
f.buttonTextColor = color
return f
}
// AddInputField adds an input field to the form. It has a label, an optional
// initial value, a field length (a value of 0 extends it as far as possible),
// and an optional accept function to validate the item's value (set to nil to
@ -119,6 +162,17 @@ func (f *Form) AddDropDown(label string, options []string, initialOption int, se
return f
}
// AddCheckbox adds a checkbox to the form. It has a label, an initial state,
// and an (optional) callback function which is invoked when the state of the
// checkbox was changed by the user.
func (f *Form) AddCheckbox(label string, checked bool, changed func(checked bool)) *Form {
f.items = append(f.items, NewCheckbox().
SetLabel(label).
SetChecked(checked).
SetChangedFunc(changed))
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 {
@ -126,6 +180,13 @@ func (f *Form) AddButton(label string, selected func()) *Form {
return f
}
// SetCancelFunc sets a handler which is called when the user hits the Escape
// key.
func (f *Form) SetCancelFunc(callback func()) *Form {
f.cancel = callback
return f
}
// Draw draws this primitive onto the screen.
func (f *Form) Draw(screen tcell.Screen) {
f.Box.Draw(screen)
@ -134,13 +195,18 @@ func (f *Form) Draw(screen tcell.Screen) {
x := f.x
y := f.y
width := f.width
bottomLimit := f.y + f.height
height := f.height
if f.border {
x++
y++
width -= 2
bottomLimit -= 2
height -= 2
}
x += f.paddingLeft
y += f.paddingTop
width -= f.paddingLeft + f.paddingRight
height -= f.paddingTop + f.paddingBottom
bottomLimit := y + height
rightLimit := x + width
// Find the longest label.
@ -174,23 +240,46 @@ func (f *Form) Draw(screen tcell.Screen) {
y += 1 + f.itemPadding
}
// Draw the buttons.
// How wide are the buttons?
buttonWidths := make([]int, len(f.buttons))
buttonsWidth := 0
for index, button := range f.buttons {
width := len([]rune(button.GetLabel())) + 4
buttonWidths[index] = width
buttonsWidth += width + 2
}
buttonsWidth -= 2
// Where do we place them?
if x+buttonsWidth < rightLimit {
if f.buttonsAlign == AlignRight {
x = rightLimit - buttonsWidth
} else if f.buttonsAlign == AlignCenter {
x = (x + rightLimit - buttonsWidth) / 2
}
}
// Draw them.
if f.itemPadding == 0 {
y++
}
if y >= bottomLimit {
return // Stop here.
}
for _, button := range f.buttons {
for index, button := range f.buttons {
space := rightLimit - x
if space < 1 {
return // No space for this button anymore.
break // No space for this button anymore.
}
buttonWidth := len([]rune(button.GetLabel())) + 4
buttonWidth := buttonWidths[index]
if buttonWidth > space {
buttonWidth = space
}
button.SetRect(x, y, buttonWidth, 1)
button.SetLabelColor(f.buttonTextColor).
SetLabelColorActivated(f.buttonBackgroundColor).
SetBackgroundColorActivated(f.buttonTextColor).
SetBackgroundColor(f.buttonBackgroundColor).
SetRect(x, y, buttonWidth, 1)
button.Draw(screen)
x += buttonWidth + 2
@ -211,15 +300,21 @@ func (f *Form) Focus(delegate func(p Primitive)) {
switch key {
case tcell.KeyTab, tcell.KeyEnter:
f.focusedElement++
f.Focus(delegate)
case tcell.KeyBacktab:
f.focusedElement--
if f.focusedElement < 0 {
f.focusedElement = len(f.items) + len(f.buttons) - 1
}
f.Focus(delegate)
case tcell.KeyEscape:
f.focusedElement = 0
if f.cancel != nil {
f.cancel()
} else {
f.focusedElement = 0
f.Focus(delegate)
}
}
f.Focus(delegate)
}
if f.focusedElement < len(f.items) {

View File

@ -64,6 +64,12 @@ func (f *Frame) AddText(text string, header bool, align int, color tcell.Color)
return f
}
// ClearText removes all text from the frame.
func (f *Frame) ClearText() *Frame {
f.text = nil
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).
@ -140,10 +146,10 @@ func (f *Frame) Draw(screen tcell.Screen) {
if bottomMin < bottom {
bottom = bottomMin - f.footer
}
if top >= bottom {
if top > bottom {
return // No space for the primitive.
}
f.primitive.SetRect(left, top, right+1-left, bottom-top)
f.primitive.SetRect(left, top, right+1-left, bottom+1-top)
// Finally, draw the contained primitive.
f.primitive.Draw(screen)

View File

@ -177,11 +177,6 @@ func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
return i.SetDoneFunc(handler)
}
// GetFocusable returns the item's Focusable.
func (i *InputField) GetFocusable() Focusable {
return i.focus
}
// Draw draws this primitive onto the screen.
func (i *InputField) Draw(screen tcell.Screen) {
i.Box.Draw(screen)

121
modal.go Normal file
View File

@ -0,0 +1,121 @@
package tview
import (
"github.com/gdamore/tcell"
)
// Modal is a centered message window used to inform the user or prompt them
// for an immediate decision. It needs to have at least one button (added via
// AddButtons()) or it will never disappear.
type Modal struct {
*Frame
// The form embedded in the modal's frame.
form *Form
// The message text (original, not word-wrapped).
text string
// The text color.
textColor tcell.Color
// The optional callback for when the user clicked one of the buttons. It
// receives the index of the clicked button and the button's label.
done func(buttonIndex int, buttonLabel string)
}
// NewModal returns a new modal message window.
func NewModal() *Modal {
m := &Modal{
textColor: tcell.ColorWhite,
}
m.form = NewForm().
SetPadding(0, 0, 0, 0).
SetButtonsAlign(AlignCenter).
SetButtonBackgroundColor(tcell.ColorBlack).
SetButtonTextColor(tcell.ColorWhite)
m.form.SetBackgroundColor(tcell.ColorBlue)
m.Frame = NewFrame(m.form)
m.Box.SetBorder(true).SetBackgroundColor(tcell.ColorBlue)
return m
}
// SetTextColor sets the color of the message text.
func (m *Modal) SetTextColor(color tcell.Color) *Modal {
m.textColor = color
return m
}
// SetDoneFunc sets a handler which is called when one of the buttons was
// pressed. It receives the index of the button as well as its label text. The
// handler is also called when the user presses the Escape key. The index will
// then be negative and the label text an emptry string.
func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) *Modal {
m.done = handler
return m
}
// SetText sets the message text of the window. The text may contain line
// breaks. Note that words are wrapped, too, based on the final size of the
// window.
func (m *Modal) SetText(text string) *Modal {
m.text = text
return m
}
// AddButtons adds buttons to the window. There must be at least one button and
// a "done" handler so the window can be closed again.
func (m *Modal) AddButtons(labels []string) *Modal {
for index, label := range labels {
func(i int, l string) {
m.form.AddButton(label, func() {
if m.done != nil {
m.done(i, l)
}
})
}(index, label)
}
return m
}
// Focus is called when this primitive receives focus.
func (m *Modal) Focus(delegate func(p Primitive)) {
delegate(m.form)
}
// HasFocus returns whether or not this primitive has focus.
func (m *Modal) HasFocus() bool {
return m.form.HasFocus()
}
// Draw draws this primitive onto the screen.
func (m *Modal) Draw(screen tcell.Screen) {
// Calculate the width of this modal.
buttonsWidth := 0
for _, button := range m.form.buttons {
buttonsWidth += len([]rune(button.label)) + 4 + 2
}
buttonsWidth -= 2
screenWidth, screenHeight := screen.Size()
width := screenWidth / 3
if width < buttonsWidth {
width = buttonsWidth
}
// width is now without the box border.
// Reset the text and find out how wide it is.
m.Frame.ClearText()
lines := WordWrap(m.text, width)
for _, line := range lines {
m.Frame.AddText(line, true, AlignCenter, m.textColor)
}
// Set the modal's position and size.
height := len(lines) + 6
x := (screenWidth - width) / 2
y := (screenHeight - height) / 2
m.SetRect(x, y, width, height)
// Draw the frame.
m.Frame.Draw(screen)
}

169
pages.go Normal file
View File

@ -0,0 +1,169 @@
package tview
import "github.com/gdamore/tcell"
// page represents one page of a Pages object.
type page struct {
Name string // The page's name.
Item Primitive // The page's primitive.
Visible bool // Whether or not this page is visible.
}
// Pages is a container for other primitives often used as the application's
// root primitive. It allows to easily switch the visibility of the contained
// primitives.
type Pages struct {
*Box
// The contained pages.
pages []*page
// An optional handler which is called whenever the visibility or the order of
// pages changes.
changed func()
}
// NewPages returns a new Pages object.
func NewPages() *Pages {
p := &Pages{
Box: NewBox(),
}
p.focus = p
return p
}
// SetChangedFunc sets a handler which is called whenever the visibility or the
// order of any visible pages changes. This can be used to redraw the pages.
func (p *Pages) SetChangedFunc(handler func()) *Pages {
p.changed = handler
return p
}
// AddPage adds a new page with the given name and primitive. Leaving the name
// empty or using the same name for multiple items may cause conflicts in other
// functions.
//
// Visible pages will be drawn in the order they were added (unless that order
// was changed in one of the other functions).
func (p *Pages) AddPage(name string, item Primitive, visible bool) *Pages {
p.pages = append(p.pages, &page{Item: item, Name: name, Visible: true})
if p.changed != nil {
p.changed()
}
return p
}
// RemovePage removes the page with the given name.
func (p *Pages) RemovePage(name string) *Pages {
for index, page := range p.pages {
if page.Name == name {
p.pages = append(p.pages[:index], p.pages[index+1:]...)
if page.Visible && p.changed != nil {
p.changed()
}
break
}
}
return p
}
// ShowPage sets a page's visibility to "true" (in addition to any other pages
// which are already visible).
func (p *Pages) ShowPage(name string) *Pages {
for _, page := range p.pages {
if page.Name == name {
page.Visible = true
if p.changed != nil {
p.changed()
}
break
}
}
return p
}
// HidePage sets a page's visibility to "false".
func (p *Pages) HidePage(name string) *Pages {
for _, page := range p.pages {
if page.Name == name {
page.Visible = false
if p.changed != nil {
p.changed()
}
break
}
}
return p
}
// SwitchToPage sets a page's visibility to "true" and all other pages'
// visibility to "false".
func (p *Pages) SwitchToPage(name string) *Pages {
for _, page := range p.pages {
if page.Name == name {
page.Visible = true
} else {
page.Visible = false
}
}
if p.changed != nil {
p.changed()
}
return p
}
// SendToFront changes the order of the pages such that the page with the given
// name comes last, causing it to be drawn last with the next update (if
// visible).
func (p *Pages) SendToFront(name string) *Pages {
for index, page := range p.pages {
if page.Name == name {
if index < len(p.pages)-1 {
p.pages = append(append(p.pages[:index], p.pages[index+1:]...), page)
}
if page.Visible && p.changed != nil {
p.changed()
}
break
}
}
return p
}
// SendToBack changes the order of the pages such that the page with the given
// name comes first, causing it to be drawn first with the next update (if
// visible).
func (p *Pages) SendToBack(name string) *Pages {
for index, pg := range p.pages {
if pg.Name == name {
if index > 0 {
p.pages = append(append([]*page{pg}, p.pages[:index]...), p.pages[index+1:]...)
}
if pg.Visible && p.changed != nil {
p.changed()
}
break
}
}
return p
}
// HasFocus returns whether or not this primitive has focus.
func (p *Pages) HasFocus() bool {
for _, page := range p.pages {
if page.Item.GetFocusable().HasFocus() {
return true
}
}
return false
}
// Draw draws this primitive onto the screen.
func (p *Pages) Draw(screen tcell.Screen) {
for _, page := range p.pages {
if !page.Visible {
continue
}
page.Item.Draw(screen)
}
}

View File

@ -36,4 +36,7 @@ type Primitive interface {
// Blur is called by the application when the primitive loses focus.
Blur()
// GetFocusable returns the item's Focusable.
GetFocusable() Focusable
}

73
util.go
View File

@ -2,6 +2,7 @@ package tview
import (
"math"
"strings"
"github.com/gdamore/tcell"
)
@ -61,3 +62,75 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc
func PrintSimple(screen tcell.Screen, text string, x, y int) {
Print(screen, text, x, y, math.MaxInt64, AlignLeft, tcell.ColorWhite)
}
// WordWrap splits a text such that each resulting line does not exceed the
// given width. Possible split points are after commas, dots, dashes, and any
// whitespace. Whitespace at split points will be dropped.
//
// Text is always split at newline characters ('\n').
func WordWrap(text string, width int) (lines []string) {
x := 0
start := 0
candidate := -1 // -1 = no candidate yet.
startAfterCandidate := 0
countAfterCandidate := 0
var evaluatingCandidate bool
text = strings.TrimSpace(text)
for pos, ch := range text {
if !evaluatingCandidate && x >= width {
// We've exceeded the width, we must split.
if candidate >= 0 {
lines = append(lines, text[start:candidate])
start = startAfterCandidate
x = countAfterCandidate
} else {
lines = append(lines, text[start:pos])
start = pos
x = 0
}
candidate = -1
evaluatingCandidate = false
}
switch ch {
// We have a candidate.
case ',', '.', '-':
if x > 0 {
candidate = pos + 1
evaluatingCandidate = true
}
// If we've had a candidate, skip whitespace. If not, we have a candidate.
case ' ', '\t':
if x > 0 && !evaluatingCandidate {
candidate = pos
evaluatingCandidate = true
}
// Split in any case.
case '\n':
lines = append(lines, text[start:pos])
start = pos + 1
evaluatingCandidate = false
countAfterCandidate = 0
x = 0
continue
// If we've had a candidate, we have a new start.
default:
if evaluatingCandidate {
startAfterCandidate = pos
evaluatingCandidate = false
countAfterCandidate = 0
}
}
x++
countAfterCandidate++
}
// Process remaining text.
text = strings.TrimSpace(text[start:])
if len(text) > 0 {
lines = append(lines, text)
}
return
}