tview icon indicating copy to clipboard operation
tview copied to clipboard

Add scroll bar to List, DropDown, Table and TreeView

Open tslocum opened this issue 4 years ago • 4 comments

Scroll bars are drawn when at least one item or line is offscreen.

A horizontal scroll bar has not yet been implemented for Table.

Live demo: ssh cview.rocketnine.space -p 20000

cview_scrollbar

tslocum avatar Feb 01 '20 05:02 tslocum

Maybe scroll bars length could represent visible lines count compared to all lines as in most GUIs? That is, the the more lines, the smaller the scroll bar. It could be further resized using block elements.

░░░░░░░░▆
░░░░░░░░▔
░░░░░░░░
░░░░░░░░

gnojus avatar Feb 21 '20 20:02 gnojus

Hey @nojus297, that should be possible. I don't plan on implementing more complex rendering/styling myself but would gladly merge such changes into cview if you or someone else proposes them.

I don't plan on implementing horizontal scrolling for Tables at this time. Any help with wrapping this up would be appreciated.

tslocum avatar Apr 14 '20 16:04 tslocum

This is my implementation of a scrollbar. Supports horizontal ones too and divides uses eights of a block. Horizontal scrollbars are a quite thick though.

package tview

import (
	"math"

	"github.com/gdamore/tcell"
)

// Direction of an item (DirectionHorizontal or DirectionVertical). Used for
// scrollbar.
type Direction int

// Configuration values.
const (
	DirectionHorizontal = iota
	DirectionVertical
)

// ScrollbarVisibility specifies the visibility of a scroll bar.
type ScrollbarVisibility int

const (
	// ScrollbarNever never show a scroll bar.
	ScrollbarNever ScrollbarVisibility = iota

	// ScrollbarAuto show a scroll bar when there are items offscreen.
	ScrollbarAuto

	// ScrollbarAlways always show a scroll bar.
	ScrollbarAlways
)

// The amount of blocks in which a single cell is divided.
const subblocks = 8

var verticalBlocks = [subblocks + 1]rune{
	'\u0020', // space
	'\u2581', // ▁
	'\u2582', // ▂
	'\u2583', // ▃
	'\u2584', // ▄
	'\u2585', // ▅
	'\u2586', // ▆
	'\u2587', // ▇
	'\u2588', // █
}

var horizontalBlocks = [subblocks + 1]rune{
	'\u0020', // space
	'\u258F', // ▏
	'\u258E', // ▎
	'\u258D', // ▍
	'\u258C', // ▌
	'\u258B', // ▋
	'\u258A', // ▊
	'\u2589', // ▉
	'\u2588', // █
}

type Scrollbar struct {
	// The coordinates where scrollbar starts.
	x, y int

	// The physical length of the scrollbar.
	length int

	// Direction of the scrollbar.
	direction Direction

	// Total size of the buffer that this scrollbar represents.
	totalItems int

	// Range that is visible from the whole buffer for the viewer.
	visibleFrom, visibleTo int

	// Colors of the scrollbar.
	color, backgroundColor tcell.Color
}

// NewScrollbar returns a new vertical scrollbar.
func NewScrollbar() *Scrollbar {
	return &Scrollbar{
		direction:       DirectionVertical,
		color:           Styles.ScrollbarColor,
		backgroundColor: Styles.ScrollbarBackgroundColor,
	}
}

// SetDirection sets the direction in which the scrollbar is drawn. Can
// be DirectionVertical (default) or DirectionHorizontal.
func (s *Scrollbar) SetDirection(direction Direction) *Scrollbar {
	s.direction = direction
	return s
}

// SetColor sets the color of the scrollbar.
func (s *Scrollbar) SetColor(color tcell.Color) *Scrollbar {
	s.color = color
	return s
}

// SetColor sets the color of the scrollbar's background.
func (s *Scrollbar) SetBackgroundColor(color tcell.Color) *Scrollbar {
	s.backgroundColor = color
	return s
}

// SetPosition sets the start coordinates and length of the scrollbar.
func (s *Scrollbar) SetPosition(x, y, length int) *Scrollbar {
	s.x = x
	s.y = y
	s.length = length
	return s
}

// SetTotalItems sets the total amount of item which need scrollbar.
func (s *Scrollbar) SetTotalItems(items int) *Scrollbar {
	s.totalItems = items
	return s
}

// SetStatus sets the visible range of total items [firstVisible, lastVisible].
// Items are zero indexed, so if first 10 out 20 lines are visible, one should
// call SetStatus(0, 9).
func (s *Scrollbar) SetStatus(firstVisible, lastVisible int) *Scrollbar {
	s.visibleFrom = firstVisible
	s.visibleTo = lastVisible
	return s
}

// Draw draws the scrollbar on the previously
func (s *Scrollbar) Draw(screen tcell.Screen) *Scrollbar {
	if s.length <= 0 {
		return s
	}
	// Convertion from items to physical cells divided into subblocks.
	convertToSubblocks := func(value int) int {
		return int(math.Round(float64(value*s.length*subblocks) / float64(s.totalItems)))
	}
	startPos := convertToSubblocks(s.visibleFrom)
	endPos := startPos + convertToSubblocks(s.visibleTo-s.visibleFrom+1)
	if endPos-startPos < subblocks {
		// We cannot display a bar smaller than a single cell, therefore we
		// increase it centering on the center of the too small bar.
		mid := (startPos + endPos) / 2
		startPos = mid - subblocks/2
		endPos = mid + subblocks/2 + subblocks%2
	}
	for i := 0; i < s.length; i++ {
		// We need to invert the character if the upper/left part of the cell
		// should be rendered as block.
		inverted := startPos <= i*subblocks
		// The count of subblocks in current cell. Intersection of two ranges.
		blockCount := min(endPos, (i+1)*subblocks) - max(startPos, i*subblocks)
		blockCount = max(blockCount, 0)
		if inverted {
			blockCount = subblocks - blockCount
		}
		block := verticalBlocks[blockCount]
		if s.direction == DirectionHorizontal {
			block = horizontalBlocks[blockCount]
		}

		style := tcell.StyleDefault.Background(s.backgroundColor).Foreground(s.color).Reverse(inverted)
		if s.direction == DirectionVertical {
			screen.SetContent(s.x, s.y+i, block, nil, style)
		} else {
			screen.SetContent(s.x+i, s.y, block, nil, style)
		}
	}
	return s
}

gnojus avatar Apr 15 '20 21:04 gnojus

I am currently thinking of ripping off the AutoScrollbar entirely, as as it is really hard to implement efficiently with dynamic content.

gnojus avatar Apr 24 '20 08:04 gnojus