github.com/Seikaijyu/gio@v0.0.1/widget/list.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package widget 4 5 import ( 6 "image" 7 8 "github.com/Seikaijyu/gio/gesture" 9 "github.com/Seikaijyu/gio/io/key" 10 "github.com/Seikaijyu/gio/io/pointer" 11 "github.com/Seikaijyu/gio/layout" 12 "github.com/Seikaijyu/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 _, event := range s.track.Update(gtx) { 65 if event.Kind != gesture.KindClick || 66 event.Modifiers != key.Modifiers(0) || 67 event.NumClicks > 1 { 68 continue 69 } 70 pos := axis.Convert(image.Point{ 71 X: int(event.Position.X), 72 Y: int(event.Position.Y), 73 }) 74 normalizedPos := float32(pos.X) / trackHeight 75 // Clicking on the indicator should not jump to that position on the track. The user might've just intended to 76 // drag and changed their mind. 77 if !(normalizedPos >= viewportStart && normalizedPos <= viewportEnd) { 78 centerOnClick(normalizedPos) 79 } 80 } 81 82 // Offset to account for any drags. 83 for _, event := range s.drag.Update(gtx.Metric, gtx, gesture.Axis(axis)) { 84 switch event.Kind { 85 case pointer.Drag: 86 case pointer.Release, pointer.Cancel: 87 s.dragging = false 88 continue 89 default: 90 continue 91 } 92 dragOffset := axis.FConvert(event.Position).X 93 // The user can drag outside of the constraints, or even the window. Limit dragging to within the scrollbar. 94 if dragOffset < 0 { 95 dragOffset = 0 96 } else if dragOffset > trackHeight { 97 dragOffset = trackHeight 98 } 99 normalizedDragOffset := dragOffset / trackHeight 100 101 if !s.dragging { 102 s.dragging = true 103 s.oldDragPos = normalizedDragOffset 104 105 if normalizedDragOffset < viewportStart || normalizedDragOffset > viewportEnd { 106 // The user started dragging somewhere on the track that isn't covered by the indicator. Consider this a 107 // click in addition to a drag and jump to the clicked point. 108 // 109 // TODO(dh): this isn't perfect. We only get the pointer.Drag event once the user has actually dragged, 110 // which means that if the user presses the mouse button and neither releases it nor drags it, nothing 111 // will happen. 112 pos := axis.Convert(image.Point{ 113 X: int(event.Position.X), 114 Y: int(event.Position.Y), 115 }) 116 normalizedPos := float32(pos.X) / trackHeight 117 centerOnClick(normalizedPos) 118 } 119 } else { 120 s.delta += normalizedDragOffset - s.oldDragPos 121 122 if viewportStart+s.delta < 0 { 123 // Adjust normalizedDragOffset - and thus the future s.oldDragPos - so that futile dragging up has to be 124 // countered with dragging down again. Otherwise, dragging up would have no effect, but dragging down would 125 // immediately start scrolling. We want the user to undo their ineffective drag first. 126 normalizedDragOffset -= viewportStart + s.delta 127 // Limit s.delta to the maximum amount scrollable 128 s.delta = -viewportStart 129 } else if viewportEnd+s.delta > 1 { 130 normalizedDragOffset += (1 - viewportEnd) - s.delta 131 s.delta = 1 - viewportEnd 132 } 133 s.oldDragPos = normalizedDragOffset 134 } 135 } 136 137 // Process events from the indicator so that hover is 138 // detected properly. 139 _ = s.indicator.Update(gtx) 140 } 141 142 // AddTrack configures the track click listener for the scrollbar to use 143 // the current clip area. 144 func (s *Scrollbar) AddTrack(ops *op.Ops) { 145 s.track.Add(ops) 146 } 147 148 // AddIndicator configures the indicator click listener for the scrollbar to use 149 // the current clip area. 150 func (s *Scrollbar) AddIndicator(ops *op.Ops) { 151 s.indicator.Add(ops) 152 } 153 154 // AddDrag configures the drag listener for the scrollbar to use 155 // the current clip area. 156 func (s *Scrollbar) AddDrag(ops *op.Ops) { 157 s.drag.Add(ops) 158 } 159 160 // IndicatorHovered reports whether the scroll indicator is currently being 161 // hovered by the pointer. 162 func (s *Scrollbar) IndicatorHovered() bool { 163 return s.indicator.Hovered() 164 } 165 166 // TrackHovered reports whether the scroll track is being hovered by the 167 // pointer. 168 func (s *Scrollbar) TrackHovered() bool { 169 return s.track.Hovered() 170 } 171 172 // ScrollDistance returns the normalized distance that the scrollbar 173 // moved during the last call to Layout as a value in the range [-1,1]. 174 func (s *Scrollbar) ScrollDistance() float32 { 175 return s.delta 176 } 177 178 // Dragging reports whether the user is currently performing a drag gesture 179 // on the indicator. Note that this can return false while ScrollDistance is nonzero 180 // if the user scrolls using a different control than the scrollbar (like a mouse 181 // wheel). 182 func (s *Scrollbar) Dragging() bool { 183 return s.dragging 184 } 185 186 // List holds the persistent state for a layout.List that has a 187 // scrollbar attached. 188 type List struct { 189 Scrollbar 190 layout.List 191 }