tui/views/view_scroll.go
2023-04-24 11:55:04 +02:00

144 lines
3.7 KiB
Go

package views
import (
"math"
"git.milar.in/milarin/buf2d"
"git.milar.in/milarin/gmath"
"git.milar.in/milarin/tui"
"github.com/gdamore/tcell"
)
// ScrollView is a tui.View which can hold an arbitrary large view.
// If the sub view does not fit into its bounds, scroll bars will be shown
type ScrollView struct {
tui.WrapperTmpl
buf *tui.ViewBuffer
width, height int
verticalScrollOffset int
horizontalScrollOffset int
}
var _ tui.Wrapper = &ScrollView{}
func NewScrollView(view tui.View) *ScrollView {
v := new(ScrollView)
v.SetView(view)
return v
}
func (v *ScrollView) Draw(buf *tui.ViewBuffer) {
v.ViewTmpl.Draw(buf)
w, h := v.View().Layout()
if v.buf == nil || v.buf.Width() != w || v.buf.Height() != h {
v.buf = buf2d.NewBuffer(w, h, tui.DefaultRune)
}
v.Scroll(0, 0) // limit scroll offset boundaries
v.View().Draw(v.buf)
scrollH, scrollV := v.determineViewportSize(buf)
copyBufferWidth, copyBufferHeight := 0, 0
if scrollH {
copyBufferWidth = v.width
} else {
copyBufferWidth = v.buf.Width()
}
if scrollV {
copyBufferHeight = v.height
} else {
copyBufferHeight = v.buf.Height()
}
// copy buffer
for x := 0; x < copyBufferWidth; x++ {
for y := 0; y < copyBufferHeight; y++ {
buf.Set(x, y, v.buf.Get(v.horizontalScrollOffset+x, v.verticalScrollOffset+y))
}
}
scrollVHeight := int(float64(buf.Height()) / float64(v.buf.Height()) * float64(v.height))
scrollVStart := int(math.Ceil(float64(v.verticalScrollOffset) / float64(v.buf.Height()) * float64(v.height)))
scrollHStart := int(float64(v.horizontalScrollOffset) / float64(v.buf.Width()) * float64(v.width))
scrollHWidth := int(math.Ceil(float64(buf.Width()) / float64(v.buf.Width()) * float64(v.width)))
// guarantee minimum scroll bar thumb size of 1 TODO inaccurate for small scales
if scrollVHeight <= 0 {
scrollVHeight = 1
if scrollVStart >= v.height {
scrollVStart = v.height - 1
}
}
if scrollHWidth <= 0 {
scrollHWidth = 1
if scrollHStart >= v.width {
scrollHStart = v.width - 1
}
}
// vertical scrollbar
if scrollV {
for y := 0; y < v.height; y++ {
var style tcell.Style
if y >= scrollVStart && y < scrollVStart+scrollVHeight {
style = tui.StyleDefault.Background(tcell.ColorWhite)
} else {
style = tui.StyleDefault.Background(tcell.ColorDarkSlateGray)
}
buf.Set(v.width, y, tui.Rune{Rn: ' ', Style: style})
}
}
// horizontal scrollbar
if scrollH {
for x := 0; x < v.width; x++ {
var style tcell.Style
if x >= scrollHStart && x < scrollHStart+scrollHWidth {
style = tui.StyleDefault.Background(tcell.ColorWhite)
} else {
style = tui.StyleDefault.Background(tcell.ColorDarkSlateGray)
}
buf.Set(x, v.height, tui.Rune{Rn: ' ', Style: style})
}
}
}
func (v *ScrollView) Layout() (prefWidth, prefHeight int) {
return -1, -1
}
func (v *ScrollView) Scroll(verticalOffset, horizontalOffset int) {
if v.buf != nil {
v.verticalScrollOffset = gmath.Clamp(v.verticalScrollOffset+verticalOffset, 0, gmath.Max(v.buf.Height()-v.height, 0))
v.horizontalScrollOffset = gmath.Clamp(v.horizontalScrollOffset+horizontalOffset, 0, gmath.Max(v.buf.Width()-v.width, 0))
} else {
v.verticalScrollOffset = v.verticalScrollOffset + verticalOffset
v.horizontalScrollOffset = v.horizontalScrollOffset + horizontalOffset
}
}
func (v *ScrollView) determineViewportSize(buf *tui.ViewBuffer) (scrollbarH, scrollbarV bool) {
v.width, v.height = buf.Width()-1, buf.Height()-1
scrollbarV = v.buf.Height() > v.height
scrollbarH = v.buf.Width() > v.width
if scrollbarV && !scrollbarH {
v.height++
scrollbarV = v.buf.Height() > v.height
}
if !scrollbarV && scrollbarH {
v.width++
scrollbarH = v.buf.Width() > v.width
}
return
}