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 }