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

322 lines
8.7 KiB
Go
Raw Permalink Normal View History

2023-03-31 03:19:04 +00:00
package elements
2023-03-09 01:24:43 +00:00
import "image"
2023-05-03 23:40:30 +00:00
import "tomo"
import "tomo/input"
import "art"
2023-03-09 01:24:43 +00:00
// 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.
2023-03-09 01:24:43 +00:00
type ScrollBar struct {
2023-04-20 05:10:47 +00:00
entity tomo.Entity
2023-04-15 05:45:11 +00:00
2023-05-03 05:07:44 +00:00
c tomo.Case
2023-03-09 01:24:43 +00:00
vertical bool
enabled bool
dragging bool
dragOffset image.Point
2023-03-09 01:24:43 +00:00
track image.Rectangle
bar image.Rectangle
contentBounds image.Rectangle
viewportBounds image.Rectangle
onScroll func (viewport image.Point)
2023-03-09 01:24:43 +00:00
}
// NewVScrollBar creates a new vertical scroll bar.
2023-04-20 05:10:47 +00:00
func NewVScrollBar () (element *ScrollBar) {
2023-03-09 01:24:43 +00:00
element = &ScrollBar {
2023-04-20 05:10:47 +00:00
vertical: true,
2023-03-09 01:24:43 +00:00
enabled: true,
}
2023-05-03 05:07:44 +00:00
element.c = tomo.C("tomo", "scrollBarVertical")
element.entity = tomo.GetBackend().NewEntity(element)
2023-04-20 05:10:47 +00:00
element.updateMinimumSize()
return
}
// NewHScrollBar creates a new horizontal scroll bar.
func NewHScrollBar () (element *ScrollBar) {
element = &ScrollBar {
enabled: true,
2023-03-09 01:24:43 +00:00
}
2023-05-03 05:07:44 +00:00
element.c = tomo.C("tomo", "scrollBarHorizontal")
element.entity = tomo.GetBackend().NewEntity(element)
2023-03-09 01:24:43 +00:00
element.updateMinimumSize()
return
}
2023-04-15 05:45:11 +00:00
// 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 art.Canvas) {
2023-04-15 05:45:11 +00:00
element.recalculate()
bounds := element.entity.Bounds()
state := tomo.State {
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
2023-05-03 05:07:44 +00:00
element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw (
2023-04-15 05:45:11 +00:00
destination,
bounds)
2023-05-03 05:07:44 +00:00
element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw (
2023-04-15 05:45:11 +00:00
destination,
element.bar)
}
func (element *ScrollBar) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
2023-05-03 05:07:44 +00:00
velocity := element.entity.Config().ScrollVelocity()
if position.In(element.bar) {
// the mouse is pressed down within the bar's handle
element.dragging = true
2023-04-15 05:45:11 +00:00
element.entity.Invalidate()
2023-03-10 04:27:08 +00:00
element.dragOffset =
position.Sub(element.bar.Min).
2023-04-15 05:45:11 +00:00
Add(element.entity.Bounds().Min)
element.dragTo(position)
} 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(position)
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(position) {
element.scrollBy(viewport)
} else {
element.scrollBy(-viewport)
}
case input.ButtonRight:
// inch up/down on right click
if element.isAfterHandle(position) {
element.scrollBy(velocity)
} else {
element.scrollBy(-velocity)
}
}
}
2023-03-09 01:24:43 +00:00
}
func (element *ScrollBar) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if element.dragging {
element.dragging = false
2023-04-15 05:45:11 +00:00
element.entity.Invalidate()
}
2023-03-09 01:24:43 +00:00
}
func (element *ScrollBar) HandleMotion (position image.Point) {
if element.dragging {
element.dragTo(position)
}
2023-03-09 01:24:43 +00:00
}
func (element *ScrollBar) HandleScroll (
position image.Point,
deltaX, deltaY float64,
modifiers input.Modifiers,
) {
2023-03-10 18:41:47 +00:00
if element.vertical {
element.scrollBy(int(deltaY))
} else {
element.scrollBy(int(deltaX))
}
2023-03-09 01:24:43 +00:00
}
// 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
2023-04-15 05:45:11 +00:00
element.entity.Invalidate()
2023-03-09 01:24:43 +00:00
}
// 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
2023-04-15 05:45:11 +00:00
element.entity.Invalidate()
2023-03-09 01:24:43 +00:00
}
// 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
}
2023-05-03 05:07:44 +00:00
func (element *ScrollBar) HandleThemeChange () {
2023-03-09 01:24:43 +00:00
element.updateMinimumSize()
2023-04-15 05:45:11 +00:00
element.entity.Invalidate()
2023-03-09 01:24:43 +00:00
}
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 {
2023-04-15 05:45:11 +00:00
return element.entity.Bounds().Min.
2023-03-10 04:27:08 +00:00
Add(image.Pt(0, element.bar.Dy() / 2))
} else {
2023-04-15 05:45:11 +00:00
return element.entity.Bounds().Min.
2023-03-10 04:27:08 +00:00
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)))
}
}
2023-03-09 01:24:43 +00:00
func (element *ScrollBar) recalculate () {
if element.vertical {
element.recalculateVertical()
} else {
element.recalculateHorizontal()
}
}
func (element *ScrollBar) recalculateVertical () {
2023-04-15 05:45:11 +00:00
bounds := element.entity.Bounds()
2023-05-03 05:07:44 +00:00
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
2023-03-09 01:24:43 +00:00
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()) /
2023-03-09 01:24:43 +00:00
float64(contentBounds.Dy())
element.bar.Min.Y = int(float64(viewportBounds.Min.Y) * ratio)
element.bar.Max.Y = int(float64(viewportBounds.Max.Y) * ratio)
2023-03-09 01:24:43 +00:00
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 () {
2023-04-15 05:45:11 +00:00
bounds := element.entity.Bounds()
2023-05-03 05:07:44 +00:00
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
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 { }
}
2023-03-09 01:24:43 +00:00
}
func (element *ScrollBar) updateMinimumSize () {
2023-05-03 05:07:44 +00:00
gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c)
2023-03-09 01:24:43 +00:00
if element.vertical {
2023-04-15 05:45:11 +00:00
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
2023-03-09 01:24:43 +00:00
} else {
2023-04-15 05:45:11 +00:00
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
gutterPadding.Vertical() + handlePadding.Vertical())
2023-03-09 01:24:43 +00:00
}
}