123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- // 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
- }
|