From dad7891c89046cd2a00547b01c2b91cf9e249fb6 Mon Sep 17 00:00:00 2001 From: Oliver <480930+rivo@users.noreply.github.com> Date: Wed, 20 Dec 2017 20:54:49 +0100 Subject: [PATCH] Added Checkbox, Modal, and Pages. --- Application.go | 6 ++ box.go | 10 ++- checkbox.go | 175 +++++++++++++++++++++++++++++++++++++++++++++++++ demos/basic.go | 62 ++++++++++++------ doc.go | 2 + dropdown.go | 5 -- flex.go | 87 ++++++++++++++---------- form.go | 129 +++++++++++++++++++++++++++++++----- frame.go | 10 ++- inputfield.go | 5 -- modal.go | 121 ++++++++++++++++++++++++++++++++++ pages.go | 169 +++++++++++++++++++++++++++++++++++++++++++++++ primitive.go | 3 + util.go | 73 +++++++++++++++++++++ 14 files changed, 772 insertions(+), 85 deletions(-) create mode 100644 checkbox.go create mode 100644 modal.go create mode 100644 pages.go diff --git a/Application.go b/Application.go index a304eef..5341599 100644 --- a/Application.go +++ b/Application.go @@ -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 +} diff --git a/box.go b/box.go index a26ee8e..aa03f33 100644 --- a/box.go +++ b/box.go @@ -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 +} diff --git a/checkbox.go b/checkbox.go new file mode 100644 index 0000000..958fb08 --- /dev/null +++ b/checkbox.go @@ -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) + } + } + } +} diff --git a/demos/basic.go b/demos/basic.go index d49802b..d0a5d1b 100644 --- a/demos/basic.go +++ b/demos/basic.go @@ -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) diff --git a/doc.go b/doc.go index a13ef9d..bcb1106 100644 --- a/doc.go +++ b/doc.go @@ -1,5 +1,7 @@ /* Package tview implements primitives for terminal based applications. It uses github.com/gdamore/tcell. + +No mouse input (yet). */ package tview diff --git a/dropdown.go b/dropdown.go index 6aef40e..2d72e07 100644 --- a/dropdown.go +++ b/dropdown.go @@ -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) diff --git a/flex.go b/flex.go index 924ff3c..950d3b9 100644 --- a/flex.go +++ b/flex.go @@ -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 } diff --git a/form.go b/form.go index d47f962..b6bbe92 100644 --- a/form.go +++ b/form.go @@ -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) { diff --git a/frame.go b/frame.go index 05924ed..1de7534 100644 --- a/frame.go +++ b/frame.go @@ -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) diff --git a/inputfield.go b/inputfield.go index 0e0ace5..3fb310d 100644 --- a/inputfield.go +++ b/inputfield.go @@ -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) diff --git a/modal.go b/modal.go new file mode 100644 index 0000000..efc0fe1 --- /dev/null +++ b/modal.go @@ -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) +} diff --git a/pages.go b/pages.go new file mode 100644 index 0000000..2434460 --- /dev/null +++ b/pages.go @@ -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) + } +} diff --git a/primitive.go b/primitive.go index d66a4fb..c0b0e1a 100644 --- a/primitive.go +++ b/primitive.go @@ -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 } diff --git a/util.go b/util.go index 5d53833..68a3068 100644 --- a/util.go +++ b/util.go @@ -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 +}