Skip to content

Commit 5da04b9

Browse files
Implement a TouchpadBackend (#2497)
Co-authored-by: Leonardo Lemos <[email protected]>
1 parent ff401a5 commit 5da04b9

File tree

8 files changed

+201
-21
lines changed

8 files changed

+201
-21
lines changed

lib/Gestures/GestureBackend.vala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
private interface Gala.GestureBackend : Object {
99
public signal bool on_gesture_detected (Gesture gesture, uint32 timestamp);
10-
public signal void on_begin (double delta, uint64 time);
11-
public signal void on_update (double delta, uint64 time);
12-
public signal void on_end (double delta, uint64 time);
10+
public signal void on_begin (double percentage, uint64 time);
11+
public signal void on_update (double percentage, uint64 time);
12+
public signal void on_end (double percentage, uint64 time);
1313

1414
public virtual void prepare_gesture_handling () { }
1515

lib/Gestures/GestureController.vala

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,17 @@
1919
* it will be a hard boundary, if they are fractional it will slow the gesture progress when over the
2020
* limit simulating a kind of spring that pushes against it.
2121
* Note that the progress snaps to full integer values after a gesture ends.
22+
* Events are always shared between all GestureControllers in the same group (except for the group NONE).
23+
* This means that two gestures that can be done in one motion (e.g. horizontal and vertical swipe)
24+
* can be done simultaneously if each of two GestureControllers in the same group handle one of
25+
* the gestures.
2226
*/
2327
public class Gala.GestureController : Object {
28+
public enum Group {
29+
NONE,
30+
MULTITASKING_VIEW,
31+
}
32+
2433
/**
2534
* When a gesture ends with a velocity greater than this constant, the action is not cancelled,
2635
* even if the animation threshold has not been reached.
@@ -34,6 +43,7 @@ public class Gala.GestureController : Object {
3443

3544
public GestureAction action { get; construct; }
3645
public WindowManager wm { get; construct; }
46+
public Group group { get; construct; }
3747

3848
private unowned RootTarget? _target;
3949
public RootTarget target {
@@ -77,7 +87,8 @@ public class Gala.GestureController : Object {
7787

7888
public bool recognizing { get; private set; }
7989

80-
private ToucheggBackend? touchpad_backend;
90+
private ToucheggBackend? touchegg_backend;
91+
private TouchpadBackend? touchpad_backend;
8192
private ScrollBackend? scroll_backend;
8293

8394
private GestureBackend? recognizing_backend;
@@ -90,8 +101,8 @@ public class Gala.GestureController : Object {
90101

91102
private SpringTimeline? timeline;
92103

93-
public GestureController (GestureAction action, WindowManager wm) {
94-
Object (action: action, wm: wm);
104+
public GestureController (GestureAction action, WindowManager wm, Group group = NONE) {
105+
Object (action: action, wm: wm, group: group);
95106
}
96107

97108
/**
@@ -107,12 +118,20 @@ public class Gala.GestureController : Object {
107118
unref ();
108119
}
109120

110-
public void enable_touchpad () {
111-
touchpad_backend = ToucheggBackend.get_default ();
112-
touchpad_backend.on_gesture_detected.connect (gesture_detected);
113-
touchpad_backend.on_begin.connect (gesture_begin);
114-
touchpad_backend.on_update.connect (gesture_update);
115-
touchpad_backend.on_end.connect (gesture_end);
121+
public void enable_touchpad (Clutter.Actor actor) {
122+
if (Meta.Util.is_wayland_compositor ()) {
123+
touchpad_backend = new TouchpadBackend (actor, group);
124+
touchpad_backend.on_gesture_detected.connect (gesture_detected);
125+
touchpad_backend.on_begin.connect (gesture_begin);
126+
touchpad_backend.on_update.connect (gesture_update);
127+
touchpad_backend.on_end.connect (gesture_end);
128+
}
129+
130+
touchegg_backend = ToucheggBackend.get_default (); // Will automatically filter events on wayland
131+
touchegg_backend.on_gesture_detected.connect (gesture_detected);
132+
touchegg_backend.on_begin.connect (gesture_begin);
133+
touchegg_backend.on_update.connect (gesture_update);
134+
touchegg_backend.on_end.connect (gesture_end);
116135
}
117136

118137
public void enable_scroll (Clutter.Actor actor, Clutter.Orientation orientation) {

lib/Gestures/ToucheggBackend.vala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ private class Gala.ToucheggBackend : Object, GestureBackend {
192192
signal_params.get ("(uudiut)", out type, out direction, out percentage, out fingers,
193193
out performed_on_device_type, out elapsed_time);
194194

195+
if (Meta.Util.is_wayland_compositor () && performed_on_device_type != DeviceType.TOUCHSCREEN && type != PINCH) {
196+
return;
197+
}
198+
195199
var delta = percentage * DELTA_MULTIPLIER;
196200

197201
switch (signal_name) {

lib/Gestures/TouchpadBackend.vala

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
private class Gala.TouchpadBackend : Object, GestureBackend {
9+
private const int TOUCHPAD_BASE_HEIGHT = 300;
10+
private const int TOUCHPAD_BASE_WIDTH = 400;
11+
private const int DRAG_THRESHOLD_DISTANCE = 16;
12+
13+
private enum State {
14+
NONE,
15+
IGNORED,
16+
IGNORED_HORIZONTAL,
17+
IGNORED_VERTICAL,
18+
ONGOING
19+
}
20+
21+
public Clutter.Actor actor { get; construct; }
22+
public GestureController.Group group { get; construct; }
23+
24+
private static List<TouchpadBackend> instances = new List<TouchpadBackend> ();
25+
26+
private State state = NONE;
27+
private GestureDirection direction = UNKNOWN;
28+
private double distance_x = 0;
29+
private double distance_y = 0;
30+
private double distance = 0;
31+
32+
public TouchpadBackend (Clutter.Actor actor, GestureController.Group group) {
33+
Object (actor: actor, group: group);
34+
}
35+
36+
~TouchpadBackend () {
37+
instances.remove (this);
38+
}
39+
40+
construct {
41+
actor.captured_event.connect (on_captured_event);
42+
43+
instances.append (this);
44+
}
45+
46+
public override void cancel_gesture () {
47+
state = IGNORED;
48+
}
49+
50+
private bool on_captured_event (Clutter.Event event) {
51+
return handle_event (event, true);
52+
}
53+
54+
private bool handle_event (Clutter.Event event, bool main_handler) {
55+
if (event.get_type () != TOUCHPAD_SWIPE) {
56+
return Clutter.EVENT_PROPAGATE;
57+
}
58+
59+
if (state == IGNORED) {
60+
if (event.get_gesture_phase () == END || event.get_gesture_phase () == CANCEL) {
61+
reset ();
62+
}
63+
64+
return Clutter.EVENT_PROPAGATE;
65+
}
66+
67+
double delta_x, delta_y;
68+
event.get_gesture_motion_delta_unaccelerated (out delta_x, out delta_y);
69+
70+
if (state != ONGOING) {
71+
distance_x += delta_x;
72+
distance_y += delta_y;
73+
74+
Gesture? gesture = null;
75+
State state_if_ignored = NONE;
76+
77+
var threshold = main_handler ? DRAG_THRESHOLD_DISTANCE : DRAG_THRESHOLD_DISTANCE * 4;
78+
79+
if (state != IGNORED_HORIZONTAL && distance_x.abs () >= threshold) {
80+
gesture = new Gesture ();
81+
gesture.direction = direction = distance_x > 0 ? GestureDirection.RIGHT : GestureDirection.LEFT;
82+
state_if_ignored = IGNORED_HORIZONTAL;
83+
} else if (state != IGNORED_VERTICAL && distance_y.abs () >= threshold) {
84+
gesture = new Gesture ();
85+
gesture.direction = direction = distance_y > 0 ? GestureDirection.DOWN : GestureDirection.UP;
86+
state_if_ignored = IGNORED_VERTICAL;
87+
} else {
88+
return Clutter.EVENT_PROPAGATE;
89+
}
90+
91+
gesture.type = event.get_type ();
92+
gesture.fingers = (int) event.get_touchpad_gesture_finger_count ();
93+
gesture.performed_on_device_type = event.get_device ().get_device_type ();
94+
95+
if (!on_gesture_detected (gesture, event.get_time ())) {
96+
if (state == NONE) {
97+
state = state_if_ignored;
98+
} else { // Both directions were ignored, so stop trying
99+
state = IGNORED;
100+
}
101+
return Clutter.EVENT_PROPAGATE;
102+
}
103+
104+
state = ONGOING;
105+
on_begin (0, event.get_time ());
106+
} else if (main_handler && group != NONE) {
107+
foreach (var instance in instances) {
108+
if (instance != this && instance.group == group) {
109+
instance.handle_event (event, false);
110+
}
111+
}
112+
}
113+
114+
distance += get_value_for_direction (delta_x, delta_y);
115+
116+
var percentage = get_percentage (distance);
117+
118+
switch (event.get_gesture_phase ()) {
119+
case BEGIN:
120+
// We don't rely on the begin phase because we delay activation until the drag threshold is reached
121+
break;
122+
123+
case UPDATE:
124+
on_update (percentage, event.get_time ());
125+
break;
126+
127+
case END:
128+
case CANCEL:
129+
on_end (percentage, event.get_time ());
130+
reset ();
131+
break;
132+
}
133+
134+
return Clutter.EVENT_STOP;
135+
}
136+
137+
private void reset () {
138+
state = NONE;
139+
distance = 0;
140+
direction = UNKNOWN;
141+
distance_x = 0;
142+
distance_y = 0;
143+
}
144+
145+
private double get_percentage (double value) {
146+
return value / (direction == LEFT || direction == RIGHT ? TOUCHPAD_BASE_WIDTH : TOUCHPAD_BASE_HEIGHT);
147+
}
148+
149+
private double get_value_for_direction (double delta_x, double delta_y) {
150+
if (direction == LEFT || direction == RIGHT) {
151+
return direction == LEFT ? -delta_x : delta_x;
152+
} else {
153+
return direction == UP ? -delta_y : delta_y;
154+
}
155+
}
156+
}

lib/meson.build

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ gala_lib_sources = files(
3030
'Gestures/RootTarget.vala',
3131
'Gestures/ScrollBackend.vala',
3232
'Gestures/SpringTimeline.vala',
33-
'Gestures/ToucheggBackend.vala'
33+
'Gestures/ToucheggBackend.vala',
34+
'Gestures/TouchpadBackend.vala'
3435
)
3536

3637
gala_resources = gnome.compile_resources(

src/Widgets/MultitaskingView/MultitaskingView.vala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,18 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
5858
opened = false;
5959
display = wm.get_display ();
6060

61-
multitasking_gesture_controller = new GestureController (MULTITASKING_VIEW, wm);
62-
multitasking_gesture_controller.enable_touchpad ();
61+
multitasking_gesture_controller = new GestureController (MULTITASKING_VIEW, wm, MULTITASKING_VIEW);
62+
multitasking_gesture_controller.enable_touchpad (wm.stage);
6363
add_gesture_controller (multitasking_gesture_controller);
6464

6565
add_target (ShellClientsManager.get_instance ()); // For hiding the panels
6666

6767
workspaces = new WorkspaceRow (display);
6868

69-
workspaces_gesture_controller = new GestureController (SWITCH_WORKSPACE, wm) {
69+
workspaces_gesture_controller = new GestureController (SWITCH_WORKSPACE, wm, MULTITASKING_VIEW) {
7070
overshoot_upper_clamp = 0.1
7171
};
72-
workspaces_gesture_controller.enable_touchpad ();
72+
workspaces_gesture_controller.enable_touchpad (wm.stage);
7373
workspaces_gesture_controller.enable_scroll (this, HORIZONTAL);
7474
add_gesture_controller (workspaces_gesture_controller);
7575

@@ -252,7 +252,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
252252
show ();
253253
grab_key_focus ();
254254

255-
modal_proxy = wm.push_modal (this);
255+
modal_proxy = wm.push_modal (get_stage ());
256256
modal_proxy.set_keybinding_filter (keybinding_filter);
257257
modal_proxy.allow_actions ({ MULTITASKING_VIEW, SWITCH_WORKSPACE, ZOOM });
258258

src/Widgets/WindowSwitcher/WindowSwitcher.vala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public class Gala.WindowSwitcher : CanvasActor, GestureTarget, RootTarget {
6565
overshoot_lower_clamp = int.MIN,
6666
snap = false
6767
};
68-
gesture_controller.enable_touchpad ();
68+
gesture_controller.enable_touchpad (wm.stage);
6969
gesture_controller.notify["recognizing"].connect (recognizing_changed);
7070
add_gesture_controller (gesture_controller);
7171

@@ -448,7 +448,7 @@ public class Gala.WindowSwitcher : CanvasActor, GestureTarget, RootTarget {
448448
}
449449

450450
private void push_modal () {
451-
modal_proxy = wm.push_modal (this);
451+
modal_proxy = wm.push_modal (get_stage ());
452452
modal_proxy.allow_actions ({ SWITCH_WINDOWS });
453453
modal_proxy.set_keybinding_filter ((binding) => {
454454
var action = Meta.Prefs.get_keybinding_action (binding.get_name ());

src/Zoom.vala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class Gala.Zoom : Object, GestureTarget, RootTarget {
3737
gesture_controller = new GestureController (ZOOM, wm) {
3838
snap = false
3939
};
40-
gesture_controller.enable_touchpad ();
40+
gesture_controller.enable_touchpad (wm.stage);
4141
add_gesture_controller (gesture_controller);
4242

4343
behavior_settings = new GLib.Settings ("io.elementary.desktop.wm.behavior");

0 commit comments

Comments
 (0)