package tview import ( "fmt" "github.com/gdamore/tcell" ) // listItem represents one item in a List. type listItem struct { MainText string // The main text of the list item. SecondaryText string // A secondary text to be shown underneath the main text. Shortcut rune // The key to select the list item directly, 0 if there is no shortcut. Selected func() // The optional function which is called when the item is selected. } // List displays rows of items, each of which can be selected. // // See https://github.com/rivo/tview/wiki/List for an example. type List struct { *Box // The items of the list. items []*listItem // The index of the currently selected item. currentItem int // Whether or not to show the secondary item texts. showSecondaryText bool // The item main text color. mainTextColor tcell.Color // The item secondary text color. secondaryTextColor tcell.Color // The item shortcut text color. shortcutColor tcell.Color // The text color for selected items. selectedTextColor tcell.Color // The background color for selected items. selectedBackgroundColor tcell.Color // If true, the selection is only shown when the list has focus. selectedFocusOnly bool // An optional function which is called when the user has navigated to a list // item. changed func(index int, mainText, secondaryText string, shortcut rune) // An optional function which is called when a list item was selected. This // function will be called even if the list item defines its own callback. selected func(index int, mainText, secondaryText string, shortcut rune) // An optional function which is called when the user presses the Escape key. done func() } // NewList returns a new form. func NewList() *List { return &List{ Box: NewBox(), showSecondaryText: true, mainTextColor: Styles.PrimaryTextColor, secondaryTextColor: Styles.TertiaryTextColor, shortcutColor: Styles.SecondaryTextColor, selectedTextColor: Styles.PrimitiveBackgroundColor, selectedBackgroundColor: Styles.PrimaryTextColor, } } // SetCurrentItem sets the currently selected item by its index. This triggers // a "changed" event. func (l *List) SetCurrentItem(index int) *List { l.currentItem = index if l.currentItem < len(l.items) && l.changed != nil { item := l.items[l.currentItem] l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) } return l } // GetCurrentItem returns the index of the currently selected list item. func (l *List) GetCurrentItem() int { return l.currentItem } // RemoveItem removes the item with the given index (starting at 0) from the // list. Does nothing if the index is out of range. func (l *List) RemoveItem(index int) *List { if index < 0 || index >= len(l.items) { return l } l.items = append(l.items[:index], l.items[index+1:]...) if l.currentItem >= len(l.items) { l.currentItem = len(l.items) - 1 } return l } // SetMainTextColor sets the color of the items' main text. func (l *List) SetMainTextColor(color tcell.Color) *List { l.mainTextColor = color return l } // SetSecondaryTextColor sets the color of the items' secondary text. func (l *List) SetSecondaryTextColor(color tcell.Color) *List { l.secondaryTextColor = color return l } // SetShortcutColor sets the color of the items' shortcut. func (l *List) SetShortcutColor(color tcell.Color) *List { l.shortcutColor = color return l } // SetSelectedTextColor sets the text color of selected items. func (l *List) SetSelectedTextColor(color tcell.Color) *List { l.selectedTextColor = color return l } // SetSelectedBackgroundColor sets the background color of selected items. func (l *List) SetSelectedBackgroundColor(color tcell.Color) *List { l.selectedBackgroundColor = color return l } // SetSelectedFocusOnly sets a flag which determines when the currently selected // list item is highlighted. If set to true, selected items are only highlighted // when the list has focus. If set to false, they are always highlighted. func (l *List) SetSelectedFocusOnly(focusOnly bool) *List { l.selectedFocusOnly = focusOnly return l } // ShowSecondaryText determines whether or not to show secondary item texts. func (l *List) ShowSecondaryText(show bool) *List { l.showSecondaryText = show return l } // SetChangedFunc sets the function which is called when the user navigates to // a list item. The function receives the item's index in the list of items // (starting with 0), its main text, secondary text, and its shortcut rune. // // This function is also called when the first item is added or when // SetCurrentItem() is called. func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List { l.changed = handler return l } // SetSelectedFunc sets the function which is called when the user selects a // list item by pressing Enter on the current selection. The function receives // the item's index in the list of items (starting with 0), its main text, // secondary text, and its shortcut rune. func (l *List) SetSelectedFunc(handler func(int, string, string, rune)) *List { l.selected = handler return l } // SetDoneFunc sets a function which is called when the user presses the Escape // key. func (l *List) SetDoneFunc(handler func()) *List { l.done = handler return l } // AddItem adds a new item to the list. An item has a main text which will be // highlighted when selected. It also has a secondary text which is shown // underneath the main text (if it is set to visible) but which may remain // empty. // // The shortcut is a key binding. If the specified rune is entered, the item // is selected immediately. Set to 0 for no binding. // // The "selected" callback will be invoked when the user selects the item. You // may provide nil if no such item is needed or if all events are handled // through the selected callback set with SetSelectedFunc(). func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List { l.items = append(l.items, &listItem{ MainText: mainText, SecondaryText: secondaryText, Shortcut: shortcut, Selected: selected, }) if len(l.items) == 1 && l.changed != nil { item := l.items[0] l.changed(0, item.MainText, item.SecondaryText, item.Shortcut) } return l } // GetItemCount returns the number of items in the list. func (l *List) GetItemCount() int { return len(l.items) } // GetItemText returns an item's texts (main and secondary). Panics if the index // is out of range. func (l *List) GetItemText(index int) (main, secondary string) { return l.items[index].MainText, l.items[index].SecondaryText } // SetItemText sets an item's main and secondary text. Panics if the index is // out of range. func (l *List) SetItemText(index int, main, secondary string) *List { item := l.items[index] item.MainText = main item.SecondaryText = secondary return l } // Clear removes all items from the list. func (l *List) Clear() *List { l.items = nil l.currentItem = 0 return l } // Draw draws this primitive onto the screen. func (l *List) Draw(screen tcell.Screen) { l.Box.Draw(screen) // Determine the dimensions. x, y, width, height := l.GetInnerRect() bottomLimit := y + height // Do we show any shortcuts? var showShortcuts bool for _, item := range l.items { if item.Shortcut != 0 { showShortcuts = true x += 4 width -= 4 break } } // We want to keep the current selection in view. What is our offset? var offset int if l.showSecondaryText { if 2*l.currentItem >= height { offset = (2*l.currentItem + 2 - height) / 2 } } else { if l.currentItem >= height { offset = l.currentItem + 1 - height } } // Draw the list items. for index, item := range l.items { if index < offset { continue } if y >= bottomLimit { break } // Shortcuts. if showShortcuts && item.Shortcut != 0 { Print(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 4, AlignRight, l.shortcutColor) } // Main text. Print(screen, item.MainText, x, y, width, AlignLeft, l.mainTextColor) // Background color of selected text. if index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus()) { textWidth := StringWidth(item.MainText) for bx := 0; bx < textWidth && bx < width; bx++ { m, c, style, _ := screen.GetContent(x+bx, y) fg, _, _ := style.Decompose() if fg == l.mainTextColor { fg = l.selectedTextColor } style = style.Background(l.selectedBackgroundColor).Foreground(fg) screen.SetContent(x+bx, y, m, c, style) } } y++ if y >= bottomLimit { break } // Secondary text. if l.showSecondaryText { Print(screen, item.SecondaryText, x, y, width, AlignLeft, l.secondaryTextColor) y++ } } } // InputHandler returns the handler for this primitive. func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { previousItem := l.currentItem switch key := event.Key(); key { case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight: l.currentItem++ case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft: l.currentItem-- case tcell.KeyHome: l.currentItem = 0 case tcell.KeyEnd: l.currentItem = len(l.items) - 1 case tcell.KeyPgDn: l.currentItem += 5 case tcell.KeyPgUp: l.currentItem -= 5 case tcell.KeyEnter: if l.currentItem >= 0 && l.currentItem < len(l.items) { item := l.items[l.currentItem] if item.Selected != nil { item.Selected() } if l.selected != nil { l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) } } case tcell.KeyEscape: if l.done != nil { l.done() } case tcell.KeyRune: ch := event.Rune() if ch != ' ' { // It's not a space bar. Is it a shortcut? var found bool for index, item := range l.items { if item.Shortcut == ch { // We have a shortcut. found = true l.currentItem = index break } } if !found { break } } item := l.items[l.currentItem] if item.Selected != nil { item.Selected() } if l.selected != nil { l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) } } if l.currentItem < 0 { l.currentItem = len(l.items) - 1 } else if l.currentItem >= len(l.items) { l.currentItem = 0 } if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil { item := l.items[l.currentItem] l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) } }) }