Chris proposes a physics-based approach to get rid of the ugly drag
scroll artifacts we currently have. This patch proposes an alternative
that locks the heuristic for scroll distance, instead of trying to
improve it.
It works for me for Chris' scroll test program, and for
kitchen.
It is not mergeable as is, and I don't have time to finish
it; however, if the approach indeed works I propose holding off merging
the physics approach.
Elias Naur (1):
widget,widget/material: lock drag scroll heuristic on first drag
widget/list.go | 24 +++++++++++++++++++++++-
widget/material/list.go | 13 ++++++++++++-
2 files changed, 35 insertions(+), 2 deletions(-)
--
2.32.0
From: Elias Naur <mail@eliasnaur.com>
When dragging a scroll indicator, the distance scrolled is propoertional
to the indicator length/height. However, since the length is based on an
estimation (and content, for dynamic lists), the scale from distance
dragged to pixels scrolled would change every frame, leading to ugly
flickering artifacts.
This (hacky) change proposes a simple workaround: lock the
distance-to-scroll scale on the first drag.
You know, I didn't try this because I thought it would be too
inaccurate, but it works really for most use-cases, and it doesn't
have the physics latency problems. Nicely done!
I'm happy to develop a polished implementation based on this
principle, but I have a few questions about how it should work:
- If someone clicks in the track (not on the indicator), should we
attempt to jump to the corresponding list position using our current
estimate of the content size?
- I was able to get into some irritating scenarios using this in
Yes, in a sense as if the user pressed the indicator, instantly moved it
to the track click position, and released.
I assume you ask because immediately after jumping, the indicator may
not end up below the cursor. I think that's acceptable behaviour.
Note that I haven't investigated track clicking; I focused on the jerky
drag behaviour.
git.sr.ht/~gioverse/kitchen where the error between the heuristic and
the real content size allowed me to drag my cursor all the way to the
top or bottom of my monitor without reaching the top or bottom of the
list. This is because the list is dynamically fetching more content,
so it's growing underneath us. Anyway, to navigate use-cases like
that, I had to re-grab the indicator several times. What would you
think of a hybrid approach where we use this mechanism, then apply
physics if our final scroll position is still far away from their
cursor? That would allow for dragging the indicator out of the window
to continuously scroll through infinite structures, and it would
Given there's a promising zero-latency approach, I hope to avoid physics
scrolling.
I agree that the chat kitchen example (thanks!) exhibits unacceptable
behaviour. I believe (but haven't tried) extending my approach to freeze
every parameter of the scrolling indicator (including estimated list
length) will make sure the indicator is always attached to the cursor.
In other words, as soon as the user presses the indicator, it switches
from a live representation of the underlying list and viewport to a
static control for scrolling the list. The indicator size remains
constant and only moves in response to drag events.
When the user eventually releases the indicator, it switches back to
representing its list. That may very well mean the indicator jumps; I
think that's acceptable behaviour, similar to the track clicking
behaviour above.
provide a means of compensating for the inaccuracy inherent in using
the first estimate. If you craft a scroll position that you know has
an unusual content density and start dragging from there, it's pretty
easy to desynchronize the cursor and indicator positions. Having the
physics as well would be a decent way to compensate. It is more
complex though, no denying that. Also, Jack Mordaunt is the one who
suggested combining the two approaches, it's not actually my idea. :D
My refined approach above does not deal with the scroll extremes, where
the indicator reaches the start or end of its track but the underlying
list is longer in that direction. Thanks for bringing up that case.
A idea (I haven't tried) is when an indicator is at an extreme, it
continously forces the list to the same extreme, similar to how
List.ScrollToEnd can make a List "stick" to the end extreme.
So: if a dragging indicator is laid out, and is at the extreme start of
the track, scroll the list to index 0, offset 0. If it's at the extreme
end, scroll to the end (as if List.Position.BeforeEnd = false and
List.ScrollToEnd = true).
Elias
Cheers,
Chris
Signed-off-by: Elias Naur <mail@eliasnaur.com>
---
widget/list.go | 24 +++++++++++++++++++++++-widget/material/list.go | 13 ++++++++++++-
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/widget/list.go b/widget/list.go
index 1d0c2a6..b293c0e 100644
--- a/widget/list.go+++ b/widget/list.go
@@ -24,6 +24,10 @@ type Scrollbar struct {
track, indicator gesture.Click
drag gesture.Drag
delta float32
++ dragging bool+ scrollScale float32+ dragPos, oldDragPos float32}
// Layout updates the internal state of the scrollbar based on events
@@ -60,12 +64,23 @@ func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart,
// Offset to account for any drags.
for _, event := range s.drag.Events(gtx.Metric, gtx, gesture.Axis(axis)) {
- if event.Type != pointer.Drag {+ switch event.Type {+ case pointer.Drag:+ case pointer.Release:+ s.dragging = false+ default: continue
}
dragOffset := axis.FConvert(event.Position).X
normalizedDragOffset := dragOffset / trackHeight
s.delta += normalizedDragOffset - viewportStart
++ s.dragPos = dragOffset+ if !s.dragging {+ s.dragging = true+ s.scrollScale = 1.0 / trackHeight+ s.oldDragPos = s.dragPos+ } }
// Process events from the indicator so that hover is
@@ -105,6 +120,13 @@ func (s *Scrollbar) ScrollDistance() float32 {
return s.delta
}
+func (s *Scrollbar) ScrollDistance2() float32 {+ d := s.dragPos - s.oldDragPos+ dscroll := float32(d) * s.scrollScale+ s.oldDragPos = s.dragPos+ return dscroll+}+// List holds the persistent state for a layout.List that has a
// scrollbar attached.
type List struct {
diff --git a/widget/material/list.go b/widget/material/list.go
index 220671e..b3f8b96 100644
--- a/widget/material/list.go+++ b/widget/material/list.go
@@ -279,12 +279,23 @@ func (l ListStyle) Layout(gtx layout.Context, length int, w layout.ListElement)
return l.ScrollbarStyle.Layout(gtx, l.state.Axis, start, end)
})
- if delta := l.state.ScrollDistance(); delta != 0 {+ /*if delta := l.state.ScrollDistance(); delta != 0 { // Handle any changes to the list position as a result of user interaction
// with the scrollbar.
deltaPx := int(math.Round(float64(float32(l.state.Position.Length) * delta)))
l.state.List.Position.Offset += deltaPx
+ // Ensure that the list pays attention to the Offset field when the scrollbar drag+ // is started while the bar is at the end of the list. Without this, the scrollbar+ // cannot be dragged away from the end.+ l.state.List.Position.BeforeEnd = true+ }*/++ if delta := l.state.ScrollDistance2(); delta != 0 {+ // Handle any changes to the list position as a result of user interaction+ // with the scrollbar.+ l.state.List.Position.Offset += int(math.Round(float64(float32(l.state.Position.Length) * delta)))+ // Ensure that the list pays attention to the Offset field when the scrollbar drag
// is started while the bar is at the end of the list. Without this, the scrollbar
// cannot be dragged away from the end.
--
2.32.0