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

324 lines
9.1 KiB
Go
Raw Normal View History

2023-03-30 21:19:04 -06:00
package elements
2023-03-08 18:24:43 -07:00
import "image"
2023-03-30 23:06:29 -06:00
import "git.tebibyte.media/sashakoshka/tomo"
2023-03-08 18:24:43 -07:00
import "git.tebibyte.media/sashakoshka/tomo/input"
2023-04-14 23:45:11 -06:00
import "git.tebibyte.media/sashakoshka/tomo/canvas"
2023-03-30 23:06:29 -06:00
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
2023-03-08 18:24:43 -07:00
// Orientation represents an orientation configuration that can be passed to
// scrollbars and sliders.
type Orientation bool; const (
Vertical Orientation = true
Horizontal = false
)
// 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-08 18:24:43 -07:00
type ScrollBar struct {
2023-04-14 23:45:11 -06:00
entity tomo.ContainerEntity
2023-03-08 18:24:43 -07:00
vertical bool
enabled bool
dragging bool
dragOffset image.Point
2023-03-08 18:24:43 -07:00
track image.Rectangle
bar image.Rectangle
contentBounds image.Rectangle
viewportBounds image.Rectangle
config config.Wrapped
theme theme.Wrapped
onScroll func (viewport image.Point)
2023-03-08 18:24:43 -07:00
}
// NewScrollBar creates a new scroll bar.
func NewScrollBar (orientation Orientation) (element *ScrollBar) {
2023-03-08 18:24:43 -07:00
element = &ScrollBar {
vertical: bool(orientation),
2023-03-08 18:24:43 -07:00
enabled: true,
}
if orientation == Vertical {
2023-03-30 23:06:29 -06:00
element.theme.Case = tomo.C("tomo", "scrollBarHorizontal")
2023-03-08 18:24:43 -07:00
} else {
2023-03-30 23:06:29 -06:00
element.theme.Case = tomo.C("tomo", "scrollBarVertical")
2023-03-08 18:24:43 -07:00
}
2023-04-14 23:45:11 -06:00
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
2023-03-08 18:24:43 -07:00
element.updateMinimumSize()
return
}
2023-04-14 23:45:11 -06: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 canvas.Canvas) {
element.recalculate()
bounds := element.entity.Bounds()
state := tomo.State {
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
2023-04-14 23:45:11 -06:00
element.theme.Pattern(tomo.PatternGutter, state).Draw (
destination,
bounds)
element.theme.Pattern(tomo.PatternHandle, state).Draw (
destination,
element.bar)
}
2023-03-08 18:24:43 -07:00
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
2023-04-14 23:45:11 -06:00
element.entity.Invalidate()
2023-03-09 21:27:08 -07:00
element.dragOffset =
point.Sub(element.bar.Min).
2023-04-14 23:45:11 -06:00
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)
}
}
}
2023-03-08 18:24:43 -07:00
}
func (element *ScrollBar) HandleMouseUp (x, y int, button input.Button) {
if element.dragging {
element.dragging = false
2023-04-14 23:45:11 -06:00
element.entity.Invalidate()
}
2023-03-08 18:24:43 -07:00
}
func (element *ScrollBar) HandleMotion (x, y int) {
if element.dragging {
element.dragTo(image.Pt(x, y))
}
2023-03-08 18:24:43 -07:00
}
func (element *ScrollBar) HandleScroll (x, y int, deltaX, deltaY float64) {
2023-03-10 11:41:47 -07:00
if element.vertical {
element.scrollBy(int(deltaY))
} else {
element.scrollBy(int(deltaX))
}
2023-03-08 18:24:43 -07: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-14 23:45:11 -06:00
element.entity.Invalidate()
2023-03-08 18:24:43 -07: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-14 23:45:11 -06:00
element.entity.Invalidate()
2023-03-08 18:24:43 -07: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-03-08 18:24:43 -07:00
// SetTheme sets the element's theme.
2023-03-30 23:06:29 -06:00
func (element *ScrollBar) SetTheme (new tomo.Theme) {
2023-03-08 18:24:43 -07:00
if new == element.theme.Theme { return }
element.theme.Theme = new
2023-04-14 23:45:11 -06:00
element.entity.Invalidate()
2023-03-08 18:24:43 -07:00
}
// SetConfig sets the element's configuration.
2023-03-30 23:06:29 -06:00
func (element *ScrollBar) SetConfig (new tomo.Config) {
2023-03-08 18:24:43 -07:00
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
2023-04-14 23:45:11 -06:00
element.entity.Invalidate()
2023-03-08 18:24:43 -07: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-14 23:45:11 -06:00
return element.entity.Bounds().Min.
2023-03-09 21:27:08 -07:00
Add(image.Pt(0, element.bar.Dy() / 2))
} else {
2023-04-14 23:45:11 -06:00
return element.entity.Bounds().Min.
2023-03-09 21:27:08 -07: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-08 18:24:43 -07:00
func (element *ScrollBar) recalculate () {
if element.vertical {
element.recalculateVertical()
} else {
element.recalculateHorizontal()
}
}
func (element *ScrollBar) recalculateVertical () {
2023-04-14 23:45:11 -06:00
bounds := element.entity.Bounds()
2023-03-30 23:06:29 -06:00
padding := element.theme.Padding(tomo.PatternGutter)
2023-03-08 18:24:43 -07: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-08 18:24:43 -07: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-08 18:24:43 -07: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-14 23:45:11 -06:00
bounds := element.entity.Bounds()
2023-03-30 23:06:29 -06:00
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 { }
}
2023-03-08 18:24:43 -07:00
}
func (element *ScrollBar) updateMinimumSize () {
gutterPadding := element.theme.Padding(tomo.PatternGutter)
handlePadding := element.theme.Padding(tomo.PatternHandle)
2023-03-08 18:24:43 -07:00
if element.vertical {
2023-04-14 23:45:11 -06:00
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
2023-03-08 18:24:43 -07:00
} else {
2023-04-14 23:45:11 -06:00
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
gutterPadding.Vertical() + handlePadding.Vertical())
2023-03-08 18:24:43 -07:00
}
}