Skip to content

Add scroll bar to List, DropDown, Table and TreeView#396

Open
tslocum wants to merge 1 commit intorivo:masterfrom
tslocum:scrollbar
Open

Add scroll bar to List, DropDown, Table and TreeView#396
tslocum wants to merge 1 commit intorivo:masterfrom
tslocum:scrollbar

Conversation

@tslocum
Copy link
Copy Markdown
Contributor

@tslocum tslocum commented Feb 1, 2020

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 tslocum changed the title WIP Add scroll bar to List Add scroll bar to List, DropDown, Table and TreeView Feb 1, 2020
@tslocum tslocum force-pushed the scrollbar branch 2 times, most recently from ec4533b to 4c0a19f Compare February 13, 2020 23:10
@gnojus
Copy link
Copy Markdown

gnojus commented Feb 21, 2020

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.

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

@tslocum
Copy link
Copy Markdown
Contributor Author

tslocum commented Apr 14, 2020

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.

@gnojus
Copy link
Copy Markdown

gnojus commented Apr 15, 2020

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
Copy link
Copy Markdown

gnojus commented Apr 24, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants