318 lines
9.0 KiB
Go
318 lines
9.0 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/canvas"
|
|
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 {
|
|
entity tomo.ContainerEntity
|
|
|
|
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.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
|
|
element.updateMinimumSize()
|
|
return
|
|
}
|
|
|
|
// Entity returns this element's entity.
|
|
func (element *ScrollBar) Entity () tomo.Entity {
|
|
return element.entity
|
|
}
|
|
|
|
// Draw causes the element to draw to the specified destination canvas.
|
|
func (element *ScrollBar) Draw (destination canvas.Canvas) {
|
|
element.recalculate()
|
|
|
|
bounds := element.entity.Bounds()
|
|
state := tomo.State {
|
|
Disabled: !element.Enabled(),
|
|
Pressed: element.dragging,
|
|
}
|
|
element.theme.Pattern(tomo.PatternGutter, state).Draw (
|
|
destination,
|
|
bounds)
|
|
element.theme.Pattern(tomo.PatternHandle, state).Draw (
|
|
destination,
|
|
element.bar)
|
|
}
|
|
|
|
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.entity.Invalidate()
|
|
element.dragOffset =
|
|
point.Sub(element.bar.Min).
|
|
Add(element.entity.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.entity.Invalidate()
|
|
}
|
|
}
|
|
|
|
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.entity.Invalidate()
|
|
}
|
|
|
|
// 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.entity.Invalidate()
|
|
}
|
|
|
|
// 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.entity.Invalidate()
|
|
}
|
|
|
|
// 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.entity.Invalidate()
|
|
}
|
|
|
|
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.entity.Bounds().Min.
|
|
Add(image.Pt(0, element.bar.Dy() / 2))
|
|
} else {
|
|
return element.entity.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.entity.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.entity.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.entity.SetMinimumSize (
|
|
gutterPadding.Horizontal() + handlePadding.Horizontal(),
|
|
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
|
} else {
|
|
element.entity.SetMinimumSize (
|
|
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
|
gutterPadding.Vertical() + handlePadding.Vertical())
|
|
}
|
|
}
|