Browse Source

Merge branch 'feat/alot-of-features' into 'master'

Revamp FormItem styling, add arrow symbol to DropDown, add focus-driven style
options, add InputField autocomplete style options, provide DropDownOption in
DropDown handlers and provide ListItem in List handlers.

See merge request tslocum/cview!5
tablepad
Trevor Slocum 2 years ago
parent
commit
1cfb3711cf
  1. 7
      .gitlab-ci.yml
  2. 47
      Makefile
  3. 55
      box.go
  4. 32
      button.go
  5. 120
      checkbox.go
  6. 10
      contextmenu.go
  7. 7
      demos/dropdown/main.go
  8. 6
      demos/form/main.go
  9. 4
      demos/inputfield/autocomplete/main.go
  10. 8
      demos/inputfield/autocompleteasync/main.go
  11. 12
      demos/list/main.go
  12. 2
      demos/presentation/form.go
  13. 12
      demos/presentation/introduction.go
  14. 16
      demos/presentation/table.go
  15. 6
      demos/unicode/main.go
  16. 352
      dropdown.go
  17. 23
      flex.go
  18. 299
      form.go
  19. 303
      inputfield.go
  20. 216
      list.go
  21. 16
      list_test.go
  22. 14
      styles.go
  23. 34
      treeview.go

7
.gitlab-ci.yml

@ -7,15 +7,14 @@ stages:
fmt:
stage: validate
script:
- gofmt -l -s -e .
- exit $(gofmt -l -s -e . | wc -l)
- make check-fmt
vet:
stage: validate
script:
- go vet -composites=false ./...
- make vet
test:
stage: validate
script:
- go test -race -v ./...
- make test

47
Makefile

@ -0,0 +1,47 @@
# kernel-style V=1 build verbosity
ifeq ("$(origin V)", "command line")
BUILD_VERBOSE = $(V)
endif
ifeq ($(BUILD_VERBOSE),1)
Q =
else
Q = @
endif
define go_get
$(Q)command -v $(1) > /dev/null || GO111MODULE=off go get $(2)
endef
export CGO_ENABLED := 1
.DEFAULT_GOAL := help
.PHONY: validate
validate: check-fmt test vet ## Validates the go code format, runs tests and executes vet.
.PHONY: test
test: ## Run tests
$(Q)echo "running tests..."
$(Q)go test -race -v ./...
.PHONY: vet
vet: ## Run go vet
$(Q)echo "running go vet..."
$(Q)go vet -composites=false ./...
.PHONY: check-fmt
check-fmt: ## Check go format
$(Q)echo "checking format..."
@gofmt_out=$$(gofmt -d -e . 2>&1) && [ -z "$${gofmt_out}" ] || (echo "$${gofmt_out}" 1>&2; exit 1)
.PHONY: fmt
fmt: ## Formats the go code
$(Q)echo "formatting go code..."
$(Q)set -e
$(call go_get,goimports,golang.org/x/tools/cmd/goimports)
$(Q)goimports -w .
.PHONY: help
help: ## Shows this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

55
box.go

