This repository has been archived on 2023-08-08. You can view files and clone it, but cannot push or open issues or pull requests.
tomo-old/elements/scrollbar.go
Sasha Koshka c7cd944ae2 Removed redundant HandleWidth parameter from config
The handle width can be specified by themes with padding values.
This also allows for far more granularity of the handle width
adjustment as it can depend on context.
2023-03-31 13:55:45 -04:00

326 lines
8.9 KiB
Go

package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// ScrollBar is an element similar to Slider, but it has special behavior that
// makes it well suited for controlling the viewport position on one axis of a
// scrollable element. Instead of having a value from zero to one, it stores
// viewport and content boundaries. When the user drags the scroll bar handle,
// the scroll bar calls the OnScroll callback assigned to it with the position
// the user is trying to move the handle to. A program can check to see if this
// value is valid, move the viewport, and give the scroll bar the new viewport
// bounds (which will then cause it to move the handle).
//
// Typically, you wont't want to use a ScrollBar by itself. A ScrollContainer is
// better for most cases.
type ScrollBar struct {
*core.Core
core core.CoreControl
vertical bool
enabled bool
dragging bool
dragOffset image.Point
track image.Rectangle
bar image.Rectangle
contentBounds image.Rectangle
viewportBounds image.Rectangle
config config.Wrapped
theme theme.Wrapped
onScroll func (viewport image.Point)
}
// NewScrollBar creates a new scroll bar. If vertical is set to true, the scroll
// bar will be vertical instead of horizontal.
func NewScrollBar (vertical bool) (element *ScrollBar) {
element = &ScrollBar {
vertical: vertical,
enabled: true,
}
if vertical {
element.theme.Case = tomo.C("tomo", "scrollBarHorizontal")
} else {
element.theme.Case = tomo.C("tomo", "scrollBarVertical")
}
element.Core, element.core = core.NewCore(element, element.handleResize)
element.updateMinimumSize()
return
}
func (element *ScrollBar) handleResize () {
if element.core.HasImage() {
element.recalculate()
element.draw()
}
}
func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) {
velocity := element.config.ScrollVelocity()
point := image.Pt(x, y)
if point.In(element.bar) {
// the mouse is pressed down within the bar's handle
element.dragging = true
element.drawAndPush()
element.dragOffset =
point.Sub(element.bar.Min).
Add(element.Bounds().Min)
element.dragTo(point)
} else {
// the mouse is pressed down within the bar's gutter
switch button {
case input.ButtonLeft:
// start scrolling at this point, but set the offset to
// the middle of the handle
element.dragging = true
element.dragOffset = element.fallbackDragOffset()
element.dragTo(point)
case input.ButtonMiddle:
// page up/down on middle click
viewport := 0
if element.vertical {
viewport = element.viewportBounds.Dy()
} else {
viewport = element.viewportBounds.Dx()
}
if element.isAfterHandle(point) {
element.scrollBy(viewport)
} else {
element.scrollBy(-viewport)
}
case input.ButtonRight:
// inch up/down on right click
if element.isAfterHandle(point) {
element.scrollBy(velocity)
} else {
element.scrollBy(-velocity)
}
}
}
}
func (element *ScrollBar) HandleMouseUp (x, y int, button input.Button) {
if element.dragging {
element.dragging = false
element.drawAndPush()
}
}
func (element *ScrollBar) HandleMotion (x, y int) {
if element.dragging {
element.dragTo(image.Pt(x, y))
}
}
func (element *ScrollBar) HandleScroll (x, y int, deltaX, deltaY float64) {
if element.vertical {
element.scrollBy(int(deltaY))
} else {
element.scrollBy(int(deltaX))
}
}
// SetEnabled sets whether or not the scroll bar can be interacted with.
func (element *ScrollBar) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.drawAndPush()
}
// Enabled returns whether or not the element is enabled.
func (element *ScrollBar) Enabled () (enabled bool) {
return element.enabled
}
// SetBounds sets the content and viewport bounds of the scroll bar.
func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) {
element.contentBounds = content
element.viewportBounds = viewport
element.recalculate()
element.drawAndPush()
}
// OnScroll sets a function to be called when the user tries to move the scroll
// bar's handle. The callback is passed a point representing the new viewport
// position. For the scroll bar's position to visually update, the callback must
// check if the position is valid and call ScrollBar.SetBounds with the new
// viewport bounds.
func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) {
element.onScroll = callback
}
// SetTheme sets the element's theme.
func (element *ScrollBar) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawAndPush()
}
// SetConfig sets the element's configuration.
func (element *ScrollBar) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.drawAndPush()
}
func (element *ScrollBar) isAfterHandle (point image.Point) bool {
if element.vertical {
return point.Y > element.bar.Min.Y
} else {
return point.X > element.bar.Min.X
}
}
func (element *ScrollBar) fallbackDragOffset () image.Point {
if element.vertical {
return element.Bounds().Min.
Add(image.Pt(0, element.bar.Dy() / 2))
} else {
return element.Bounds().Min.
Add(image.Pt(element.bar.Dx() / 2, 0))
}
}
func (element *ScrollBar) scrollBy (delta int) {
deltaPoint := image.Point { }
if element.vertical {
deltaPoint.Y = delta
} else {
deltaPoint.X = delta
}
if element.onScroll != nil {
element.onScroll(element.viewportBounds.Min.Add(deltaPoint))
}
}
func (element *ScrollBar) dragTo (point image.Point) {
point = point.Sub(element.dragOffset)
var scrollX, scrollY float64
if element.vertical {
ratio :=
float64(element.contentBounds.Dy()) /
float64(element.track.Dy())
scrollX = float64(element.viewportBounds.Min.X)
scrollY = float64(point.Y) * ratio
} else {
ratio :=
float64(element.contentBounds.Dx()) /
float64(element.track.Dx())
scrollX = float64(point.X) * ratio
scrollY = float64(element.viewportBounds.Min.Y)
}
if element.onScroll != nil {
element.onScroll(image.Pt(int(scrollX), int(scrollY)))
}
}
func (element *ScrollBar) recalculate () {
if element.vertical {
element.recalculateVertical()
} else {
element.recalculateHorizontal()
}
}
func (element *ScrollBar) recalculateVertical () {
bounds := element.Bounds()
padding := element.theme.Padding(tomo.PatternGutter)
element.track = padding.Apply(bounds)
contentBounds := element.contentBounds
viewportBounds := element.viewportBounds
if element.Enabled() {
element.bar.Min.X = element.track.Min.X
element.bar.Max.X = element.track.Max.X
ratio :=
float64(element.track.Dy()) /
float64(contentBounds.Dy())
element.bar.Min.Y = int(float64(viewportBounds.Min.Y) * ratio)
element.bar.Max.Y = int(float64(viewportBounds.Max.Y) * ratio)
element.bar.Min.Y += element.track.Min.Y
element.bar.Max.Y += element.track.Min.Y
}
// if the handle is out of bounds, don't display it
if element.bar.Dy() >= element.track.Dy() {
element.bar = image.Rectangle { }
}
}
func (element *ScrollBar) recalculateHorizontal () {
bounds := element.Bounds()
padding := element.theme.Padding(tomo.PatternGutter)
element.track = padding.Apply(bounds)
contentBounds := element.contentBounds
viewportBounds := element.viewportBounds
if element.Enabled() {
element.bar.Min.Y = element.track.Min.Y
element.bar.Max.Y = element.track.Max.Y
ratio :=
float64(element.track.Dx()) /
float64(contentBounds.Dx())
element.bar.Min.X = int(float64(viewportBounds.Min.X) * ratio)
element.bar.Max.X = int(float64(viewportBounds.Max.X) * ratio)
element.bar.Min.X += element.track.Min.X
element.bar.Max.X += element.track.Min.X
}
// if the handle is out of bounds, don't display it
if element.bar.Dx() >= element.track.Dx() {
element.bar = image.Rectangle { }
}
}
func (element *ScrollBar) updateMinimumSize () {
gutterPadding := element.theme.Padding(tomo.PatternGutter)
handlePadding := element.theme.Padding(tomo.PatternHandle)
if element.vertical {
element.core.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
} else {
element.core.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
gutterPadding.Vertical() + handlePadding.Vertical())
}
}
func (element *ScrollBar) drawAndPush () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *ScrollBar) draw () {
bounds := element.Bounds()
state := tomo.State {
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
element.theme.Pattern(tomo.PatternGutter, state).Draw (
element.core,
bounds)
element.theme.Pattern(tomo.PatternHandle, state).Draw (
element.core,
element.bar)
}