From aff9aca835016e1f8e2700ff7b70272413a92274 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 9 Mar 2023 18:15:52 -0500 Subject: [PATCH] We now have an untested lone scrollbar element --- elements/basic/scrollbar.go | 127 +++++++++++++++++++++++++-- examples/{artist => drawing}/main.go | 0 go.mod | 2 +- go.sum | 2 + xcf/large.xcf | Bin 0 -> 90828 bytes xcf/small.xcf | Bin 0 -> 71706 bytes 6 files changed, 124 insertions(+), 7 deletions(-) rename examples/{artist => drawing}/main.go (100%) create mode 100644 xcf/large.xcf create mode 100644 xcf/small.xcf 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 0000000000000000000000000000000000000000..e2d7f3d9da558c590a4a9e9dbb4175e92d186d25 GIT binary patch literal 90828 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 literal 0 HcmV?d00001