v0.2.0 13c6a90

snapswipe.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"slices"
6	"time"
7)
8
9// A snap swipe moves a view through sorted snap points (libadwaita's swipe
10// tracker): one gesture reaches the neighbors of the point it starts from
11// at most, so a quick flick never skips a point. Past them the view resists
12// (rubber band); between points it sticks near each point and speeds up in
13// between (detents). When the fingers lift, a quick swipe goes on to the
14// next point in its direction; a slow one settles on the closest point.
15
16const (
17	// snapVelocity is the touchpad speed, in units per second, above which
18	// a swipe goes on to the next point (libadwaita's touchpad threshold).
19	snapVelocity = 600.0
20	// detent is how much the view sticks near a snap point: it follows the
21	// fingers at 1-detent there and 1+detent halfway between points.
22	detent = 0.3
23	// snapEpsilon is how close to a point counts as on it.
24	snapEpsilon = 1e-6
25)
26
27type snapSwipe struct {
28	tracker swipeTracker
29	// scale turns touchpad units into view units.
30	scale float64
31	// start is the view shown when the swipe began; points are sorted.
32	start  float64
33	points []float64
34	// home indexes the point the view rests on or lands on when the swipe
35	// began: the swipe steps from it, so a swipe during a landing goes one
36	// step past the landing, not from wherever the slide shows.
37	home int
38	// landLo and landHi are home's neighbors: where the swipe may land.
39	landLo, landHi float64
40	// lo and hi bound the fingers' view before resistance: the neighbors,
41	// widened to start when a slide was caught past them.
42	lo, hi float64
43	// stops are the detent ends: the points within [lo, hi], and start.
44	stops []float64
45	band  rubberBand
46}
47
48// newSnapSwipe follows a swipe from the view start toward points, the view
49// resting on or heading to rest. A rest between points becomes a point:
50// a slow swipe never settles behind the view it started from.
51func newSnapSwipe(start, rest, scale float64, points []float64, band rubberBand) snapSwipe {
52	s := snapSwipe{scale: scale, start: start, points: points, band: band}
53	s.points = withPoint(s.points, rest)
54	s.home = s.closest(rest)
55	s.landLo = s.points[max(s.home-1, 0)]
56	s.landHi = s.points[min(s.home+1, len(s.points)-1)]
57	s.lo, s.hi = min(s.landLo, start), max(s.landHi, start)
58	for _, v := range s.points {
59		if v >= s.lo && v <= s.hi {
60			s.stops = append(s.stops, v)
61		}
62	}
63	s.stops = withPoint(s.stops, start)
64	return s
65}
66
67// withPoint returns sorted points with v added unless one is within
68// snapEpsilon of it.
69func withPoint(points []float64, v float64) []float64 {
70	i, _ := slices.BinarySearch(points, v)
71	if i < len(points) && points[i]-v <= snapEpsilon || i > 0 && v-points[i-1] <= snapEpsilon {
72		return points
73	}
74	return slices.Insert(slices.Clone(points), i, v)
75}
76
77// indexPoints are the points 0 to n-1: one per workspace.
78func indexPoints(n int) []float64 {
79	points := make([]float64, n)
80	for i := range points {
81		points[i] = float64(i)
82	}
83	return points
84}
85
86func (s *snapSwipe) closest(pos float64) int {
87	best := 0
88	for i, v := range s.points {
89		if math.Abs(v-pos) < math.Abs(s.points[best]-pos) {
90			best = i
91		}
92	}
93	return best
94}
95
96func (s *snapSwipe) push(delta float64, at time.Duration) { s.tracker.push(delta, at) }
97
98// raw is where the fingers put the view, before resistance and detents.
99func (s *snapSwipe) raw() float64 { return s.start + s.tracker.pos*s.scale }
100
101// pos is the view shown: rubber-banded past the bounds, with detents
102// between them.
103func (s *snapSwipe) pos() float64 {
104	x := s.raw()
105	if x < s.lo || x > s.hi {
106		return s.band.clamp(s.lo, s.hi, x)
107	}
108	a, b, ok := s.around(x)
109	if !ok {
110		return x
111	}
112	t := (x - a) / (b - a)
113	return a + (b-a)*(t-detent*math.Sin(2*math.Pi*t)/(2*math.Pi))
114}
115
116// slope is the derivative of pos at the fingers' view.
117func (s *snapSwipe) slope() float64 {
118	x := s.raw()
119	if x < s.lo || x > s.hi {
120		return s.band.clampDerivative(s.lo, s.hi, x)
121	}
122	a, b, ok := s.around(x)
123	if !ok {
124		return 1
125	}
126	return 1 - detent*math.Cos(2*math.Pi*(x-a)/(b-a))
127}
128
129// around returns the stops around x, which lies within [lo, hi].
130func (s *snapSwipe) around(x float64) (a, b float64, ok bool) {
131	for i := 1; i < len(s.stops); i++ {
132		if x <= s.stops[i] {
133			a, b = s.stops[i-1], s.stops[i]
134			return a, b, b-a > snapEpsilon
135		}
136	}
137	return 0, 0, false
138}
139
140// end is the point the swipe settles on when the fingers lift (home when
141// cancelled), and the view's speed for the spring there, in view units
142// per second.
143func (s *snapSwipe) end(cancelled bool, at time.Duration) (target, velocity float64) {
144	// Idle time before the lift slows the swipe down.
145	s.tracker.push(0, at)
146	v := s.tracker.velocity()
147	velocity = v * s.scale * s.slope()
148	switch {
149	case cancelled:
150		return s.points[s.home], velocity
151	case math.Abs(v) < snapVelocity:
152		return min(max(s.points[s.closest(s.raw())], s.landLo), s.landHi), velocity
153	}
154	// A quick swipe lands on the first landing point at or past where its
155	// speed would carry it, in its direction.
156	proj := min(max(s.start+s.tracker.projectedEnd()*s.scale, s.landLo), s.landHi)
157	land := [3]float64{s.landLo, s.points[s.home], s.landHi}
158	if v > 0 {
159		for _, p := range land {
160			if p >= proj-snapEpsilon {
161				return p, velocity
162			}
163		}
164	}
165	// proj lies within [landLo, landHi]: one of them always matches.
166	for _, p := range slices.Backward(land[:]) {
167		if p <= proj+snapEpsilon {
168			return p, velocity
169		}
170	}
171	return s.landLo, velocity
172}
173
174// discreteSwipeMovement is the touchpad distance of one step of a swipe
175// that runs an action instead of sliding the view.
176const discreteSwipeMovement = 300.0
177
178// newStepSwipe is a swipe that settles on a step of -1, 0 or 1.
179func newStepSwipe() snapSwipe {
180	return newSnapSwipe(0, 0, 1/discreteSwipeMovement, []float64{-1, 0, 1}, workspaceBand)
181}
182
183// step is where a step swipe settles when the fingers lift.
184func (s *snapSwipe) step(cancelled bool, at time.Duration) int {
185	target, _ := s.end(cancelled, at)
186	return int(math.Round(target))
187}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close