From 4c0a19f6a12d976ef6bfe9fb32a2b7d41dda915b Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Fri, 31 Jan 2020 21:21:59 -0800 Subject: [PATCH] Add scroll bar to List, DropDown, Table and TreeView --- dropdown.go | 5 +++- inputfield.go | 3 +++ list.go | 41 ++++++++++++++++++++++++++++++++ styles.go | 2 ++ table.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++---- treeview.go | 37 +++++++++++++++++++++++++---- util.go | 47 ++++++++++++++++++++++++++++++++++++ 7 files changed, 192 insertions(+), 9 deletions(-) diff --git a/dropdown.go b/dropdown.go index 58eee24..3e6e75a 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 d.list.scrollBarVisibility == ScrollBarAlways || (d.list.scrollBarVisibility == ScrollBarAuto && len(d.options) > lheight) { + lwidth++ // Add space for scroll bar + } d.list.SetRect(lx, ly, lwidth, lheight) d.list.Draw(screen) } diff --git a/inputfield.go b/inputfield.go index 3cd255e..4a4e378 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.scrollBarVisibility == ScrollBarAlways || (i.autocompleteList.scrollBarVisibility == ScrollBarAuto && 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 14ecf43..e7e21da 100644 --- a/list.go +++ b/list.go @@ -42,6 +42,12 @@ type List struct { // The text color for selected items. selectedTextColor tcell.Color + // Visibility of the scroll bar. + scrollBarVisibility ScrollBarVisibility + + // The scroll bar color. + scrollBarColor tcell.Color + // The background color for selected items. selectedBackgroundColor tcell.Color @@ -75,10 +81,12 @@ func NewList() *List { Box: NewBox(), showSecondaryText: true, wrapAround: true, + scrollBarVisibility: ScrollBarAuto, mainTextColor: Styles.PrimaryTextColor, secondaryTextColor: Styles.TertiaryTextColor, shortcutColor: Styles.SecondaryTextColor, selectedTextColor: Styles.PrimitiveBackgroundColor, + scrollBarColor: Styles.ScrollBarColor, selectedBackgroundColor: Styles.PrimaryTextColor, } } @@ -216,6 +224,18 @@ func (l *List) ShowSecondaryText(show bool) *List { return l } +// SetScrollBarVisibility specifies the display of the scroll bar. +func (l *List) SetScrollBarVisibility(visibility ScrollBarVisibility) *List { + l.scrollBarVisibility = visibility + 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 +415,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 +489,10 @@ func (l *List) Draw(screen tcell.Screen) { } } + if l.scrollBarVisibility == ScrollBarAlways || (l.scrollBarVisibility == ScrollBarAuto && len(l.items) > scrollBarHeight) { + RenderScrollBar(screen, scrollBarX, y, scrollBarHeight, len(l.items), l.currentItem, index-l.offset, l.hasFocus, l.scrollBarColor) + } + y++ if y >= bottomLimit { @@ -466,6 +502,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.scrollBarVisibility == ScrollBarAlways || (l.scrollBarVisibility == ScrollBarAuto && len(l.items) > scrollBarHeight) { + 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 4f0448f..82cdb69 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 9d0db29..f7f9313 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 + // Visibility of the scroll bar. + scrollBarVisibility ScrollBarVisibility + + // 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(), + scrollBarVisibility: ScrollBarAuto, + scrollBarColor: Styles.ScrollBarColor, + bordersColor: Styles.GraphicsColor, + separator: ' ', + lastColumn: -1, } } @@ -300,6 +308,18 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table { return t } +// SetScrollBarVisibility specifies the display of the scroll bar. +func (t *Table) SetScrollBarVisibility(visibility ScrollBarVisibility) *Table { + t.scrollBarVisibility = visibility + 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 +590,11 @@ func (t *Table) Draw(screen tcell.Screen) { t.visibleRows = height } + showVerticalScrollBar := t.scrollBarVisibility == ScrollBarAlways || (t.scrollBarVisibility == ScrollBarAuto && len(t.cells) > t.visibleRows-t.fixedRows) + 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]) { @@ -766,6 +791,7 @@ ColumnLoop: toDistribute -= expWidth expansionTotal -= expansion } + tableWidth = width - toDistribute } // Helper function which draws border runes. @@ -861,6 +887,38 @@ ColumnLoop: } } + if showVerticalScrollBar { + // Calculate scroll bar position and dimensions. + rows := len(t.cells) + + scrollBarItems := rows - t.fixedRows + scrollBarHeight := t.visibleRows - t.fixedRows + + scrollBarX := x + width + scrollBarY := y + t.fixedRows + if scrollBarX > x+tableWidth { + scrollBarX = x + tableWidth + } + + padTotalOffset := 1 + if t.borders { + padTotalOffset = 2 + + scrollBarItems *= 2 + scrollBarHeight = (scrollBarHeight * 2) - 1 + + scrollBarY += t.fixedRows + 1 + } + + // Draw scroll bar. + cursor := int(float64(scrollBarItems) * (float64(t.rowOffset) / float64(((rows-t.fixedRows)-t.visibleRows)+padTotalOffset))) + for printed := 0; printed < scrollBarHeight; printed++ { + RenderScrollBar(screen, scrollBarX, scrollBarY+printed, scrollBarHeight, scrollBarItems, 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 80a4df1..008dfa2 100644 --- a/treeview.go +++ b/treeview.go @@ -270,6 +270,12 @@ type TreeView struct { // The color of the lines. graphicsColor tcell.Color + // Visibility of the scroll bar. + scrollBarVisibility ScrollBarVisibility + + // 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(), + scrollBarVisibility: ScrollBarAuto, + graphics: true, + graphicsColor: Styles.GraphicsColor, + scrollBarColor: Styles.ScrollBarColor, } } @@ -365,6 +373,18 @@ func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView { return t } +// SetScrollBarVisibility specifies the display of the scroll bar. +func (t *TreeView) SetScrollBarVisibility(visibility ScrollBarVisibility) *TreeView { + t.scrollBarVisibility = visibility + 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 +621,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 +692,11 @@ func (t *TreeView) Draw(screen tcell.Screen) { } } + // Draw scroll bar. + if t.scrollBarVisibility == ScrollBarAlways || (t.scrollBarVisibility == ScrollBarAuto && rows > height) { + 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 975f087..c3977fb 100644 --- a/util.go +++ b/util.go @@ -628,3 +628,50 @@ func iterateStringReverse(text string, callback func(main rune, comb []rune, tex return false } + +// ScrollBarVisibility specifies the display of a scroll bar. +type ScrollBarVisibility int + +const ( + // ScrollBarNever never shows a scroll bar. + ScrollBarNever ScrollBarVisibility = iota + + // ScrollBarAuto shows a scroll bar when there are items offscreen. + ScrollBarAuto + + // ScrollBarAlways always shows a scroll bar. + ScrollBarAlways +) + +// 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) +}