Sasha Koshka
c7cd944ae2
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.
326 lines
8.9 KiB
Go
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)
|
|
}
|