Files
gio-patched/widget/list.go
Elias Naur 8e209fd2eb gesture: report one event at a time
Events are now delivered one at a time, and this change makes the
corresponding change to gestures.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00

204 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
}
// Update updates the internal state of the scrollbar based on events
// since the previous call to Update. 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) Update(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) {
// 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, ok := s.track.Update(gtx.Source)
if !ok {
break
}
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, ok := s.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(axis))
if !ok {
break
}
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.
for {
if _, ok := s.indicator.Update(gtx.Source); !ok {
break
}
}
}
// 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
}