github.com/utopiagio/gio@v0.0.8/widget/list.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package widget 4 5 import ( 6 "image" 7 8 "github.com/utopiagio/gio/gesture" 9 "github.com/utopiagio/gio/io/key" 10 "github.com/utopiagio/gio/io/pointer" 11 "github.com/utopiagio/gio/layout" 12 "github.com/utopiagio/gio/op" 13 ) 14 15 // Scrollbar holds the persistent state for an area that can 16 // display a scrollbar. In particular, it tracks the position of a 17 // viewport along a one-dimensional region of content. The viewport's 18 // position can be adjusted by drag operations along the display area, 19 // or by clicks within the display area. 20 // 21 // Scrollbar additionally detects when a scroll indicator region is 22 // hovered. 23 type Scrollbar struct { 24 track, indicator gesture.Click 25 drag gesture.Drag 26 delta float32 27 28 dragging bool 29 oldDragPos float32 30 } 31 32 // Update updates the internal state of the scrollbar based on events 33 // since the previous call to Update. The provided axis will be used to 34 // normalize input event coordinates and constraints into an axis- 35 // independent format. viewportStart is the position of the beginning 36 // of the scrollable viewport relative to the underlying content expressed 37 // as a value in the range [0,1]. viewportEnd is the position of the end 38 // of the viewport relative to the underlying content, also expressed 39 // as a value in the range [0,1]. For example, if viewportStart is 0.25 40 // and viewportEnd is .5, the viewport described by the scrollbar is 41 // currently showing the second quarter of the underlying content. 42 func (s *Scrollbar) Update(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) { 43 // Calculate the length of the major axis of the scrollbar. This is 44 // the length of the track within which pointer events occur, and is 45 // used to scale those interactions. 46 trackHeight := float32(axis.Convert(gtx.Constraints.Max).X) 47 s.delta = 0 48 49 centerOnClick := func(normalizedPos float32) { 50 // When the user clicks on the scrollbar we center on that point, respecting the limits of the beginning and end 51 // of the scrollbar. 52 // 53 // Centering gives a consistent experience whether the user clicks above or below the indicator. 54 target := normalizedPos - (viewportEnd-viewportStart)/2 55 s.delta += target - viewportStart 56 if s.delta < -viewportStart { 57 s.delta = -viewportStart 58 } else if s.delta > 1-viewportEnd { 59 s.delta = 1 - viewportEnd 60 } 61 } 62 63 // Jump to a click in the track. 64 for { 65 event, ok := s.track.Update(gtx.Source) 66 if !ok { 67 break 68 } 69 if event.Kind != gesture.KindClick || 70 event.Modifiers != key.Modifiers(0) || 71 event.NumClicks > 1 { 72 continue 73 } 74 pos := axis.Convert(image.Point{ 75 X: int(event.Position.X), 76 Y: int(event.Position.Y), 77 }) 78 normalizedPos := float32(pos.X) / trackHeight 79 // Clicking on the indicator should not jump to that position on the track. The user might've just intended to 80 // drag and changed their mind. 81 if !(normalizedPos >= viewportStart && normalizedPos <= viewportEnd) { 82 centerOnClick(normalizedPos) 83 } 84 } 85 86 // Offset to account for any drags. 87 for { 88 event, ok := s.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(axis)) 89 if !ok { 90 break 91 } 92 switch event.Kind { 93 case pointer.Drag: 94 case pointer.Release, pointer.Cancel: 95 s.dragging = false 96 continue 97 default: 98 continue 99 } 100 dragOffset := axis.FConvert(event.Position).X 101 // The user can drag outside of the constraints, or even the window. Limit dragging to within the scrollbar. 102 if dragOffset < 0 { 103 dragOffset = 0 104 } else if dragOffset > trackHeight { 105 dragOffset = trackHeight 106 } 107 normalizedDragOffset := dragOffset / trackHeight 108 109 if !s.dragging { 110 s.dragging = true 111 s.oldDragPos = normalizedDragOffset 112 113 if normalizedDragOffset < viewportStart || normalizedDragOffset > viewportEnd { 114 // The user started dragging somewhere on the track that isn't covered by the indicator. Consider this a 115 // click in addition to a drag and jump to the clicked point. 116 // 117 // TODO(dh): this isn't perfect. We only get the pointer.Drag event once the user has actually dragged, 118 // which means that if the user presses the mouse button and neither releases it nor drags it, nothing 119 // will happen. 120 pos := axis.Convert(image.Point{ 121 X: int(event.Position.X), 122 Y: int(event.Position.Y), 123 }) 124 normalizedPos := float32(pos.X) / trackHeight 125 centerOnClick(normalizedPos) 126 } 127 } else { 128 s.delta += normalizedDragOffset - s.oldDragPos 129 130 if viewportStart+s.delta < 0 { 131 // Adjust normalizedDragOffset - and thus the future s.oldDragPos - so that futile dragging up has to be 132 // countered with dragging down again. Otherwise, dragging up would have no effect, but dragging down would 133 // immediately start scrolling. We want the user to undo their ineffective drag first. 134 normalizedDragOffset -= viewportStart + s.delta 135 // Limit s.delta to the maximum amount scrollable 136 s.delta = -viewportStart 137 } else if viewportEnd+s.delta > 1 { 138 normalizedDragOffset += (1 - viewportEnd) - s.delta 139 s.delta = 1 - viewportEnd 140 } 141 s.oldDragPos = normalizedDragOffset 142 } 143 } 144 145 // Process events from the indicator so that hover is 146 // detected properly. 147 for { 148 if _, ok := s.indicator.Update(gtx.Source); !ok { 149 break 150 } 151 } 152 } 153 154 // AddTrack configures the track click listener for the scrollbar to use 155 // the current clip area. 156 func (s *Scrollbar) AddTrack(ops *op.Ops) { 157 s.track.Add(ops) 158 } 159 160 // AddIndicator configures the indicator click listener for the scrollbar to use 161 // the current clip area. 162 func (s *Scrollbar) AddIndicator(ops *op.Ops) { 163 s.indicator.Add(ops) 164 } 165 166 // AddDrag configures the drag listener for the scrollbar to use 167 // the current clip area. 168 func (s *Scrollbar) AddDrag(ops *op.Ops) { 169 s.drag.Add(ops) 170 } 171 172 // IndicatorHovered reports whether the scroll indicator is currently being 173 // hovered by the pointer. 174 func (s *Scrollbar) IndicatorHovered() bool { 175 return s.indicator.Hovered() 176 } 177 178 // TrackHovered reports whether the scroll track is being hovered by the 179 // pointer. 180 func (s *Scrollbar) TrackHovered() bool { 181 return s.track.Hovered() 182 } 183 184 // ScrollDistance returns the normalized distance that the scrollbar 185 // moved during the last call to Layout as a value in the range [-1,1]. 186 func (s *Scrollbar) ScrollDistance() float32 { 187 return s.delta 188 } 189 190 // Dragging reports whether the user is currently performing a drag gesture 191 // on the indicator. Note that this can return false while ScrollDistance is nonzero 192 // if the user scrolls using a different control than the scrollbar (like a mouse 193 // wheel). 194 func (s *Scrollbar) Dragging() bool { 195 return s.dragging 196 } 197 198 // List holds the persistent state for a layout.List that has a 199 // scrollbar attached. 200 type List struct { 201 Scrollbar 202 layout.List 203 }