Skip to content

Commit 466926b

Browse files
committed
Implement a TouchpadBackend
1 parent 5914298 commit 466926b

File tree

8 files changed

+197
-21
lines changed

8 files changed

+197
-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: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
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 with the same id.
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 with the same id handle one of
25+
* the gestures.
2226
*/
2327
public class Gala.GestureController : Object {
2428
/**
@@ -34,6 +38,7 @@ public class Gala.GestureController : Object {
3438

3539
public GestureAction action { get; construct; }
3640
public WindowManager wm { get; construct; }
41+
public string? id { get; construct; }
3742

3843
private unowned RootTarget? _target;
3944
public RootTarget target {
@@ -77,7 +82,8 @@ public class Gala.GestureController : Object {
7782

7883
public bool recognizing { get; private set; }
7984

80-
private ToucheggBackend? touchpad_backend;
85+
private ToucheggBackend? touchegg_backend;
86+
private TouchpadBackend? touchpad_backend;
8187
private ScrollBackend? scroll_backend;
8288

8389
private GestureBackend? recognizing_backend;
@@ -90,8 +96,8 @@ public class Gala.GestureController : Object {
9096

9197
private SpringTimeline? timeline;
9298

93-
public GestureController (GestureAction action, WindowManager wm) {
94-
Object (action: action, wm: wm);
99+
public GestureController (GestureAction action, WindowManager wm, string? id = null) {
100+
Object (action: action, wm: wm, id: id);
95101
}
96102

97103
/**
@@ -107,12 +113,20 @@ public class Gala.GestureController : Object {
107113
unref ();
108114
}
109115

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);
116+
public void enable_touchpad (Clutter.Actor actor) {
117+
if (Meta.Util.is_wayland_compositor ()) {
118+
touchpad_backend = new TouchpadBackend (actor, id);
119+
touchpad_backend.on_gesture_detected.connect (gesture_detected);
120+
touchpad_backend.on_begin.connect (gesture_begin);
121+
touchpad_backend.on_update.connect (gesture_update);
122+
touchpad_backend.on_end.connect (gesture_end);
123+
}
124+
125+
touchegg_backend = ToucheggBackend.get_default (); // Will automatically filter events on wayland
126+
touchegg_backend.on_gesture_detected.connect (gesture_detected);
127+
touchegg_backend.on_begin.connect (gesture_begin);
128+
touchegg_backend.on_update.connect (gesture_update);
129+
touchegg_backend.on_end.connect (gesture_end);
116130
}
117131

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

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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableComponent {
2424
public const int ANIMATION_DURATION = 250;
2525

26+
private const string GESTURE_CONTROLLER_ID = "multitaskingview";
27+
2628
private GestureController workspaces_gesture_controller;
2729
private GestureController multitasking_gesture_controller;
2830

@@ -58,18 +60,18 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
5860
opened = false;
5961
display = wm.get_display ();
6062

61-
multitasking_gesture_controller = new GestureController (MULTITASKING_VIEW, wm);
62-
multitasking_gesture_controller.enable_touchpad ();
63+
multitasking_gesture_controller = new GestureController (MULTITASKING_VIEW, wm, GESTURE_CONTROLLER_ID);
64+
multitasking_gesture_controller.enable_touchpad (wm.stage);
6365
add_gesture_controller (multitasking_gesture_controller);
6466

6567
add_target (ShellClientsManager.get_instance ()); // For hiding the panels
6668

6769
workspaces = new WorkspaceRow (display);
6870

69-
workspaces_gesture_controller = new GestureController (SWITCH_WORKSPACE, wm) {
71+
workspaces_gesture_controller = new GestureController (SWITCH_WORKSPACE, wm, GESTURE_CONTROLLER_ID) {
7072
overshoot_upper_clamp = 0.1
7173
};
72-
workspaces_gesture_controller.enable_touchpad ();
74+
workspaces_gesture_controller.enable_touchpad (wm.stage);
7375
workspaces_gesture_controller.enable_scroll (this, HORIZONTAL);
7476
add_gesture_controller (workspaces_gesture_controller);
7577

@@ -252,7 +254,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
252254
show ();
253255
grab_key_focus ();
254256

255-
modal_proxy = wm.push_modal (this);
257+
modal_proxy = wm.push_modal (get_stage ());
256258
modal_proxy.set_keybinding_filter (keybinding_filter);
257259
modal_proxy.allow_actions ({ MULTITASKING_VIEW, SWITCH_WORKSPACE, ZOOM });
258260

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)