diff --git a/CHANGELOG b/CHANGELOG index 5fb55e6..3cec3d3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ v1.4.2 (WIP) +- Add scroll bar to List, DropDown, Table and TreeView - Add SetDoneFunc to TreeView - Fix rendering issues with TextViews which have their background color set to ColorDefault diff --git a/dropdown.go b/dropdown.go index 4c47f72..73c5761 100644 --- a/dropdown.go +++ b/dropdown.go @@ -374,7 +374,6 @@ func (d *DropDown) Draw(screen tcell.Screen) { // We prefer to drop down but if there is no space, maybe drop up? lx := x ly := y + 1 - lwidth := maxWidth lheight := len(d.options) _, sheight := screen.Size() if ly+lheight >= sheight && ly-2 > lheight-ly { @@ -386,6 +385,10 @@ func (d *DropDown) Draw(screen tcell.Screen) { if ly+lheight >= sheight { lheight = sheight - ly } + lwidth := maxWidth + if len(d.options) > lheight { + lwidth++ // Add space for scroll bar + } d.list.SetRect(lx, ly, lwidth, lheight) d.list.Draw(screen) } diff --git a/go.mod b/go.mod index b1f42db..4cb578f 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.12 require ( github.com/gdamore/tcell v1.3.0 github.com/lucasb-eyer/go-colorful v1.0.3 - github.com/mattn/go-runewidth v0.0.7 + github.com/mattn/go-runewidth v0.0.8 github.com/rivo/uniseg v0.1.0 - golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 // indirect + golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 // indirect golang.org/x/text v0.3.2 // indirect ) diff --git a/go.sum b/go.sum index 125a6d7..b652936 100644 --- a/go.sum +++ b/go.sum @@ -10,14 +10,14 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 h1:/W9OPMnnpmFXHYkcp2rQsbFUbRlRzfECQjmAFiOyHE8= -golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= diff --git a/inputfield.go b/inputfield.go index 6e8719f..da1af4d 100644 --- a/inputfield.go +++ b/inputfield.go @@ -422,6 +422,9 @@ func (i *InputField) Draw(screen tcell.Screen) { if ly+lheight >= sheight { lheight = sheight - ly } + if i.autocompleteList.GetItemCount() > lheight { + lwidth++ // Add space for scroll bar + } i.autocompleteList.SetRect(lx, ly, lwidth, lheight) i.autocompleteList.Draw(screen) } diff --git a/list.go b/list.go index b3dad8a..a1cf37c 100644 --- a/list.go +++ b/list.go @@ -42,6 +42,12 @@ type List struct { // The text color for selected items. selectedTextColor tcell.Color + // Whether or not to render a scroll bar. + showScrollBar bool + + // The scroll bar color. + scrollBarColor tcell.Color + // The background color for selected items. selectedBackgroundColor tcell.Color @@ -74,11 +80,13 @@ func NewList() *List { return &List{ Box: NewBox(), showSecondaryText: true, + showScrollBar: true, wrapAround: true, mainTextColor: Styles.PrimaryTextColor, secondaryTextColor: Styles.TertiaryTextColor, shortcutColor: Styles.SecondaryTextColor, selectedTextColor: Styles.PrimitiveBackgroundColor, + scrollBarColor: Styles.ScrollBarColor, selectedBackgroundColor: Styles.PrimaryTextColor, } } @@ -216,6 +224,19 @@ func (l *List) ShowSecondaryText(show bool) *List { return l } +// ShowScrollBar determines whether or not to render a scroll bar when there +// are additional items offscreen. +func (l *List) ShowScrollBar(show bool) *List { + l.showScrollBar = show + return l +} + +// SetScrollBarColor sets the color of the scroll bar. +func (l *List) SetScrollBarColor(color tcell.Color) *List { + l.scrollBarColor = color + return l +} + // SetWrapAround sets the flag that determines whether navigating the list will // wrap around. That is, navigating downwards on the last item will move the // selection to the first item (similarly in the other direction). If set to @@ -395,6 +416,18 @@ func (l *List) Draw(screen tcell.Screen) { x, y, width, height := l.GetInnerRect() bottomLimit := y + height + screenWidth, _ := screen.Size() + scrollBarHeight := height + scrollBarX := x + (width - 1) + if scrollBarX > screenWidth-1 { + scrollBarX = screenWidth - 1 + } + + // Halve scroll bar height when drawing two lines per list item. + if l.showSecondaryText { + scrollBarHeight /= 2 + } + // Do we show any shortcuts? var showShortcuts bool for _, item := range l.items { @@ -457,6 +490,10 @@ func (l *List) Draw(screen tcell.Screen) { } } + if l.showScrollBar { + RenderScrollBar(screen, scrollBarX, y, scrollBarHeight, len(l.items), l.currentItem, index-l.offset, l.hasFocus, l.scrollBarColor) + } + y++ if y >= bottomLimit { @@ -466,6 +503,11 @@ func (l *List) Draw(screen tcell.Screen) { // Secondary text. if l.showSecondaryText { Print(screen, item.SecondaryText, x, y, width, AlignLeft, l.secondaryTextColor) + + if l.showScrollBar { + RenderScrollBar(screen, scrollBarX, y, scrollBarHeight, len(l.items), l.currentItem, index-l.offset, l.hasFocus, l.scrollBarColor) + } + y++ } } diff --git a/styles.go b/styles.go index e906e39..e740b92 100644 --- a/styles.go +++ b/styles.go @@ -15,6 +15,7 @@ type Theme struct { TertiaryTextColor tcell.Color // Tertiary text (e.g. subtitles, notes). InverseTextColor tcell.Color // Text on primary-colored backgrounds. ContrastSecondaryTextColor tcell.Color // Secondary text on ContrastBackgroundColor-colored backgrounds. + ScrollBarColor tcell.Color // Scroll bar. } // Styles defines the theme for applications. The default is for a black @@ -32,4 +33,5 @@ var Styles = Theme{ TertiaryTextColor: tcell.ColorGreen, InverseTextColor: tcell.ColorBlue, ContrastSecondaryTextColor: tcell.ColorDarkCyan, + ScrollBarColor: tcell.ColorWhite, } diff --git a/table.go b/table.go index cfc80c1..2787c12 100644 --- a/table.go +++ b/table.go @@ -251,6 +251,12 @@ type Table struct { // The number of visible rows the last time the table was drawn. visibleRows int + // Whether or not to render a scroll bar. + showScrollBar bool + + // The scroll bar color. + scrollBarColor tcell.Color + // The style of the selected rows. If this value is 0, selected rows are // simply inverted. selectedStyle tcell.Style @@ -273,10 +279,12 @@ type Table struct { // NewTable returns a new table. func NewTable() *Table { return &Table{ - Box: NewBox(), - bordersColor: Styles.GraphicsColor, - separator: ' ', - lastColumn: -1, + Box: NewBox(), + showScrollBar: true, + scrollBarColor: Styles.ScrollBarColor, + bordersColor: Styles.GraphicsColor, + separator: ' ', + lastColumn: -1, } } @@ -300,6 +308,19 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table { return t } +// ShowScrollBar determines whether or not to render a scroll bar when there +// are additional rows and/or columns offscreen. +func (t *Table) ShowScrollBar(show bool) *Table { + t.showScrollBar = show + return t +} + +// SetScrollBarColor sets the color of the scroll bar. +func (t *Table) SetScrollBarColor(color tcell.Color) *Table { + t.scrollBarColor = color + return t +} + // SetSelectedStyle sets a specific style for selected cells. If no such style // is set, per default, selected cells are inverted (i.e. their foreground and // background colors are swapped). @@ -570,6 +591,11 @@ func (t *Table) Draw(screen tcell.Screen) { t.visibleRows = height } + showVerticalScrollBar := t.showScrollBar && len(t.cells) > height + if showVerticalScrollBar { + width-- // Subtract space for scroll bar. + } + // Return the cell at the specified position (nil if it doesn't exist). getCell := func(row, column int) *TableCell { if row < 0 || column < 0 || row >= len(t.cells) || column >= len(t.cells[row]) { @@ -861,6 +887,19 @@ ColumnLoop: } } + if showVerticalScrollBar { + // Calculate scroll bar position. + rows := len(t.cells) + cursor := int(float64(rows-t.fixedRows) * (float64(t.rowOffset) / float64(((rows-t.fixedRows)-t.visibleRows)+1))) + + // Draw scroll bar. + for printed := 0; printed < (t.visibleRows - t.fixedRows); printed++ { + RenderScrollBar(screen, x+width, y+t.fixedRows+printed, t.visibleRows-t.fixedRows, rows-t.fixedRows, cursor, printed, t.hasFocus, t.scrollBarColor) + } + } + + // TODO Draw horizontal scroll bar + // Helper function which colors the background of a box. // backgroundColor == tcell.ColorDefault => Don't color the background. // textColor == tcell.ColorDefault => Don't change the text color. diff --git a/treeview.go b/treeview.go index fe84697..1e895bc 100644 --- a/treeview.go +++ b/treeview.go @@ -270,6 +270,12 @@ type TreeView struct { // The color of the lines. graphicsColor tcell.Color + // Whether or not to render a scroll bar. + showScrollBar bool + + // The scroll bar color. + scrollBarColor tcell.Color + // An optional function which is called when the user has navigated to a new // tree node. changed func(node *TreeNode) @@ -288,9 +294,11 @@ type TreeView struct { // NewTreeView returns a new tree view. func NewTreeView() *TreeView { return &TreeView{ - Box: NewBox(), - graphics: true, - graphicsColor: Styles.GraphicsColor, + Box: NewBox(), + showScrollBar: true, + graphics: true, + graphicsColor: Styles.GraphicsColor, + scrollBarColor: Styles.ScrollBarColor, } } @@ -365,6 +373,19 @@ func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView { return t } +// ShowScrollBar determines whether or not to render a scroll bar when there +// are additional nodes offscreen. +func (t *TreeView) ShowScrollBar(show bool) *TreeView { + t.showScrollBar = show + return t +} + +// SetScrollBarColor sets the color of the scroll bar. +func (t *TreeView) SetScrollBarColor(color tcell.Color) *TreeView { + t.scrollBarColor = color + return t +} + // SetChangedFunc sets the function which is called when the user navigates to // a new tree node. func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView { @@ -601,12 +622,16 @@ func (t *TreeView) Draw(screen tcell.Screen) { t.offsetY = 0 } + // Calculate scroll bar position. + rows := len(t.nodes) + cursor := int(float64(rows) * (float64(t.offsetY) / float64(rows-height))) + // Draw the tree. posY := y lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor) for index, node := range t.nodes { // Skip invisible parts. - if posY >= y+height+1 { + if posY >= y+height { break } if index < t.offsetY { @@ -668,6 +693,11 @@ func (t *TreeView) Draw(screen tcell.Screen) { } } + // Draw scroll bar. + if t.showScrollBar { + RenderScrollBar(screen, x+(width-1), posY, height, rows, cursor, posY-y, t.hasFocus, tcell.ColorWhite) + } + // Advance. posY++ } diff --git a/util.go b/util.go index 810e0e1..000b7fa 100644 --- a/util.go +++ b/util.go @@ -630,3 +630,36 @@ func iterateStringReverse(text string, callback func(main rune, comb []rune, tex return false } + +// RenderScrollBar renders a scroll bar character at the specified position. +func RenderScrollBar(screen tcell.Screen, x int, y int, height int, items int, cursor int, printed int, focused bool, color tcell.Color) { + // Do not render a scroll bar when all items are visible. + if items <= height { + return + } + + // Handle negative cursor. + if cursor < 0 { + cursor = 0 + } + + // Calculate handle position. + handlePosition := int(float64(height-1) * (float64(cursor) / float64(items-1))) + + // Print character. + var scrollBar string + if printed == handlePosition { + if focused { + scrollBar = "[::r] [-:-:-]" + } else { + scrollBar = "▓" + } + } else { + if focused { + scrollBar = "▒" + } else { + scrollBar = "░" + } + } + Print(screen, scrollBar, x, y, 1, AlignLeft, color) +}