Files
gio/widget/list.go
T
Elias Naur c756986d9e gesture: [API] rename gesture state update methods to Update
Change the gesture state update methods to align with the convention.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-08 12:37:12 -05:00

194 lines
6.5 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package widget
import (
"image"
"gioui.org/gesture"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
)
// Scrollbar holds the persistent state for an area that can
// display a scrollbar. In particular, it tracks the position of a
// viewport along a one-dimensional region of content. The viewport's
// position can be adjusted by drag operations along the display area,
// or by clicks within the display area.
//
// Scrollbar additionally detects when a scroll indicator region is
// hovered.
type Scrollbar struct {
track, indicator gesture.Click
drag gesture.Drag
delta float32
dragging bool
oldDragPos float32
}
// Layout updates the internal state of the scrollbar based on events
// since the previous call to Layout. The provided axis will be used to
// normalize input event coordinates and constraints into an axis-
// independent format. viewportStart is the position of the beginning
// of the scrollable viewport relative to the underlying content expressed
// as a value in the range [0,1]. viewportEnd is the position of the end
// of the viewport relative to the underlying content, also expressed
// as a value in the range [0,1]. For example, if viewportStart is 0.25
// and viewportEnd is .5, the viewport described by the scrollbar is
// currently showing the second quarter of the underlying content.
func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions {
// Calculate the length of the major axis of the scrollbar. This is
// the length of the track within which pointer events occur, and is
// used to scale those interactions.
trackHeight := float32(axis.Convert(gtx.Constraints.Max).X)
s.delta = 0
centerOnClick := func(normalizedPos float32) {
// When the user clicks on the scrollbar we center on that point, respecting the limits of the beginning and end
// of the scrollbar.
//
// Centering gives a consistent experience whether the user clicks above or below the indicator.
target := normalizedPos - (viewportEnd-viewportStart)/2
s.delta += target - viewportStart
if s.delta < -viewportStart {
s.delta = -viewportStart
} else if s.delta > 1-viewportEnd {
s.delta = 1 - viewportEnd
}
}
// Jump to a click in the track.
for _, event := range s.track.Update(gtx) {
if event.Kind != gesture.KindClick ||
event.Modifiers != key.Modifiers(0) ||
event.NumClicks > 1 {
continue
}
pos := axis.Convert(image.Point{
X: int(event.Position.X),
Y: int(event.Position.Y),
})
normalizedPos := float32(pos.X) / trackHeight
// Clicking on the indicator should not jump to that position on the track. The user might've just intended to
// drag and changed their mind.
if !(normalizedPos >= viewportStart && normalizedPos <= viewportEnd) {
centerOnClick(normalizedPos)
}
}
// Offset to account for any drags.
for _, event := range s.drag.Update(gtx.Metric, gtx, gesture.Axis(axis)) {
switch event.Kind {
case pointer.Drag:
case pointer.Release, pointer.Cancel:
s.dragging = false
continue
default:
continue
}
dragOffset := axis.FConvert(event.Position).X
// The user can drag outside of the constraints, or even the window. Limit dragging to within the scrollbar.
if dragOffset < 0 {
dragOffset = 0
} else if dragOffset > trackHeight {
dragOffset = trackHeight
}
normalizedDragOffset := dragOffset / trackHeight
if !s.dragging {
s.dragging = true
s.oldDragPos = normalizedDragOffset
if normalizedDragOffset < viewportStart || normalizedDragOffset > viewportEnd {
// The user started dragging somewhere on the track that isn't covered by the indicator. Consider this a
// click in addition to a drag and jump to the clicked point.
//
// TODO(dh): this isn't perfect. We only get the pointer.Drag event once the user has actually dragged,
// which means that if the user presses the mouse button and neither releases it nor drags it, nothing
// will happen.
pos := axis.Convert(image.Point{
X: int(event.Position.X),
Y: int(event.Position.Y),
})
normalizedPos := float32(pos.X) / trackHeight
centerOnClick(normalizedPos)
}
} else {
s.delta += normalizedDragOffset - s.oldDragPos
if viewportStart+s.delta < 0 {
// Adjust normalizedDragOffset - and thus the future s.oldDragPos - so that futile dragging up has to be
// countered with dragging down again. Otherwise, dragging up would have no effect, but dragging down would
// immediately start scrolling. We want the user to undo their ineffective drag first.
normalizedDragOffset -= viewportStart + s.delta
// Limit s.delta to the maximum amount scrollable
s.delta = -viewportStart
} else if viewportEnd+s.delta > 1 {
normalizedDragOffset += (1 - viewportEnd) - s.delta
s.delta = 1 - viewportEnd
}
s.oldDragPos = normalizedDragOffset
}
}
// Process events from the indicator so that hover is
// detected properly.
_ = s.indicator.Update(gtx)
return layout.Dimensions{}
}
// AddTrack configures the track click listener for the scrollbar to use
// the current clip area.
func (s *Scrollbar) AddTrack(ops *op.Ops) {
s.track.Add(ops)
}
// AddIndicator configures the indicator click listener for the scrollbar to use
// the current clip area.
func (s *Scrollbar) AddIndicator(ops *op.Ops) {
s.indicator.Add(ops)
}
// AddDrag configures the drag listener for the scrollbar to use
// the current clip area.
func (s *Scrollbar) AddDrag(ops *op.Ops) {
s.drag.Add(ops)
}
// IndicatorHovered reports whether the scroll indicator is currently being
// hovered by the pointer.
func (s *Scrollbar) IndicatorHovered() bool {
return s.indicator.Hovered()
}
// TrackHovered reports whether the scroll track is being hovered by the
// pointer.
func (s *Scrollbar) TrackHovered() bool {
return s.track.Hovered()
}
// ScrollDistance returns the normalized distance that the scrollbar
// moved during the last call to Layout as a value in the range [-1,1].
func (s *Scrollbar) ScrollDistance() float32 {
return s.delta
}
// Dragging reports whether the user is currently performing a drag gesture
// on the indicator. Note that this can return false while ScrollDistance is nonzero
// if the user scrolls using a different control than the scrollbar (like a mouse
// wheel).
func (s *Scrollbar) Dragging() bool {
return s.dragging
}
// List holds the persistent state for a layout.List that has a
// scrollbar attached.
type List struct {
Scrollbar
layout.List
}