mirror of
https://github.com/jroimartin/gocui.git
synced 2025-04-26 13:48:49 +08:00
add scrollbar
This commit is contained in:
parent
2eb424ce3d
commit
ae531166c8
110
gui.go
110
gui.go
@ -743,6 +743,8 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showScrollbar, realScrollbarStart, realScrollbarEnd := calcRealScrollbarStartEnd(v)
|
||||
for y := v.y0 + 1; y < v.y1 && y < g.maxY; y++ {
|
||||
if y < 0 {
|
||||
continue
|
||||
@ -753,7 +755,9 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
|
||||
}
|
||||
}
|
||||
if v.x1 > -1 && v.x1 < g.maxX {
|
||||
if err := g.SetRune(v.x1, y, runeV, fgColor, bgColor); err != nil {
|
||||
runeToPrint := calcScrollbarRune(showScrollbar, realScrollbarStart, realScrollbarEnd, v.y0+1, v.y1-1, y, runeV)
|
||||
|
||||
if err := g.SetRune(v.x1, y, runeToPrint, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -761,6 +765,44 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func calcScrollbarRune(showScrollbar bool, scrollbarStart int, scrollbarEnd int, rangeStart int, rangeEnd int, position int, runeV rune) rune {
|
||||
if !showScrollbar {
|
||||
return runeV
|
||||
} else if position == rangeStart {
|
||||
return '▲'
|
||||
} else if position == rangeEnd {
|
||||
return '▼'
|
||||
} else if position > scrollbarStart && position < scrollbarEnd {
|
||||
return '█'
|
||||
} else if position > rangeStart && position < rangeEnd {
|
||||
// keeping this as a separate branch in case we later want to render something different here.
|
||||
return runeV
|
||||
} else {
|
||||
return runeV
|
||||
}
|
||||
}
|
||||
|
||||
func calcRealScrollbarStartEnd(v *View) (bool, int, int) {
|
||||
height := v.InnerHeight() + 1
|
||||
fullHeight := v.ViewLinesHeight()
|
||||
|
||||
if v.CanScrollPastBottom {
|
||||
fullHeight += height - 2
|
||||
}
|
||||
|
||||
if height < 2 || height >= fullHeight {
|
||||
return false, 0, 0
|
||||
}
|
||||
|
||||
originY := v.OriginY()
|
||||
scrollbarStart, scrollbarHeight := calcScrollbar(fullHeight, height, originY, height-1)
|
||||
top := v.y0 + 1
|
||||
realScrollbarStart := top + scrollbarStart
|
||||
realScrollbarEnd := realScrollbarStart + scrollbarHeight
|
||||
|
||||
return true, realScrollbarStart, realScrollbarEnd
|
||||
}
|
||||
|
||||
func cornerRune(index byte) rune {
|
||||
return []rune{' ', '│', '│', '│', '─', '┘', '┐', '┤', '─', '└', '┌', '├', '├', '┴', '┬', '┼'}[index]
|
||||
}
|
||||
@ -1014,6 +1056,40 @@ func (g *Gui) draw(v *View) error {
|
||||
if !v.Visible || v.y1 < v.y0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if g.Cursor {
|
||||
if curview := g.currentView; curview != nil {
|
||||
vMaxX, vMaxY := curview.Size()
|
||||
if curview.cx < 0 {
|
||||
curview.cx = 0
|
||||
} else if curview.cx >= vMaxX {
|
||||
curview.cx = vMaxX - 1
|
||||
}
|
||||
if curview.cy < 0 {
|
||||
curview.cy = 0
|
||||
} else if curview.cy >= vMaxY {
|
||||
curview.cy = vMaxY - 1
|
||||
}
|
||||
|
||||
gMaxX, gMaxY := g.Size()
|
||||
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
|
||||
// This test probably doesn't need to be here.
|
||||
// tcell is hiding cursor by setting coordinates outside of screen.
|
||||
// Keeping it here for now, as I'm not 100% sure :)
|
||||
if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY {
|
||||
Screen.ShowCursor(cx, cy)
|
||||
} else {
|
||||
Screen.HideCursor()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Screen.HideCursor()
|
||||
}
|
||||
|
||||
if err := v.draw(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v.Frame {
|
||||
var fgColor, bgColor, frameColor Attribute
|
||||
if g.Highlight && v == g.currentView {
|
||||
@ -1057,38 +1133,6 @@ func (g *Gui) draw(v *View) error {
|
||||
}
|
||||
}
|
||||
|
||||
if g.Cursor {
|
||||
if curview := g.currentView; curview != nil {
|
||||
vMaxX, vMaxY := curview.Size()
|
||||
if curview.cx < 0 {
|
||||
curview.cx = 0
|
||||
} else if curview.cx >= vMaxX {
|
||||
curview.cx = vMaxX - 1
|
||||
}
|
||||
if curview.cy < 0 {
|
||||
curview.cy = 0
|
||||
} else if curview.cy >= vMaxY {
|
||||
curview.cy = vMaxY - 1
|
||||
}
|
||||
|
||||
gMaxX, gMaxY := g.Size()
|
||||
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
|
||||
// This test probably doesn't need to be here.
|
||||
// tcell is hiding cursor by setting coordinates outside of screen.
|
||||
// Keeping it here for now, as I'm not 100% sure :)
|
||||
if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY {
|
||||
Screen.ShowCursor(cx, cy)
|
||||
} else {
|
||||
Screen.HideCursor()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Screen.HideCursor()
|
||||
}
|
||||
|
||||
if err := v.draw(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
33
scrollbar.go
Normal file
33
scrollbar.go
Normal file
@ -0,0 +1,33 @@
|
||||
package gocui
|
||||
|
||||
import "math"
|
||||
|
||||
// returns start and height of scrollbar
|
||||
// `max` is the maximum possible value of `position`
|
||||
func calcScrollbar(listSize int, pageSize int, position int, scrollAreaSize int) (int, int) {
|
||||
height := calcScrollbarHeight(listSize, pageSize, scrollAreaSize)
|
||||
// assume we can't scroll past the last item
|
||||
maxPosition := listSize - pageSize
|
||||
if maxPosition <= 0 {
|
||||
return 0, height
|
||||
}
|
||||
if position == maxPosition {
|
||||
return scrollAreaSize - height, height
|
||||
}
|
||||
// we only want to show the scrollbar at the top or bottom positions if we're at the end. Hence the .Ceil (for moving the scrollbar once we scroll down) and the -1 (for pretending there's a smaller range than we actually have, with the above condition ensuring we snap to the bottom once we're at the end of the list)
|
||||
start := int(math.Ceil(((float64(position) / float64(maxPosition)) * float64(scrollAreaSize-height-1))))
|
||||
return start, height
|
||||
}
|
||||
|
||||
func calcScrollbarHeight(listSize int, pageSize int, scrollAreaSize int) int {
|
||||
if pageSize >= listSize {
|
||||
return scrollAreaSize
|
||||
}
|
||||
height := int((float64(pageSize) / float64(listSize)) * float64(scrollAreaSize))
|
||||
minHeight := 2
|
||||
if height < minHeight {
|
||||
return minHeight
|
||||
}
|
||||
|
||||
return height
|
||||
}
|
114
scrollbar_test.go
Normal file
114
scrollbar_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package gocui
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCalcScrollbar(t *testing.T) {
|
||||
tests := []struct {
|
||||
testName string
|
||||
listSize int
|
||||
pageSize int
|
||||
position int
|
||||
scrollAreaSize int
|
||||
|
||||
expectedStart int
|
||||
expectedHeight int
|
||||
}{
|
||||
{
|
||||
testName: "page size greater than list size",
|
||||
listSize: 5,
|
||||
pageSize: 10,
|
||||
position: 0,
|
||||
scrollAreaSize: 20,
|
||||
|
||||
expectedStart: 0,
|
||||
expectedHeight: 20,
|
||||
},
|
||||
{
|
||||
testName: "page size matches list size",
|
||||
listSize: 10,
|
||||
pageSize: 10,
|
||||
position: 0,
|
||||
scrollAreaSize: 20,
|
||||
|
||||
expectedStart: 0,
|
||||
expectedHeight: 20,
|
||||
},
|
||||
{
|
||||
testName: "page size half of list size",
|
||||
listSize: 10,
|
||||
pageSize: 5,
|
||||
position: 0,
|
||||
scrollAreaSize: 20,
|
||||
|
||||
expectedStart: 0,
|
||||
expectedHeight: 10,
|
||||
},
|
||||
{
|
||||
testName: "page size half of list size at scroll end",
|
||||
listSize: 10,
|
||||
pageSize: 5,
|
||||
position: 5,
|
||||
scrollAreaSize: 20,
|
||||
|
||||
expectedStart: 10,
|
||||
expectedHeight: 10,
|
||||
},
|
||||
{
|
||||
testName: "page size third of list size having scrolled half the way",
|
||||
listSize: 15,
|
||||
// Recall that my max position is listSize - pageSize i.e 15 - 5 i.e. 10.
|
||||
// So if I've scrolled to position 5 that means I've done one page and I've got
|
||||
// one page to go which means by scrollbar should take up a third of the available
|
||||
// space and appear in the centre of the scrollbar area
|
||||
pageSize: 5,
|
||||
position: 5,
|
||||
scrollAreaSize: 21,
|
||||
|
||||
expectedStart: 7,
|
||||
expectedHeight: 7,
|
||||
},
|
||||
{
|
||||
testName: "page size third of list size having scrolled the full way",
|
||||
listSize: 15,
|
||||
pageSize: 5,
|
||||
position: 10,
|
||||
scrollAreaSize: 21,
|
||||
|
||||
expectedStart: 14,
|
||||
expectedHeight: 7,
|
||||
},
|
||||
{
|
||||
testName: "page size third of list size having scrolled by one",
|
||||
listSize: 15,
|
||||
pageSize: 5,
|
||||
position: 1,
|
||||
scrollAreaSize: 21,
|
||||
|
||||
expectedStart: 2,
|
||||
expectedHeight: 7,
|
||||
},
|
||||
{
|
||||
testName: "page size third of list size having scrolled up from the bottom by one",
|
||||
listSize: 15,
|
||||
pageSize: 5,
|
||||
position: 9,
|
||||
scrollAreaSize: 21,
|
||||
|
||||
expectedStart: 12,
|
||||
expectedHeight: 7,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
start, height := calcScrollbar(test.listSize, test.pageSize, test.position, test.scrollAreaSize)
|
||||
if start != test.expectedStart {
|
||||
t.Errorf("expected start to be %d, got %d", test.expectedStart, start)
|
||||
}
|
||||
|
||||
if height != test.expectedHeight {
|
||||
t.Errorf("expected height to be %d, got %d", test.expectedHeight, height)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
64
view.go
64
view.go
@ -158,6 +158,9 @@ type View struct {
|
||||
|
||||
// something like '1 of 20' for a list view
|
||||
Footer string
|
||||
|
||||
// if true, the user can scroll all the way past the last item until it appears at the top of the view
|
||||
CanScrollPastBottom bool
|
||||
}
|
||||
|
||||
// call this in the event of a view resize, or if you want to render new content
|
||||
@ -1287,3 +1290,64 @@ func (v *View) OverwriteLines(y int, content string) {
|
||||
lines := strings.Replace(content, "\n", "\x1b[K\n", -1)
|
||||
v.writeString(lines)
|
||||
}
|
||||
|
||||
func (v *View) ScrollUp(amount int) {
|
||||
newOy := v.oy - amount
|
||||
if newOy < 0 {
|
||||
newOy = 0
|
||||
}
|
||||
v.oy = newOy
|
||||
}
|
||||
|
||||
// ensures we don't scroll past the end of the view's content
|
||||
func (v *View) ScrollDown(amount int) {
|
||||
adjustedAmount := v.adjustDownwardScrollAmount(amount)
|
||||
if adjustedAmount > 0 {
|
||||
v.oy += adjustedAmount
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) ScrollLeft(amount int) {
|
||||
newOx := v.ox - amount
|
||||
if newOx < 0 {
|
||||
newOx = 0
|
||||
}
|
||||
v.ox = newOx
|
||||
}
|
||||
|
||||
// not applying any limits to this
|
||||
func (v *View) ScrollRight(amount int) {
|
||||
v.ox += amount
|
||||
}
|
||||
|
||||
func (v *View) adjustDownwardScrollAmount(scrollHeight int) int {
|
||||
_, oy := v.Origin()
|
||||
y := oy
|
||||
if !v.CanScrollPastBottom {
|
||||
_, sy := v.Size()
|
||||
y += sy
|
||||
}
|
||||
scrollableLines := v.ViewLinesHeight() - y
|
||||
if scrollableLines < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// margin is about how many lines must still appear if you scroll
|
||||
// all the way down.
|
||||
margin := 0
|
||||
if v.CanScrollPastBottom {
|
||||
// Setting to 2 because of the newline at the end of the file that we're likely showing.
|
||||
// If we want to scroll past bottom outside the context of reading a file's contents,
|
||||
// we should make this into a field on the view to be configured by the client.
|
||||
// For now we're hardcoding it.
|
||||
margin = 2
|
||||
}
|
||||
if scrollableLines-margin < scrollHeight {
|
||||
scrollHeight = scrollableLines - margin
|
||||
}
|
||||
if oy+scrollHeight < 0 {
|
||||
return 0
|
||||
} else {
|
||||
return scrollHeight
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user