Skip to content

Commit dd8f1c7

Browse files
committed
Introduce GestureController
1 parent 741f2ac commit dd8f1c7

File tree

4 files changed

+417
-0
lines changed

4 files changed

+417
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. (https://elementary.io)
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Leonhard Kargl <[email protected]>
6+
*/
7+
8+
public abstract class Gala.GestureTarget : Object {
9+
/**
10+
* The actor manipulated by the gesture. The associated frame clock
11+
* will be used for animation timelines.
12+
*/
13+
public Clutter.Actor actor { get; construct; }
14+
15+
public abstract void update (double progress);
16+
}
17+
18+
public class Gala.GestureController : Object {
19+
/**
20+
* When a gesture ends with a velocity greater than this constant, the action is not cancelled,
21+
* even if the animation threshold has not been reached.
22+
*/
23+
private const double SUCCESS_VELOCITY_THRESHOLD = 0.003;
24+
25+
/**
26+
* Maximum velocity allowed on gesture update.
27+
*/
28+
private const double MAX_VELOCITY = 0.01;
29+
30+
// These are for calculations that only have to be done once
31+
public signal void commit (double progress);
32+
33+
public GestureSettings.GestureAction action { get; construct set; }
34+
public double distance { get; construct set; }
35+
public double overshoot_lower_clamp { get; construct set; default = 0d; }
36+
public double overshoot_upper_clamp { get; construct set; default = 1d; }
37+
38+
private double _progress = 0;
39+
public double progress {
40+
get { return _progress; }
41+
set {
42+
_progress = value;
43+
44+
var lower_clamp_int = (int) overshoot_lower_clamp;
45+
var upper_clamp_int = (int) overshoot_upper_clamp;
46+
47+
double stretched_percentage = 0;
48+
if (progress < lower_clamp_int) {
49+
stretched_percentage = (progress - lower_clamp_int) * - (overshoot_lower_clamp - lower_clamp_int);
50+
} else if (progress > upper_clamp_int) {
51+
stretched_percentage = (progress - upper_clamp_int) * (overshoot_upper_clamp - upper_clamp_int);
52+
}
53+
54+
var clamped = progress.clamp (lower_clamp_int, upper_clamp_int);
55+
56+
target.update (clamped + stretched_percentage);
57+
}
58+
}
59+
60+
public GestureTarget target { get; construct set; }
61+
62+
private ToucheggBackend touchpad_backend;
63+
private ScrollBackend scroll_backend;
64+
65+
private bool recognizing = false;
66+
private double previous_percentage;
67+
private uint64 previous_time;
68+
private double previous_delta;
69+
private double velocity;
70+
// Used to check whether to cancel. Necessary because on_end is often called
71+
// with the same percentage as the last update so this is the one before the last update.
72+
private double old_previous;
73+
private int direction_multiplier;
74+
75+
private Clutter.Timeline? timeline;
76+
77+
public GestureController (GestureSettings.GestureAction action) {
78+
Object (action: action);
79+
}
80+
81+
public void enable_touchpad () {
82+
touchpad_backend = ToucheggBackend.get_default ();
83+
touchpad_backend.on_gesture_detected.connect (gesture_detected);
84+
touchpad_backend.on_begin.connect (gesture_begin);
85+
touchpad_backend.on_update.connect (gesture_update);
86+
touchpad_backend.on_end.connect (gesture_end);
87+
}
88+
89+
public void enable_scroll (Clutter.Actor actor, Clutter.Orientation orientation) {
90+
scroll_backend = new ScrollBackend (actor, orientation, new GestureSettings ());
91+
scroll_backend.on_gesture_detected.connect (gesture_detected);
92+
scroll_backend.on_begin.connect (gesture_begin);
93+
scroll_backend.on_update.connect (gesture_update);
94+
scroll_backend.on_end.connect (gesture_end);
95+
}
96+
97+
private void prepare () {
98+
if (timeline != null) {
99+
timeline.stop ();
100+
timeline = null;
101+
}
102+
}
103+
104+
private bool gesture_detected (GestureBackend backend, Gesture gesture, uint32 timestamp) {
105+
recognizing = GestureSettings.get_action (gesture) == action || GestureSettings.get_action (gesture) == NONE;
106+
107+
if (recognizing) {
108+
if (gesture.direction == UP || gesture.direction == RIGHT) {
109+
direction_multiplier = 1;
110+
} else {
111+
direction_multiplier = -1;
112+
}
113+
}
114+
115+
return recognizing;
116+
}
117+
118+
private void gesture_begin (double percentage, uint64 elapsed_time) {
119+
if (!recognizing) {
120+
return;
121+
}
122+
123+
prepare ();
124+
125+
previous_percentage = percentage;
126+
previous_time = elapsed_time;
127+
}
128+
129+
private void gesture_update (double percentage, uint64 elapsed_time) {
130+
if (!recognizing) {
131+
return;
132+
}
133+
134+
var updated_delta = previous_delta;
135+
if (elapsed_time != previous_time) {
136+
double distance = percentage - previous_percentage;
137+
double time = (double)(elapsed_time - previous_time);
138+
velocity = (distance / time);
139+
140+
if (velocity > MAX_VELOCITY) {
141+
velocity = MAX_VELOCITY;
142+
var used_percentage = MAX_VELOCITY * time + previous_percentage;
143+
updated_delta += percentage - used_percentage;
144+
}
145+
}
146+
147+
progress += calculate_applied_delta (percentage, updated_delta);
148+
149+
old_previous = previous_percentage;
150+
previous_percentage = percentage;
151+
previous_time = elapsed_time;
152+
previous_delta = updated_delta;
153+
}
154+
155+
private void gesture_end (double percentage, uint64 elapsed_time) {
156+
if (!recognizing) {
157+
return;
158+
}
159+
160+
progress += calculate_applied_delta (percentage, previous_delta);
161+
162+
int completions = (int) Math.round (progress);
163+
164+
if (velocity.abs () > SUCCESS_VELOCITY_THRESHOLD) {
165+
completions += velocity > 0 ? direction_multiplier : -direction_multiplier;
166+
}
167+
168+
var lower_clamp_int = (int) overshoot_lower_clamp;
169+
var upper_clamp_int = (int) overshoot_upper_clamp;
170+
171+
completions = completions.clamp (lower_clamp_int, upper_clamp_int);
172+
173+
recognizing = false;
174+
175+
finish (velocity, (double) completions);
176+
177+
previous_percentage = 0;
178+
previous_time = 0;
179+
previous_delta = 0;
180+
velocity = 0;
181+
old_previous = 0;
182+
direction_multiplier = 0;
183+
}
184+
185+
private inline double calculate_applied_delta (double percentage, double percentage_delta) {
186+
return ((percentage - percentage_delta) - (previous_percentage - previous_delta)) * direction_multiplier;
187+
}
188+
189+
private void finish (double velocity, double to) {
190+
var transition = new SpringTimeline (target.actor, progress, to, velocity, 1, 0.5, 500);
191+
transition.progress.connect ((value) => progress = value);
192+
193+
timeline = transition;
194+
195+
commit (to);
196+
}
197+
198+
public void goto (double to) {
199+
prepare ();
200+
finish (0.005, to);
201+
}
202+
}

src/Gestures/PropertyTarget.vala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. (https://elementary.io)
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Leonhard Kargl <[email protected]>
6+
*/
7+
8+
public class Gala.PropertyTarget : GestureTarget {
9+
/**
10+
* The property that will be animated. To be properly animated it has to be marked as
11+
* animatable in the Clutter documentation and should be numeric.
12+
*/
13+
public string property { get; construct; }
14+
15+
public Clutter.Interval interval { get; construct; }
16+
17+
public PropertyTarget (Clutter.Actor actor, string property, Type value_type, Value from_value, Value to_value) {
18+
Object (actor: actor, property: property, interval: new Clutter.Interval.with_values (value_type, from_value, to_value));
19+
}
20+
21+
public override void update (double progress) {
22+
actor.set_property (property, interval.compute (progress));
23+
}
24+
}

0 commit comments

Comments
 (0)