@ -19,6 +19,9 @@ type Box struct {
// Border padding.
paddingTop, paddingBottom, paddingLeft, paddingRight int
// The border color when the box has focus.
borderColorFocused tcell.Color
// The box's background color.
backgroundColor tcell.Color
@ -73,19 +76,27 @@ type Box struct {
// NewBox returns a Box without a border.
func NewBox() *Box {
b := &Box{
width: 15,
height: 10,
innerX: -1, // Mark as uninitialized.
backgroundColor: Styles.PrimitiveBackgroundColor,
borderColor: Styles.BorderColor,
titleColor: Styles.TitleColor,
titleAlign: AlignCenter,
showFocus: true,
width: 15,
height: 10,
innerX: -1, // Mark as uninitialized.
backgroundColor: Styles.PrimitiveBackgroundColor,
borderColor: Styles.BorderColor,
borderColorFocused: Styles.BorderColor,
titleColor: Styles.TitleColor,
titleAlign: AlignCenter,
showFocus: true,
}
b.focus = b
return b
}
// GetBorderPadding returns the size of the borders around the box content.
func (b *Box) GetBorderPadding() (top, bottom, left, right int) {
b.l.RLock()
defer b.l.RUnlock()
return b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight
}
// SetBorderPadding sets the size of the borders around the box content.
func (b *Box) SetBorderPadding(top, bottom, left, right int) *Box {
b.l.Lock()
@ -296,6 +307,13 @@ func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
return b
}
// GetBackgroundColor returns the box's background color.
func (b *Box) GetBackgroundColor() tcell.Color {
b.l.RLock()
defer b.l.RUnlock()
return b.backgroundColor
}
// SetBackgroundTransparent sets the flag indicating whether or not the box's
// background is transparent.
func (b *Box) SetBackgroundTransparent(transparent bool) *Box {
@ -306,6 +324,14 @@ func (b *Box) SetBackgroundTransparent(transparent bool) *Box {
return b
}
// GetBorder returns a value indicating whether the box have a border
// or not.
func (b *Box) GetBorder() bool {
b.l.RLock()
defer b.l.RUnlock()
return b.border
}
// SetBorder sets the flag indicating whether or not the box should have a
// border.
func (b *Box) SetBorder(show bool) *Box {
@ -325,6 +351,14 @@ func (b *Box) SetBorderColor(color tcell.Color) *Box {
return b
}
// SetBorderColorFocused sets the box's border color when the box is focused.
func (b *Box) SetBorderColorFocused(color tcell.Color) *Box {
b.l.Lock()
defer b.l.Unlock()
b.borderColorFocused = color
return b
}
// SetBorderAttributes sets the border's style attributes. You can combine
// different attributes using bitmask operations:
//
@ -406,6 +440,11 @@ func (b *Box) Draw(screen tcell.Screen) {
} else {
hasFocus = b.focus.HasFocus()
}
if hasFocus {
border = SetAttributes(background.Foreground(b.borderColorFocused), b.borderAttributes)
}
if hasFocus && b.showFocus {
horizontal = Borders.HorizontalFocus
vertical = Borders.VerticalFocus

32
button.go

@ -17,10 +17,10 @@ type Button struct {
labelColor tcell.Color
// The label color when the button is in focus.
labelColorActivated tcell.Color
labelColorFocused tcell.Color
// The background color when the button is in focus.
backgroundColorActivated tcell.Color
backgroundColorFocused tcell.Color
// An optional function which is called when the button was selected.
selected func()
@ -37,11 +37,11 @@ func NewButton(label string) *Button {
box := NewBox().SetBackgroundColor(Styles.ContrastBackgroundColor)
box.SetRect(0, 0, TaggedStringWidth(label)+4, 1)
return &Button{
Box: box,
label: label,
labelColor: Styles.PrimaryTextColor,
labelColorActivated: Styles.InverseTextColor,
backgroundColorActivated: Styles.PrimaryTextColor,
Box: box,
label: label,
labelColor: Styles.PrimaryTextColor,
labelColorFocused: Styles.InverseTextColor,
backgroundColorFocused: Styles.PrimaryTextColor,
}
}
@ -71,23 +71,23 @@ func (b *Button) SetLabelColor(color tcell.Color) *Button {
return b
}
// SetLabelColorActivated sets the color of the button text when the button is
// SetLabelColorFocused sets the color of the button text when the button is
// in focus.
func (b *Button) SetLabelColorActivated(color tcell.Color) *Button {
func (b *Button) SetLabelColorFocused(color tcell.Color) *Button {
b.Lock()
defer b.Unlock()
b.labelColorActivated = color
b.labelColorFocused = color
return b
}
// SetBackgroundColorActivated sets the background color of the button text when
// SetBackgroundColorFocused sets the background color of the button text when
// the button is in focus.
func (b *Button) SetBackgroundColorActivated(color tcell.Color) *Button {
func (b *Button) SetBackgroundColorFocused(color tcell.Color) *Button {
b.Lock()
defer b.Unlock()
b.backgroundColorActivated = color
b.backgroundColorFocused = color
return b
}
@ -124,8 +124,8 @@ func (b *Button) Draw(screen tcell.Screen) {
borderColor := b.borderColor
backgroundColor := b.backgroundColor
if b.focus.HasFocus() {
b.backgroundColor = b.backgroundColorActivated
b.borderColor = b.labelColorActivated
b.backgroundColor = b.backgroundColorFocused
b.borderColor = b.labelColorFocused
defer func() {
b.borderColor = borderColor
}()
@ -141,7 +141,7 @@ func (b *Button) Draw(screen tcell.Screen) {
y = y + height/2
labelColor := b.labelColor
if b.focus.HasFocus() {
labelColor = b.labelColorActivated
labelColor = b.labelColorFocused
}
Print(screen, b.label, x, y, width, AlignCenter, labelColor)
}

120
checkbox.go

@ -27,12 +27,21 @@ type CheckBox struct {
// The label color.
labelColor tcell.Color
// The label color when focused.
labelColorFocused tcell.Color
// The background color of the input area.
fieldBackgroundColor tcell.Color
// The background color of the input area when focused.
fieldBackgroundColorFocused tcell.Color
// The text color of the input area.
fieldTextColor tcell.Color
// The text color of the input area when focused.
fieldTextColorFocused tcell.Color
// An optional function which is called when the user changes the checked
// state of this checkbox.
changed func(checked bool)
@ -46,16 +55,23 @@ type CheckBox struct {
// this form item.
finished func(tcell.Key)
// The rune to show when the checkbox is checked
checkedRune rune
sync.RWMutex
}
// NewCheckBox returns a new input field.
func NewCheckBox() *CheckBox {
return &CheckBox{
Box: NewBox(),
labelColor: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
Box: NewBox(),
labelColor: Styles.SecondaryTextColor,
labelColorFocused: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldBackgroundColorFocused: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
fieldTextColorFocused: Styles.PrimaryTextColor,
checkedRune: Styles.CheckBoxCheckedRune,
}
}
@ -68,6 +84,15 @@ func (c *CheckBox) SetChecked(checked bool) *CheckBox {
return c
}
// SetCheckedRune sets the rune to show when the checkbox is checked.
func (c *CheckBox) SetCheckedRune(rune rune) *CheckBox {
c.Lock()
defer c.Unlock()
c.checkedRune = rune
return c
}
// IsChecked returns whether or not the box is checked.
func (c *CheckBox) IsChecked() bool {
c.RLock()
@ -110,6 +135,24 @@ func (c *CheckBox) GetMessage() string {
return c.message
}
// SetAttributes sets the given attributes on the check box.
func (c *CheckBox) SetAttributes(attributes ...FormItemAttribute) {
allAttributes := newFormItemAttributes()
for _, attribute := range attributes {
attribute.apply(allAttributes)
}
allAttributes.setLabelWidth(&c.labelWidth)
allAttributes.setBackgroundColor(&c.backgroundColor)
allAttributes.setLabelColor(&c.labelColor)
allAttributes.setLabelColorFocused(&c.labelColorFocused)
allAttributes.setFieldTextColor(&c.fieldTextColor)
allAttributes.setFieldTextColorFocused(&c.fieldTextColorFocused)
allAttributes.setFieldBackgroundColor(&c.fieldBackgroundColor)
allAttributes.setFieldBackgroundColorFocused(&c.fieldBackgroundColorFocused)
allAttributes.setFinishedFunc(&c.finished)
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (c *CheckBox) SetLabelWidth(width int) *CheckBox {
@ -120,6 +163,12 @@ func (c *CheckBox) SetLabelWidth(width int) *CheckBox {
return c
}
// SetBackgroundColor sets the background color.
func (c *CheckBox) SetBackgroundColor(color tcell.Color) *CheckBox {
c.Box.SetBackgroundColor(color)
return c
}
// SetLabelColor sets the color of the label.
func (c *CheckBox) SetLabelColor(color tcell.Color) *CheckBox {
c.Lock()
@ -129,6 +178,15 @@ func (c *CheckBox) SetLabelColor(color tcell.Color) *CheckBox {
return c
}
// SetLabelColorFocused sets the color of the label when focused.
func (c *CheckBox) SetLabelColorFocused(color tcell.Color) *CheckBox {
c.Lock()
defer c.Unlock()
c.labelColorFocused = color
return c
}
// SetFieldBackgroundColor sets the background color of the input area.
func (c *CheckBox) SetFieldBackgroundColor(color tcell.Color) *CheckBox {
c.Lock()
@ -138,6 +196,15 @@ func (c *CheckBox) SetFieldBackgroundColor(color tcell.Color) *CheckBox {
return c
}
// SetFieldBackgroundColorFocused sets the background color of the input area when focused.
func (c *CheckBox) SetFieldBackgroundColorFocused(color tcell.Color) *CheckBox {
c.Lock()
defer c.Unlock()
c.fieldBackgroundColorFocused = color
return c
}
// SetFieldTextColor sets the text color of the input area.
func (c *CheckBox) SetFieldTextColor(color tcell.Color) *CheckBox {
c.Lock()
@ -147,19 +214,20 @@ func (c *CheckBox) SetFieldTextColor(color tcell.Color) *CheckBox {
return c
}
// SetFormAttributes sets attributes shared by all form items.
func (c *CheckBox) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
// SetFieldTextColorFocused sets the text color of the input area when focused.
func (c *CheckBox) SetFieldTextColorFocused(color tcell.Color) *CheckBox {
c.Lock()
defer c.Unlock()
c.labelWidth = labelWidth
c.labelColor = labelColor
c.backgroundColor = bgColor
c.fieldTextColor = fieldTextColor
c.fieldBackgroundColor = fieldBgColor
c.fieldTextColorFocused = color
return c
}
// GetFieldHeight returns the height of the field.
func (c *CheckBox) GetFieldHeight() int {
return 1
}
// GetFieldWidth returns this primitive's field width.
func (c *CheckBox) GetFieldWidth() int {
c.RLock()
@ -199,7 +267,7 @@ func (c *CheckBox) SetDoneFunc(handler func(key tcell.Key)) *CheckBox {
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (c *CheckBox) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
func (c *CheckBox) SetFinishedFunc(handler func(key tcell.Key)) *CheckBox {
c.Lock()
defer c.Unlock()
@ -214,6 +282,16 @@ func (c *CheckBox) Draw(screen tcell.Screen) {
c.Lock()
defer c.Unlock()
// Select colors
labelColor := c.labelColor
fieldBackgroundColor := c.fieldBackgroundColor
fieldTextColor := c.fieldTextColor
if c.GetFocusable().HasFocus() {
labelColor = c.labelColorFocused
fieldBackgroundColor = c.fieldBackgroundColorFocused
fieldTextColor = c.fieldTextColorFocused
}
// Prepare
x, y, width, height := c.GetInnerRect()
rightLimit := x + width
@ -227,26 +305,26 @@ func (c *CheckBox) Draw(screen tcell.Screen) {
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
}
Print(screen, c.label, x, y, labelWidth, AlignLeft, c.labelColor)
Print(screen, c.label, x, y, labelWidth, AlignLeft, labelColor)
x += labelWidth
} else {
_, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, c.labelColor)
_, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, labelColor)
x += drawnWidth
}
// 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'
fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor).Foreground(fieldTextColor)
checkedRune := c.checkedRune
if !c.checked {
checkedRune = ' '
}
screen.SetContent(x, y, checkedRune, nil, fieldStyle)
screen.SetContent(x, y, ' ', nil, fieldStyle)
screen.SetContent(x+1, y, checkedRune, nil, fieldStyle)
screen.SetContent(x+2, y, ' ', nil, fieldStyle)
if c.message != "" {
Print(screen, c.message, x+2, y, len(c.message), AlignLeft, c.labelColor)
Print(screen, c.message, x+4, y, len(c.message), AlignLeft, labelColor)
}
}

10
contextmenu.go

@ -60,11 +60,11 @@ func (c *ContextMenu) AddContextItem(text string, shortcut rune, selected func(i
c.initializeList()
c.list.AddItem(text, "", shortcut, c.wrap(selected))
c.list.AddItem(NewListItem(text).SetShortcut(shortcut).SetSelectedFunc(c.wrap(selected)))
if text == "" && shortcut == 0 {
c.list.Lock()
index := len(c.list.items) - 1
c.list.items[index].Enabled = false
c.list.items[index].enabled = false
c.list.Unlock()
}
@ -139,14 +139,14 @@ func (c *ContextMenu) show(item int, x int, y int, setFocus func(Primitive)) {
c.list.Lock()
for i, item := range c.list.items {
if item.Enabled {
if item.enabled {
c.list.currentItem = i
break
}
}
c.list.Unlock()
c.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
c.list.SetSelectedFunc(func(index int, item *ListItem) {
c.l.Lock()
// A context item was selected. Close the menu.
@ -154,7 +154,7 @@ func (c *ContextMenu) show(item int, x int, y int, setFocus func(Primitive)) {
if c.selected != nil {
c.l.Unlock()
c.selected(index, mainText, shortcut)
c.selected(index, item.mainText, item.shortcut)
} else {
c.l.Unlock()
}

7
demos/dropdown/main.go

@ -7,7 +7,12 @@ func main() {
app := cview.NewApplication()
dropdown := cview.NewDropDown().
SetLabel("Select an option (hit Enter): ").
SetOptions([]string{"First", "Second", "Third", "Fourth", "Fifth"}, nil)
SetOptions(nil,
cview.NewDropDownOption("First"),
cview.NewDropDownOption("Second"),
cview.NewDropDownOption("Third"),
cview.NewDropDownOption("Fourth"),
cview.NewDropDownOption("Fifth"))
if err := app.SetRoot(dropdown, true).EnableMouse(true).Run(); err != nil {
panic(err)
}

6
demos/form/main.go

@ -8,9 +8,13 @@ import (
func main() {
app := cview.NewApplication()
form := cview.NewForm().
AddDropDown("Title", []string{"Mr.", "Ms.", "Mrs.", "Dr.", "Prof."}, 0, nil).
AddDropDownSimple("Title", 0, nil, "Mr.", "Ms.", "Mrs.", "Dr.", "Prof.").
AddInputField("First name", "", 20, nil, nil).
AddInputField("Last name", "", 20, nil, nil).
AddFormItem(cview.NewInputField().
SetLabel("Address").
SetFieldWidth(30).
SetFieldNote("Your complete address")).
AddPasswordField("Password", "", 10, '*', nil).
AddCheckBox("", "Age 18+", false, nil).
AddButton("Save", nil).

4
demos/inputfield/autocomplete/main.go

@ -19,13 +19,13 @@ func main() {
SetDoneFunc(func(key tcell.Key) {
app.Stop()
})
inputField.SetAutocompleteFunc(func(currentText string) (entries []string) {
inputField.SetAutocompleteFunc(func(currentText string) (entries []*cview.ListItem) {
if len(currentText) == 0 {
return
}
for _, word := range words {
if strings.HasPrefix(strings.ToLower(word), strings.ToLower(currentText)) {
entries = append(entries, word)
entries = append(entries, cview.NewListItem(word))
}
}
if len(entries) <= 1 {

8
demos/inputfield/autocompleteasync/main.go

@ -26,8 +26,8 @@ func main() {
// Set up autocomplete function.
var mutex sync.Mutex
prefixMap := make(map[string][]string)
inputField.SetAutocompleteFunc(func(currentText string) []string {
prefixMap := make(map[string][]*cview.ListItem)
inputField.SetAutocompleteFunc(func(currentText string) []*cview.ListItem {
// Ignore empty text.
prefix := strings.TrimSpace(strings.ToLower(currentText))
if prefix == "" {
@ -57,9 +57,9 @@ func main() {
if err := dec.Decode(&companies); err != nil {
return
}
entries := make([]string, 0, len(companies))
entries := make([]*cview.ListItem, 0, len(companies))
for _, c := range companies {
entries = append(entries, c.Name)
entries = append(entries, cview.NewListItem(c.Name))
}
mutex.Lock()
prefixMap[prefix] = entries

12
demos/list/main.go

@ -12,13 +12,13 @@ func main() {
reset := func() {
list.
Clear().
AddItem("List item 1", "Some explanatory text", 'a', nil).
AddItem("List item 2", "Some explanatory text", 'b', nil).
AddItem("List item 3", "Some explanatory text", 'c', nil).
AddItem("List item 4", "Some explanatory text", 'd', nil).
AddItem("Quit", "Press to exit", 'q', func() {
AddItem(cview.NewListItem("List item 1").SetSecondaryText("Some explanatory text").SetShortcut('a')).
AddItem(cview.NewListItem("List item 2").SetSecondaryText("Some explanatory text").SetShortcut('b')).
AddItem(cview.NewListItem("List item 3").SetSecondaryText("Some explanatory text").SetShortcut('c')).
AddItem(cview.NewListItem("List item 4").SetSecondaryText("Some explanatory text").SetShortcut('d')).
AddItem(cview.NewListItem("Quit").SetSecondaryText("Press to exit").SetShortcut('q').SetSelectedFunc(func() {
app.Stop()
})
}))
list.ContextMenuList().SetItemEnabled(3, false)
}

2
demos/presentation/form.go

@ -33,7 +33,7 @@ func Form(nextSlide func()) (title string, content cview.Primitive) {
f := cview.NewForm().
AddInputField("First name:", "", 20, nil, nil).
AddInputField("Last name:", "", 20, nil, nil).
AddDropDown("Role:", []string{"Engineer", "Manager", "Administration"}, 0, nil).
AddDropDownSimple("Role:", 0, nil, "Engineer", "Manager", "Administration").
AddPasswordField("Password:", "", 10, '*', nil).
AddCheckBox("", "On vacation", false, nil).
AddButton("Save", nextSlide).

12
demos/presentation/introduction.go

@ -9,12 +9,12 @@ func Introduction(nextSlide func()) (title string, content cview.Primitive) {
reset := func() {
list.
Clear().
AddItem("A Go package for terminal based UIs", "with a special focus on rich interactive widgets", '1', nextSlide).
AddItem("Based on github.com/gdamore/tcell", "Like termbox but better (see tcell docs)", '2', nextSlide).
AddItem("Designed to be simple", `"Hello world" is 5 lines of code`, '3', nextSlide).
AddItem("Good for data entry", `For charts, use "termui" - for low-level views, use "gocui" - ...`, '4', nextSlide).
AddItem("Supports context menus", "Right click on one of these items or press Alt+Enter", '5', nextSlide).
AddItem("Extensive documentation", "Demo code is available for each widget", '6', nextSlide)
AddItem(cview.NewListItem("A Go package for terminal based UIs").SetSecondaryText("with a special focus on rich interactive widgets").SetShortcut('1').SetSelectedFunc(nextSlide)).
AddItem(cview.NewListItem("Based on github.com/gdamore/tcell").SetSecondaryText("Like termbox but better (see tcell docs)").SetShortcut('2').SetSelectedFunc(nextSlide)).
AddItem(cview.NewListItem("Designed to be simple").SetSecondaryText(`"Hello world" is 5 lines of code`).SetShortcut('3').SetSelectedFunc(nextSlide)).
AddItem(cview.NewListItem("Good for data entry").SetSecondaryText(`For charts, use "termui" - for low-level views, use "gocui" - ...`).SetShortcut('4').SetSelectedFunc(nextSlide)).
AddItem(cview.NewListItem("Supports context menus").SetSecondaryText("Right click on one of these items or press Alt+Enter").SetShortcut('5').SetSelectedFunc(nextSlide)).
AddItem(cview.NewListItem("Extensive documentation").SetSecondaryText("Demo code is available for each widget").SetShortcut('6').SetSelectedFunc(nextSlide))
list.ContextMenuList().SetItemEnabled(3, false)
}

16
demos/presentation/table.go

@ -341,14 +341,14 @@ func Table(nextSlide func()) (title string, content cview.Primitive) {
}
list.ShowSecondaryText(false).
AddItem("Basic table", "", 'b', basic).
AddItem("Table with separator", "", 's', separator).
AddItem("Table with borders", "", 'o', borders).
AddItem("Selectable rows", "", 'r', selectRow).
AddItem("Selectable columns", "", 'c', selectColumn).
AddItem("Selectable cells", "", 'l', selectCell).
AddItem("Navigate", "", 'n', navigate).
AddItem("Next slide", "", 'x', nextSlide)
AddItem(cview.NewListItem("Basic table").SetShortcut('b').SetSelectedFunc(basic)).
AddItem(cview.NewListItem("Table with separator").SetShortcut('s').SetSelectedFunc(separator)).
AddItem(cview.NewListItem("Table with borders").SetShortcut('o').SetSelectedFunc(borders)).
AddItem(cview.NewListItem("Selectable rows").SetShortcut('r').SetSelectedFunc(selectRow)).
AddItem(cview.NewListItem("Selectable columns").SetShortcut('c').SetSelectedFunc(selectColumn)).
AddItem(cview.NewListItem("Selectable cells").SetShortcut('l').SetSelectedFunc(selectCell)).
AddItem(cview.NewListItem("Navigate").SetShortcut('n').SetSelectedFunc(navigate)).
AddItem(cview.NewListItem("Next slide").SetShortcut('x').SetSelectedFunc(nextSlide))
list.SetBorderPadding(1, 1, 2, 2)
basic()

6
demos/unicode/main.go

@ -12,15 +12,15 @@ func main() {
pages := cview.NewPages()
form := cview.NewForm()
form.AddDropDown("称谓", []string{"先生", "女士", "博士", "老师", "师傅"}, 0, nil).
form.AddDropDownSimple("称谓", 0, nil, "先生", "女士", "博士", "老师", "师傅").
AddInputField("姓名", "", 20, nil, nil).
AddPasswordField("密码", "", 10, '*', nil).
AddCheckBox("", "年龄 18+", false, nil).
AddButton("保存", func() {
_, title := form.GetFormItem(0).(*cview.DropDown).GetCurrentOption()
_, option := form.GetFormItem(0).(*cview.DropDown).GetCurrentOption()
userName := form.GetFormItem(1).(*cview.InputField).GetText()
alert(pages, "alert-dialog", fmt.Sprintf("保存成功,%s %s!", userName, title))
alert(pages, "alert-dialog", fmt.Sprintf("保存成功,%s %s!", userName, option.GetText()))
}).
AddButton("退出", func() {
app.Stop()

352
dropdown.go

@ -7,10 +7,55 @@ import (
"github.com/gdamore/tcell/v2"
)
// dropDownOption is one option that can be selected in a drop-down primitive.
type dropDownOption struct {
Text string // The text to be displayed in the drop-down.
Selected func() // The (optional) callback for when this option was selected.
// DropDownOption is one option that can be selected in a drop-down primitive.
type DropDownOption struct {
text string // The text to be displayed in the drop-down.
selected func(index int, option *DropDownOption) // The (optional) callback for when this option was selected.
reference interface{} // An optional reference object.
}
func NewDropDownOption(text string) *DropDownOption {
return &DropDownOption{text: text}
}
// GetText returns the text of this dropdown option.
func (d *DropDownOption) GetText() string {
return d.text
}
// SetText returns the text of this dropdown option.
func (d *DropDownOption) SetText(text string) *DropDownOption {
d.text = text
return d
}
// SetSelectedFunc sets the handler to be called when this option is selected.
func (d *DropDownOption) SetSelectedFunc(handler func(index int, option *DropDownOption)) *DropDownOption {
d.selected = handler
return d
}
// GetReference returns the reference object of this dropdown option.
func (d *DropDownOption) GetReference() interface{} {
return d.reference
}
// SetReference allows you to store a reference of any type in this option.
func (d *DropDownOption) SetReference(reference interface{}) *DropDownOption {
d.reference = reference
return d
}
// SetChangedFunc sets a handler which is called when the user changes the
// drop-down's option. This handler will be called in addition and prior to
// an option's optional individual handler. The handler is provided with the
// selected option's index and the option itself. If "no option" was selected, these values
// are -1 and nil.
func (d *DropDown) SetChangedFunc(handler func(index int, option *DropDownOption)) *DropDown {
d.list.SetChangedFunc(func(index int, item *ListItem) {
handler(index, d.options[index])
})
return d
}
// DropDown implements a selection widget whose options become visible in a
@ -19,7 +64,7 @@ type DropDown struct {
*Box
// The options from which the user can choose.
options []*dropDownOption
options []*DropDownOption
// Strings to be placed before and after each drop-down option.
optionPrefix, optionSuffix string
@ -28,7 +73,7 @@ type DropDown struct {
// currently selected.
currentOption int
// Strings to be placed beefore and after the current option.
// Strings to be placed before and after the current option.
currentOptionPrefix, currentOptionSuffix string
// The text to be displayed when no option has yet been selected.
@ -49,12 +94,21 @@ type DropDown struct {
// The label color.
labelColor tcell.Color
// The label color when focused.
labelColorFocused tcell.Color
// The background color of the input area.
fieldBackgroundColor tcell.Color
// The background color of the input area when focused.
fieldBackgroundColorFocused tcell.Color
// The text color of the input area.
fieldTextColor tcell.Color
// The text color of the input area when focused.
fieldTextColorFocused tcell.Color
// The color for prefixes.
prefixTextColor tcell.Color
@ -77,11 +131,17 @@ type DropDown struct {
// A callback function which is called when the user changes the drop-down's
// selection.
selected func(text string, index int)
selected func(index int, option *DropDownOption)
// Set to true when mouse dragging is in progress.
dragging bool
// The chars to show when the option's text gets shortened.
abbreviationChars string
// The symbol to draw at the end of the field.
dropDownSymbol rune
sync.RWMutex
}
@ -96,13 +156,18 @@ func NewDropDown() *DropDown {
SetBackgroundColor(Styles.MoreContrastBackgroundColor)
d := &DropDown{
Box: NewBox(),
currentOption: -1,
list: list,
labelColor: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
prefixTextColor: Styles.ContrastSecondaryTextColor,
Box: NewBox(),
currentOption: -1,
list: list,
labelColor: Styles.SecondaryTextColor,
labelColorFocused: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldBackgroundColorFocused: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
fieldTextColorFocused: Styles.PrimaryTextColor,
prefixTextColor: Styles.ContrastSecondaryTextColor,
dropDownSymbol: Styles.DropDownSymbol,
abbreviationChars: Styles.DropDownAbbreviationChars,
}
d.focus = d
@ -110,6 +175,15 @@ func NewDropDown() *DropDown {
return d
}
// SetDropDownSymbolRune sets the rune to be drawn at the end of the dropdown field
// to indicate that this field is a dropdown.
func (d *DropDown) SetDropDownSymbolRune(symbol rune) *DropDown {
d.Lock()
defer d.Unlock()
d.dropDownSymbol = symbol
return d
}
// SetCurrentOption sets the index of the currently selected option. This may
// be a negative value to indicate that no option is currently selected. Calling
// this function will also trigger the "selected" callback (if there is one).
@ -122,12 +196,12 @@ func (d *DropDown) SetCurrentOption(index int) *DropDown {
d.list.SetCurrentItem(index)
if d.selected != nil {
d.Unlock()
d.selected(d.options[index].Text, index)
d.selected(index, d.options[index])
d.Lock()
}
if d.options[index].Selected != nil {
if d.options[index].selected != nil {
d.Unlock()
d.options[index].Selected()
d.options[index].selected(index, d.options[index])
d.Lock()
}
} else {
@ -135,7 +209,7 @@ func (d *DropDown) SetCurrentOption(index int) *DropDown {
d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
if d.selected != nil {
d.Unlock()
d.selected("", -1)
d.selected(-1, nil)
d.Lock()
}
}
@ -143,16 +217,16 @@ func (d *DropDown) SetCurrentOption(index int) *DropDown {
}
// GetCurrentOption returns the index of the currently selected option as well
// as its text. If no option was selected, -1 and an empty string is returned.
func (d *DropDown) GetCurrentOption() (int, string) {
// as the option itself. If no option was selected, -1 and nil is returned.
func (d *DropDown) GetCurrentOption() (int, *DropDownOption) {
d.RLock()
defer d.RUnlock()
var text string
var option *DropDownOption
if d.currentOption >= 0 && d.currentOption < len(d.options) {
text = d.options[d.currentOption].Text
option = d.options[d.currentOption]
}
return d.currentOption, text
return d.currentOption, option
}
// SetTextOptions sets the text to be placed before and after each drop-down
@ -170,7 +244,7 @@ func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix,
d.optionPrefix = prefix
d.optionSuffix = suffix
for index := 0; index < d.list.GetItemCount(); index++ {
d.list.SetItemText(index, prefix+d.options[index].Text+suffix, "")
d.list.SetItemText(index, prefix+d.options[index].text+suffix, "")
}
return d
}
@ -192,6 +266,24 @@ func (d *DropDown) GetLabel() string {
return d.label
}
// SetAttributes sets the given attributes on the drop down.
func (d *DropDown) SetAttributes(attributes ...FormItemAttribute) {
allAttributes := newFormItemAttributes()
for _, attribute := range attributes {
attribute.apply(allAttributes)
}
allAttributes.setLabelWidth(&d.labelWidth)
allAttributes.setBackgroundColor(&d.backgroundColor)
allAttributes.setLabelColor(&d.labelColor)
allAttributes.setLabelColorFocused(&d.labelColorFocused)
allAttributes.setFieldTextColor(&d.fieldTextColor)
allAttributes.setFieldTextColorFocused(&d.fieldTextColorFocused)
allAttributes.setFieldBackgroundColor(&d.fieldBackgroundColor)
allAttributes.setFieldBackgroundColorFocused(&d.fieldBackgroundColorFocused)
allAttributes.setFinishedFunc(&d.finished)
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (d *DropDown) SetLabelWidth(width int) *DropDown {
@ -202,6 +294,12 @@ func (d *DropDown) SetLabelWidth(width int) *DropDown {
return d
}
// SetBackgroundColor sets the background color.
func (d *DropDown) SetBackgroundColor(color tcell.Color) *DropDown {
d.Box.SetBackgroundColor(color)
return d
}
// SetLabelColor sets the color of the label.
func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown {
d.Lock()
@ -211,6 +309,15 @@ func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown {
return d
}
// SetLabelColorFocused sets the color of the label when focused.
func (d *DropDown) SetLabelColorFocused(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.labelColorFocused = color
return d
}
// SetFieldBackgroundColor sets the background color of the options area.
func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
d.Lock()
@ -220,6 +327,15 @@ func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
return d
}
// SetFieldBackgroundColorFocused sets the background color of the options area when focused.
func (d *DropDown) SetFieldBackgroundColorFocused(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.fieldBackgroundColorFocused = color
return d
}
// SetFieldTextColor sets the text color of the options area.
func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
d.Lock()
@ -229,27 +345,59 @@ func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
return d
}
// SetPrefixTextColor sets the color of the prefix string. The prefix string is
// shown when the user starts typing text, which directly selects the first
// option that starts with the typed string.
func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
// SetFieldTextColorFocused sets the text color of the options area when focused.
func (d *DropDown) SetFieldTextColorFocused(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.prefixTextColor = color
d.fieldTextColorFocused = color
return d
}
// SetDropDownTextColor sets text color of the drop down list.
func (d *DropDown) SetDropDownTextColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.list.SetMainTextColor(color)
return d
}
// SetFormAttributes sets attributes shared by all form items.
func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
// SetDropDownBackgroundColor sets the background color of the drop list.
func (d *DropDown) SetDropDownBackgroundColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.labelWidth = labelWidth
d.labelColor = labelColor
d.backgroundColor = bgColor
d.fieldTextColor = fieldTextColor
d.fieldBackgroundColor = fieldBgColor
d.list.SetBackgroundColor(color)
return d
}
// The text color of the selected option in the drop down list.
func (d *DropDown) SetDropDownSelectedTextColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.list.SetSelectedTextColor(color)
return d
}
// The background color of the selected option in the drop down list.
func (d *DropDown) SetDropDownSelectedBackgroundColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.list.SetSelectedBackgroundColor(color)
return d
}
// SetPrefixTextColor sets the color of the prefix string. The prefix string is
// shown when the user starts typing text, which directly selects the first
// option that starts with the typed string.
func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.prefixTextColor = color
return d
}
@ -263,54 +411,84 @@ func (d *DropDown) SetFieldWidth(width int) *DropDown {
return d
}
// GetFieldHeight returns the height of the field.
func (d *DropDown) GetFieldHeight() int {
return 1
}
// GetFieldWidth returns this primitive's field screen width.
func (d *DropDown) GetFieldWidth() int {
d.RLock()
defer d.RUnlock()
return d.getFieldWidth()
}
func (d *DropDown) getFieldWidth() int {
if d.fieldWidth > 0 {
return d.fieldWidth
}
fieldWidth := 0
for _, option := range d.options {
width := TaggedStringWidth(option.Text)
width := TaggedStringWidth(option.text)
if width > fieldWidth {
fieldWidth = width
}
}
fieldWidth += len(d.optionPrefix) + len(d.optionSuffix)
fieldWidth += len(d.currentOptionPrefix) + len(d.currentOptionSuffix)
fieldWidth += 3 // space + dropDownSymbol + space
return fieldWidth
}
// AddOption adds a new selectable option to this drop-down. The "selected"
// callback is called when this option was selected. It may be nil.
func (d *DropDown) AddOption(text string, selected func()) *DropDown {
// AddOptions adds new selectable options to this drop-down.
func (d *DropDown) AddOptionsSimple(options ...string) *DropDown {
optionsToAdd := make([]*DropDownOption, len(options))
for i, option := range options {
optionsToAdd[i] = NewDropDownOption(option)
}
d.AddOptions(optionsToAdd...)
return d
}
// AddOptions adds new selectable options to this drop-down.
func (d *DropDown) AddOptions(options ...*DropDownOption) *DropDown {
d.Lock()
defer d.Unlock()
return d.addOptions(options...)
}
return d.addOption(text, selected)
func (d *DropDown) addOptions(options ...*DropDownOption) *DropDown {
d.options = append(d.options, options...)
for _, option := range options {
d.list.AddItem(NewListItem(d.optionPrefix + option.text + d.optionSuffix))
}
return d
}
func (d *DropDown) addOption(text string, selected func()) *DropDown {
d.options = append(d.options, &dropDownOption{Text: text, Selected: selected})
d.list.AddItem(d.optionPrefix+text+d.optionSuffix, "", 0, nil)
// SetOptionsSimple replaces all current options with the ones provided and installs
// one callback function which is called when one of the options is selected.
// It will be called with the option's index and the option itself
// The "selected" parameter may be nil.
func (d *DropDown) SetOptionsSimple(selected func(index int, option *DropDownOption), options ...string) *DropDown {
optionsToSet := make([]*DropDownOption, len(options))
for i, option := range options {
optionsToSet[i] = NewDropDownOption(option)
}
d.SetOptions(selected, optionsToSet...)
return d
}
// SetOptions replaces all current options with the ones provided and installs
// one callback function which is called when one of the options is selected.
// It will be called with the option's text and its index into the options
// slice. The "selected" parameter may be nil.
func (d *DropDown) SetOptions(texts []string, selected func(text string, index int)) *DropDown {
// It will be called with the option's index and the option itself.
// The "selected" parameter may be nil.
func (d *DropDown) SetOptions(selected func(index int, option *DropDownOption), options ...*DropDownOption) *DropDown {
d.Lock()
defer d.Unlock()
d.list.Clear()
d.options = nil
for index, text := range texts {
func(t string, i int) {
d.addOption(text, nil)
}(text, index)
}
d.addOptions(options...)
d.selected = selected
return d
}
@ -318,9 +496,9 @@ func (d *DropDown) SetOptions(texts []string, selected func(text string, index i
// SetSelectedFunc sets a handler which is called when the user changes the
// drop-down's option. This handler will be called in addition and prior to
// an option's optional individual handler. The handler is provided with the
// selected option's text and index. If "no option" was selected, these values
// are an empty string and -1.
func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown {
// selected option's index and the option itself. If "no option" was selected, these values
// are -1 and nil.
func (d *DropDown) SetSelectedFunc(handler func(index int, option *DropDownOption)) *DropDown {
d.Lock()
defer d.Unlock()
@ -344,7 +522,7 @@ func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown {
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) *DropDown {
d.Lock()
defer d.Unlock()
@ -360,6 +538,16 @@ func (d *DropDown) Draw(screen tcell.Screen) {
d.Lock()
defer d.Unlock()
// Select colors
labelColor := d.labelColor
fieldBackgroundColor := d.fieldBackgroundColor
fieldTextColor := d.fieldTextColor
if hasFocus {
labelColor = d.labelColorFocused
fieldBackgroundColor = d.fieldBackgroundColorFocused
fieldTextColor = d.fieldTextColorFocused
}
// Prepare.
x, y, width, height := d.GetInnerRect()
rightLimit := x + width
@ -373,10 +561,10 @@ func (d *DropDown) Draw(screen tcell.Screen) {
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
}
Print(screen, d.label, x, y, labelWidth, AlignLeft, d.labelColor)
Print(screen, d.label, x, y, labelWidth, AlignLeft, labelColor)
x += labelWidth
} else {
_, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor)
_, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, labelColor)
x += drawnWidth
}
@ -384,14 +572,14 @@ func (d *DropDown) Draw(screen tcell.Screen) {
maxWidth := 0
optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
for _, option := range d.options {
strWidth := TaggedStringWidth(option.Text) + optionWrapWidth
strWidth := TaggedStringWidth(option.text) + optionWrapWidth
if strWidth > maxWidth {
maxWidth = strWidth
}
}
// Draw selection area.
fieldWidth := d.fieldWidth
fieldWidth := d.getFieldWidth()
if fieldWidth == 0 {
fieldWidth = maxWidth
if d.currentOption < 0 {
@ -400,7 +588,7 @@ func (d *DropDown) Draw(screen tcell.Screen) {
fieldWidth = noSelectionWidth
}
} else if d.currentOption < len(d.options) {
currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix)
currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix)
if currentOptionWidth > fieldWidth {
fieldWidth = currentOptionWidth
}
@ -409,10 +597,7 @@ func (d *DropDown) Draw(screen tcell.Screen) {
if rightLimit-x < fieldWidth {
fieldWidth = rightLimit - x
}
fieldStyle := tcell.StyleDefault.Background(d.fieldBackgroundColor)
if hasFocus && !d.open {
fieldStyle = fieldStyle.Background(d.fieldTextColor)
}
fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
for index