1package core
2
3import (
4 "math"
5 "time"
6)
7
8// Touchpad motion follows GNOME Shell and niri: a swipe tracker measures the
9// fingers' speed, the gesture lands where that speed would decelerate to a
10// halt, and a spring carries the view there from where the fingers left it.
11
12// trackerHistory is how far back the swipe speed is measured.
13const trackerHistory = 150 * time.Millisecond
14
15// touchpadDeceleration is the per-millisecond speed ratio of a swipe left
16// to decelerate after the fingers lift.
17const touchpadDeceleration = 0.997
18
19type trackerEvent struct {
20 delta float64
21 at time.Duration
22}
23
24// swipeTracker adds up one swipe axis. Times are event timestamps (the
25// device clock), so the speed does not depend on when events are read.
26type swipeTracker struct {
27 history []trackerEvent
28 pos float64
29}
30
31// push adds a movement. An event older than the last one is ignored.
32func (t *swipeTracker) push(delta float64, at time.Duration) {
33 if n := len(t.history); n > 0 && at < t.history[n-1].at {
34 return
35 }
36 t.history = append(t.history, trackerEvent{delta: delta, at: at})
37 t.pos += delta
38 i := 0
39 for i < len(t.history) && at > t.history[i].at+trackerHistory {
40 i++
41 }
42 t.history = t.history[i:]
43}
44
45// velocity is the speed over the recent history, in units per second.
46func (t *swipeTracker) velocity() float64 {
47 if len(t.history) < 2 {
48 return 0
49 }
50 span := (t.history[len(t.history)-1].at - t.history[0].at).Seconds()
51 if span <= 0 {
52 return 0
53 }
54 sum := 0.0
55 for _, e := range t.history {
56 sum += e.delta
57 }
58 return sum / span
59}
60
61// projectedEnd is where the swipe stops once its speed decelerates.
62func (t *swipeTracker) projectedEnd() float64 {
63 return t.pos - t.velocity()/(1000*math.Log(touchpadDeceleration))
64}
65
66// spring is a damped spring from From to To (libadwaita's model, as in
67// niri). Velocity is the initial speed in units per second.
68type spring struct {
69 From, To, Velocity float64
70 DampingRatio float64
71 Stiffness float64
72 Epsilon float64
73 // Snap, when set, settles the motion as soon as it can no longer
74 // stray that far from To: a position in pixels whose rounded value
75 // is already the settled one draws no different frame, so the tail
76 // of the spring (sub-pixel, hundreds of frames with nothing to show)
77 // is cut. Fades and veils keep Epsilon alone. Critically damped
78 // springs only (settled).
79 Snap float64
80}
81
82// pixelSnap is the Snap of a spring in logical pixels: offsets are rounded
83// to whole logical pixels before the renderer scales them (rectMotion.apply,
84// Workspace.shiftPixels), so a spring that stays under half a pixel from
85// its target draws the settled rect from then on, at any output scale.
86const pixelSnap = 0.5
87
88// viewSpring moves a scrolled view, or a rect offset, in logical pixels;
89// workspaceSpring switches workspaces, in workspaces (the caller sets its
90// Snap from the output height). Both are critically damped: fast, with no
91// overshoot.
92func viewSpring(from, velocity float64) spring {
93 return spring{From: from, Velocity: velocity, DampingRatio: 1, Stiffness: 800, Epsilon: 0.0001, Snap: pixelSnap}
94}
95
96// levelSpring is viewSpring for a level in 0..1 (a fade, a veil): no snap,
97// a fraction of a level still shows.
98func levelSpring(from, velocity float64) spring {
99 s := viewSpring(from, velocity)
100 s.Snap = 0
101 return s
102}
103
104func workspaceSpring(from, velocity float64) spring {
105 return spring{From: from, Velocity: velocity, DampingRatio: 1, Stiffness: 1000, Epsilon: 0.0001}
106}
107
108// settled reports whether a critically damped spring at pos with speed vel
109// stays within Snap of To from now on. From there the offset is
110// x(t) = e^(-βt)(x + (βx+v)t), bounded by |x| + (β|x|+|v|)·max(t·e^(-βt))
111// = |x|(1+1/e) + |v|/(βe) (the peak is at t = 1/β).
112func (s spring) settled(pos, vel float64) bool {
113 if s.Snap <= 0 {
114 return false
115 }
116 beta, omega0 := s.coefficients()
117 if math.Abs(beta-omega0) > f32Epsilon || beta <= 0 {
118 return false
119 }
120 x := math.Abs(pos - s.To)
121 return x*(1+1/math.E)+math.Abs(vel)/(beta*math.E) < s.Snap
122}
123
124// f32Epsilon compares damping terms: float64's epsilon is too small for
125// them (libadwaita, niri).
126const f32Epsilon = 1.1920929e-07
127
128func (s spring) coefficients() (beta, omega0 float64) {
129 const mass = 1.0
130 k := max(s.Stiffness, 0)
131 damping := max(s.DampingRatio, 0) * 2 * math.Sqrt(mass*k)
132 return damping / (2 * mass), math.Sqrt(k / mass)
133}
134
135// valueAt is the spring position t after it started.
136func (s spring) valueAt(t time.Duration) float64 {
137 sec := t.Seconds()
138 beta, omega0 := s.coefficients()
139 x0 := s.From - s.To
140 envelope := math.Exp(-beta * sec)
141 switch {
142 case math.Abs(beta-omega0) <= f32Epsilon:
143 // Critically damped.
144 return s.To + envelope*(x0+(beta*x0+s.Velocity)*sec)
145 case beta < omega0:
146 w := math.Sqrt(omega0*omega0 - beta*beta)
147 return s.To + envelope*(x0*math.Cos(w*sec)+((beta*x0+s.Velocity)/w)*math.Sin(w*sec))
148 default:
149 w := math.Sqrt(beta*beta - omega0*omega0)
150 return s.To + envelope*(x0*math.Cosh(w*sec)+((beta*x0+s.Velocity)/w)*math.Sinh(w*sec))
151 }
152}
153
154// duration is how long the spring takes to settle within Epsilon.
155func (s spring) duration() time.Duration {
156 const step = 0.001
157 beta, omega0 := s.coefficients()
158 if beta <= 0 {
159 return time.Duration(math.MaxInt64)
160 }
161 if math.Abs(s.To-s.From) <= math.SmallestNonzeroFloat64 && s.Velocity == 0 {
162 return 0
163 }
164 x0 := -math.Log(max(s.Epsilon, math.SmallestNonzeroFloat64)) / beta
165 if math.Abs(beta-omega0) <= f32Epsilon || beta < omega0 {
166 return seconds(x0)
167 }
168 // Overdamped: the envelope decays faster than the motion; find where
169 // the motion itself settles (Newton's method).
170 at := func(sec float64) float64 { return s.valueAt(seconds(sec)) }
171 y0 := at(x0)
172 m := (at(x0+step) - y0) / step
173 x1 := (s.To - y0 + m*x0) / m
174 y1 := at(x1)
175 for i := 0; math.Abs(s.To-y1) > s.Epsilon; i++ {
176 if i > 1000 {
177 return 0
178 }
179 x0, y0 = x1, y1
180 m = (at(x0+step) - y0) / step
181 x1 = (s.To - y0 + m*x0) / m
182 y1 = at(x1)
183 if math.IsNaN(y1) || math.IsInf(y1, 0) {
184 return seconds(x0)
185 }
186 }
187 return seconds(x1)
188}
189
190func seconds(s float64) time.Duration { return time.Duration(s * float64(time.Second)) }
191
192// velocityAt is the spring speed t after it started, in units per second.
193func (s spring) velocityAt(t time.Duration) float64 {
194 sec := t.Seconds()
195 beta, omega0 := s.coefficients()
196 x0 := s.From - s.To
197 envelope := math.Exp(-beta * sec)
198 switch {
199 case math.Abs(beta-omega0) <= f32Epsilon:
200 // Critically damped.
201 return envelope * (s.Velocity - beta*(beta*x0+s.Velocity)*sec)
202 case beta < omega0:
203 w := math.Sqrt(omega0*omega0 - beta*beta)
204 b := (beta*x0 + s.Velocity) / w
205 cos, sin := math.Cos(w*sec), math.Sin(w*sec)
206 return envelope * (-beta*(x0*cos+b*sin) + w*(b*cos-x0*sin))
207 default:
208 w := math.Sqrt(beta*beta - omega0*omega0)
209 b := (beta*x0 + s.Velocity) / w
210 cosh, sinh := math.Cosh(w*sec), math.Sinh(w*sec)
211 return envelope * (-beta*(x0*cosh+b*sinh) + w*(x0*sinh+b*cosh))
212 }
213}
214
215// motion is a spring started at a given time; a zero start begins at the
216// first frame. It is a value: on tells whether one runs. slow stretches
217// the spring's time (a slowdown of 2 takes twice as long) and v is the
218// current speed in units per second, so a retarget can keep it.
219type motion struct {
220 spring spring
221 start time.Time
222 end time.Duration
223 slow float64
224 v float64
225 on bool
226}
227
228// newMotion starts s at now, slowed down slow times (slow <= 0 is 1). The
229// spring's Velocity is in units per second of real time.
230func newMotion(s spring, now time.Time, slow float64) motion {
231 if slow <= 0 {
232 slow = 1
233 }
234 v := s.Velocity
235 s.Velocity *= slow
236 // The slowdown is capped by the config, so this cannot overflow.
237 end := time.Duration(float64(s.duration()) * slow)
238 return motion{spring: s, start: now, end: end, slow: slow, v: v, on: true}
239}
240
241// at is the position at now and whether the motion has settled.
242func (m *motion) at(now time.Time) (float64, bool) {
243 if m.start.IsZero() {
244 m.start = now
245 }
246 t := now.Sub(m.start)
247 if t >= m.end {
248 m.v = 0
249 return m.spring.To, true
250 }
251 tau := time.Duration(float64(max(t, 0)) / m.slow)
252 pos, vel := m.spring.valueAt(tau), m.spring.velocityAt(tau)
253 if m.spring.settled(pos, vel) {
254 m.v = 0
255 return m.spring.To, true
256 }
257 m.v = vel / m.slow
258 return pos, false
259}
260
261// sampleAt is the position and speed m has at now, in units per second,
262// without moving m: m is a copy, so at's bookkeeping (start, v) stays out.
263func (m motion) sampleAt(now time.Time) (pos, vel float64) {
264 pos, _ = m.at(now)
265 return pos, m.v
266}
267
268// scale multiplies the motion's distance and speed by k: the spring is
269// linear in its start offset and speed, so the progress is unchanged.
270func (m *motion) scale(k float64) {
271 if !m.on {
272 return
273 }
274 m.spring.From *= k
275 m.spring.Velocity *= k
276 m.v *= k
277}
278
279// velocity is the speed of the last sample, in units per second.
280func (m motion) velocity() float64 { return m.v }
281
282// rubberBand resists movement past the edges: the further out, the less
283// it moves, never more than limit.
284type rubberBand struct{ stiffness, limit float64 }
285
286// workspaceBand holds a swipe past the steps it may reach, in steps: a
287// twentieth of a workspace at most. Column swipes scale it to pixels.
288var workspaceBand = rubberBand{stiffness: 0.5, limit: 0.05}
289
290// scaled is the band measured in units k times smaller.
291func (r rubberBand) scaled(k float64) rubberBand {
292 return rubberBand{stiffness: r.stiffness, limit: r.limit * k}
293}
294
295func (r rubberBand) band(x float64) float64 {
296 return (1 - 1/(x*r.stiffness/r.limit+1)) * r.limit
297}
298
299func (r rubberBand) derivative(x float64) float64 {
300 d := r.stiffness*x + r.limit
301 return r.stiffness * r.limit * r.limit / (d * d)
302}
303
304// clamp keeps x in [lo, hi], letting it stretch past the edges.
305func (r rubberBand) clamp(lo, hi, x float64) float64 {
306 c := min(max(x, lo), hi)
307 if x < c {
308 return c - r.band(c-x)
309 }
310 return c + r.band(x-c)
311}
312
313// clampDerivative is the slope of clamp at x: 1 inside the edges.
314func (r rubberBand) clampDerivative(lo, hi, x float64) float64 {
315 if lo <= x && x <= hi {
316 return 1
317 }
318 c := min(max(x, lo), hi)
319 return r.derivative(math.Abs(x - c))
320}