diff --git a/elements/basic/scrollbar.go b/elements/basic/scrollbar.go index 5681896..f2714cd 100644 --- a/elements/basic/scrollbar.go +++ b/elements/basic/scrollbar.go @@ -7,6 +7,14 @@ import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +// 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). type ScrollBar struct { *core.Core core core.CoreControl @@ -14,7 +22,7 @@ type ScrollBar struct { vertical bool enabled bool dragging bool - dragOffset int + dragOffset image.Point track image.Rectangle bar image.Rectangle @@ -24,10 +32,11 @@ type ScrollBar struct { config config.Wrapped theme theme.Wrapped - onSlide func () - onRelease func () + 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, @@ -44,15 +53,61 @@ func NewScrollBar (vertical bool) (element *ScrollBar) { } 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.redo() + element.dragOffset = point + 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.redo() + } } func (element *ScrollBar) HandleMouseMove (x, y int) { - + if element.dragging { + element.dragTo(image.Pt(x, y)) + } } func (element *ScrollBar) HandleMouseScroll (x, y int, deltaX, deltaY float64) { @@ -78,6 +133,15 @@ func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) { element.redo() } +// 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 theme.Theme) { if new == element.theme.Theme { return } @@ -93,6 +157,57 @@ func (element *ScrollBar) SetConfig (new config.Config) { element.redo() } +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.bar.Min.Add(image.Pt(0, element.bar.Dy() / 2)) + } else { + return element.bar.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() diff --git a/examples/artist/main.go b/examples/drawing/main.go similarity index 100% rename from examples/artist/main.go rename to examples/drawing/main.go diff --git a/go.mod b/go.mod index 5a43861..20559ab 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.tebibyte.media/sashakoshka/tomo go 1.19 require ( - git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309013201-fc0de8121523 + git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309044548-401cba83602b github.com/faiface/beep v1.1.0 github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 golang.org/x/image v0.3.0 diff --git a/go.sum b/go.sum index 07bcd4c..6cf2167 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309013013-f7ee80c8f908 h1:kFdc git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309013013-f7ee80c8f908/go.mod h1:cpXX8SAUDEvZX5m7scoyruavUhEqQ1SByfWzPFHkTbg= git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309013201-fc0de8121523 h1:1KaoiGetWYIDQKts6yas1hW+4ObkuTm6+TkFpl6jZxg= git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309013201-fc0de8121523/go.mod h1:cpXX8SAUDEvZX5m7scoyruavUhEqQ1SByfWzPFHkTbg= +git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309044548-401cba83602b h1:vPFKR7vjN1VrMdMtpATMrKQobz/cqbPiRrA1EbtG6PM= +git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309044548-401cba83602b/go.mod h1:cpXX8SAUDEvZX5m7scoyruavUhEqQ1SByfWzPFHkTbg= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= diff --git a/xcf/large.xcf b/xcf/large.xcf new file mode 100644 index 0000000..e2d7f3d Binary files /dev/null and b/xcf/large.xcf differ diff --git a/xcf/small.xcf b/xcf/small.xcf new file mode 100644 index 0000000..c19733e Binary files /dev/null and b/xcf/small.xcf differ