package views import ( "image" "math" "time" "git.milar.in/milarin/gmath" "git.milar.in/milarin/gui" "github.com/hajimehoshi/ebiten/v2" ) // ScrollView is a gui.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 { gui.WrapperTmpl buf *gui.Image viewportWidth, viewportHeight int viewWidth, viewHeight int VerticalScrollOffset float64 HorizontalScrollOffset float64 ScrollStep int targetOffset gui.Point ScrollSpeed int lastDraw time.Time verticalScrollBar *ScrollbarView horizontalScrollBar *ScrollbarView } var _ gui.Wrapper = &ScrollView{} func NewScrollView(view gui.View) *ScrollView { v := new(ScrollView) v.SetView(view) v.ScrollStep = 100 v.ScrollSpeed = 1000 v.lastDraw = time.Now() v.verticalScrollBar = NewScrollbarView(gui.Vertical) v.horizontalScrollBar = NewScrollbarView(gui.Horizontal) return v } func (v *ScrollView) update(deltaTime float64) { deltaSpeed := deltaTime * float64(v.ScrollSpeed) if math.Abs(v.VerticalScrollOffset-float64(v.targetOffset.Y)) >= deltaSpeed { vDir := iff(v.VerticalScrollOffset-float64(v.targetOffset.Y) < 0, 1.0, -1.0) v.VerticalScrollOffset += deltaSpeed * vDir } else { v.VerticalScrollOffset = float64(v.targetOffset.Y) } if math.Abs(v.HorizontalScrollOffset-float64(v.targetOffset.X)) >= deltaSpeed { hDir := iff(v.HorizontalScrollOffset-float64(v.targetOffset.X) < 0, 1.0, -1.0) v.HorizontalScrollOffset += deltaSpeed * hDir } else { v.HorizontalScrollOffset = float64(v.targetOffset.X) } } func (v *ScrollView) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) { return -1, -1 } func (v *ScrollView) Draw(img *gui.Image, ctx gui.AppContext) { now := time.Now() deltaTime := now.Sub(v.lastDraw).Seconds() v.lastDraw = now v.viewWidth = img.Bounds().Dx() v.viewHeight = img.Bounds().Dy() v.update(deltaTime) v.ViewTmpl.Draw(img, ctx) w, h := v.View().Layout(ctx) w = iff(w >= 0, w, img.Bounds().Dx()-v.verticalScrollBar.Thickness) h = iff(h >= 0, h, img.Bounds().Dy()-v.horizontalScrollBar.Thickness) if v.buf == nil || v.buf.Bounds().Dx() != w || v.buf.Bounds().Dy() != h { v.buf = gui.NewImage(w, h) } v.limit() v.View().Draw(v.buf, ctx) scrollH, scrollV := v.determineViewportSize(img) copyBufferWidth, copyBufferHeight := 0, 0 if scrollH { copyBufferWidth = v.viewportWidth } else { copyBufferWidth = v.buf.Bounds().Dx() } if scrollV { copyBufferHeight = v.viewportHeight } else { copyBufferHeight = v.buf.Bounds().Dy() } // copy buffer min := image.Pt(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset)) size := image.Pt(copyBufferWidth, copyBufferHeight) max := min.Add(size) rect := image.Rectangle{min, max} op := new(ebiten.DrawImageOptions) //op.GeoM.Translate(-float64(min.X), -float64(min.Y)) img.DrawImage(v.buf.SubImage(rect).(*gui.Image), op) //-min.X, -min.Y) if scrollV { v.verticalScrollBar.ViewSize = v.buf.Bounds().Dy() v.verticalScrollBar.ViewportSize = size.Y v.verticalScrollBar.ViewportPos = int(v.VerticalScrollOffset) v.verticalScrollBar.Draw(img.SubImage(image.Rect(img.Bounds().Max.X-v.verticalScrollBar.Thickness, 0, img.Bounds().Max.X, img.Bounds().Max.Y)).(*gui.Image), ctx) } if scrollH { v.horizontalScrollBar.ViewSize = v.buf.Bounds().Dx() v.horizontalScrollBar.ViewportSize = size.X v.horizontalScrollBar.ViewportPos = int(v.HorizontalScrollOffset) v.horizontalScrollBar.Draw(img.SubImage(image.Rect(0, img.Bounds().Max.Y-v.horizontalScrollBar.Thickness, img.Bounds().Max.X, img.Bounds().Max.Y)).(*gui.Image), ctx) } } func (v *ScrollView) limit() { if v.buf != nil { v.targetOffset.Y = gmath.Clamp(v.targetOffset.Y, 0, gmath.Max(v.buf.Bounds().Dy()-v.viewportHeight, 0)) v.targetOffset.X = gmath.Clamp(v.targetOffset.X, 0, gmath.Max(v.buf.Bounds().Dx()-v.viewportWidth, 0)) v.VerticalScrollOffset = gmath.Clamp(v.VerticalScrollOffset, 0, float64(gmath.Max(v.buf.Bounds().Dy()-v.viewportHeight, 0))) v.HorizontalScrollOffset = gmath.Clamp(v.HorizontalScrollOffset, 0, float64(gmath.Max(v.buf.Bounds().Dx()-v.viewportWidth, 0))) } } func (v *ScrollView) Scroll(horizontalOffset, verticalOffset int) { v.targetOffset.Y = v.targetOffset.Y + verticalOffset v.targetOffset.X = v.targetOffset.X + horizontalOffset } func (v *ScrollView) determineViewportSize(img *gui.Image) (scrollbarH, scrollbarV bool) { v.viewportWidth, v.viewportHeight = img.Bounds().Dx()-v.verticalScrollBar.Thickness, img.Bounds().Dy()-v.horizontalScrollBar.Thickness scrollbarV = v.buf.Bounds().Dy() > v.viewportHeight scrollbarH = v.buf.Bounds().Dx() > v.viewportWidth if scrollbarV && !scrollbarH { v.viewportHeight += v.horizontalScrollBar.Thickness scrollbarV = v.buf.Bounds().Dy() > v.viewportHeight } if !scrollbarV && scrollbarH { v.viewportWidth += v.verticalScrollBar.Thickness scrollbarH = v.buf.Bounds().Dx() > v.viewportWidth } return } func (v *ScrollView) OnMouseClick(event gui.MouseEvent) (consumed bool) { return v.View().OnMouseClick(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset)))) } func (v *ScrollView) OnMouseMove(event gui.MouseEvent) (consumed bool) { if event.Position.In(gui.D(v.viewportWidth, 0, v.viewWidth-v.viewportWidth, v.viewHeight)) { return v.verticalScrollBar.OnMouseMove(event.SubtractPos(gui.P(v.viewportWidth, 0))) } if event.Position.In(gui.D(0, v.viewportHeight, v.viewWidth, v.viewHeight-v.viewportHeight)) { return v.horizontalScrollBar.OnMouseMove(event.SubtractPos(gui.P(0, v.viewportHeight))) } return v.View().OnMouseMove(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset)))) } func (v *ScrollView) OnMouseScroll(event gui.MouseEvent) (consumed bool) { if v.View().OnMouseScroll(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset)))) { return true } if event.Wheel.X != 0 || event.Wheel.Y != 0 { v.Scroll(-event.Wheel.X*v.ScrollStep, -event.Wheel.Y*v.ScrollStep) return true } return false }