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}