jesseduffield.lazygit/pkg/gui/controllers/helpers/drag_autoscroller.go
Stefan Haller f5a069342b Add DragAutoScroller helper
It can be used to auto-scroll a view during drag gestures when the
cursor reaches the edge of the view.
2026-07-31 08:26:35 +02:00

155 lines
4.3 KiB
Go

package helpers
import (
"time"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
const (
dragAutoscrollInitialDelay = 300 * time.Millisecond
dragAutoscrollSlowInterval = 250 * time.Millisecond
dragAutoscrollFastInterval = 100 * time.Millisecond
dragAutoscrollVeryFastInterval = 50 * time.Millisecond
)
// All state is UI-thread-owned. Timer goroutines only enqueue tick back onto
// the UI thread, where generation changes and scroll callbacks are serialized
// with mouse handlers and focus changes.
type DragAutoscroller struct {
c *HelperCommon
context types.Context
canScroll func(direction int) bool
onScroll func(viewIndex int) bool
// Incremented whenever the scroll direction changes or the autoscroller
// is canceled. A scheduled tick carries the generation it was created
// for, so stale ticks can be told apart from the one that is current.
generation uint64
direction int
interval time.Duration
// Last known pointer position relative to the viewport; used by ticks to
// compute which line ends up under the pointer after scrolling.
pointerViewportY int
}
func NewDragAutoscroller(
c *HelperCommon,
context types.Context,
canScroll func(direction int) bool,
onScroll func(viewIndex int) bool,
) *DragAutoscroller {
return &DragAutoscroller{
c: c,
context: context,
canScroll: canScroll,
onScroll: onScroll,
}
}
// Update is called with the pointer position of every drag event. Entering a
// scroll zone arms a timer (with an initial delay, so that merely passing
// through the zone doesn't scroll); once armed, scrolling continues on its
// own until the pointer leaves the zone, the drag ends, or a callback stops
// it.
func (self *DragAutoscroller) Update(pointerViewportY int) {
_, viewportHeight := self.context.GetViewTrait().ViewPortYBounds()
direction, interval := dragAutoscrollZone(viewportHeight, pointerViewportY)
if direction != 0 && self.canScroll != nil && !self.canScroll(direction) {
direction = 0
interval = 0
}
self.pointerViewportY = pointerViewportY
generation, schedule := self.updateState(direction, interval)
if schedule {
self.schedule(generation, dragAutoscrollInitialDelay)
}
}
func (self *DragAutoscroller) Direction() int {
return self.direction
}
func (self *DragAutoscroller) updateState(direction int, interval time.Duration) (uint64, bool) {
if direction == self.direction {
self.interval = interval
return self.generation, false
}
self.generation++
self.direction = direction
self.interval = interval
return self.generation, direction != 0
}
func (self *DragAutoscroller) Cancel() {
self.generation++
self.direction = 0
self.interval = 0
}
func (self *DragAutoscroller) schedule(generation uint64, delay time.Duration) {
time.AfterFunc(delay, func() {
self.c.OnUIThreadBackground(func() error {
self.tick(generation)
return nil
})
})
}
func (self *DragAutoscroller) tick(generation uint64) {
if generation != self.generation {
return
}
if self.direction == 0 ||
self.canScroll != nil && !self.canScroll(self.direction) {
self.Cancel()
return
}
view := self.context.GetViewTrait()
oldOriginY, _ := view.ViewPortYBounds()
if self.direction < 0 {
view.ScrollUp(1)
} else {
view.ScrollDown(1)
}
newOriginY, _ := view.ViewPortYBounds()
if newOriginY == oldOriginY {
self.Cancel()
return
}
if !self.onScroll(newOriginY + self.pointerViewportY) {
self.Cancel()
return
}
self.schedule(generation, self.interval)
}
// dragAutoscrollZone returns the scroll direction and tick interval for a
// pointer position: anything beyond the view scrolls very fast, the outermost
// viewport row scrolls fast, the row just inside it scrolls slowly, and anything
// further inside doesn't scroll at all.
func dragAutoscrollZone(viewportHeight int, pointerViewportY int) (int, time.Duration) {
switch {
case pointerViewportY < 0:
return -1, dragAutoscrollVeryFastInterval
case pointerViewportY == 0:
return -1, dragAutoscrollFastInterval
case pointerViewportY == 1:
return -1, dragAutoscrollSlowInterval
case pointerViewportY > viewportHeight-1:
return 1, dragAutoscrollVeryFastInterval
case pointerViewportY == viewportHeight-1:
return 1, dragAutoscrollFastInterval
case pointerViewportY == viewportHeight-2:
return 1, dragAutoscrollSlowInterval
default:
return 0, 0
}
}