v0.4.0 b6be3a8

motion.go RawHistoryFolder

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}

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