From 4adf25fde8153a7005f5ff4cffebd12c9f095dc5 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 12 Nov 2025 17:12:08 +0100 Subject: [PATCH 1/3] GestureController: Only accept signals from the recognizing backend Backends should usually make sure that they consume events but just in case and while we're using touchegg and our own pan backend at the same time for touchscreens don't let events from other backends mess with the current gesture. --- lib/Gestures/GestureController.vala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Gestures/GestureController.vala b/lib/Gestures/GestureController.vala index aec8241c1..66bdcf40b 100644 --- a/lib/Gestures/GestureController.vala +++ b/lib/Gestures/GestureController.vala @@ -178,8 +178,8 @@ public class Gala.GestureController : Object { return recognizing; } - private void gesture_begin (double percentage, uint64 elapsed_time) { - if (!recognizing) { + private void gesture_begin (GestureBackend backend, double percentage, uint64 elapsed_time) { + if (!recognizing || backend != recognizing_backend) { return; } @@ -190,8 +190,8 @@ public class Gala.GestureController : Object { previous_time = elapsed_time; } - private void gesture_update (double percentage, uint64 elapsed_time) { - if (!recognizing) { + private void gesture_update (GestureBackend backend, double percentage, uint64 elapsed_time) { + if (!recognizing || backend != recognizing_backend) { return; } @@ -215,8 +215,8 @@ public class Gala.GestureController : Object { previous_delta = updated_delta; } - private void gesture_end (double percentage, uint64 elapsed_time) { - if (!recognizing) { + private void gesture_end (GestureBackend backend, double percentage, uint64 elapsed_time) { + if (!recognizing || backend != recognizing_backend) { return; } @@ -311,8 +311,8 @@ public class Gala.GestureController : Object { public void cancel_gesture () { if (recognizing) { + gesture_end (recognizing_backend, previous_percentage, previous_time); recognizing_backend.cancel_gesture (); - gesture_end (previous_percentage, previous_time); } } } From d0e98b35ce3be369b78a41f3a51f29b8ccac4e89 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Tue, 14 Oct 2025 00:20:14 +0200 Subject: [PATCH 2/3] Introduce a pan backend for touchscreen gestures --- lib/Gestures/GestureBackend.vala | 2 + lib/Gestures/GestureController.vala | 14 +++ lib/Gestures/PanBackend.vala | 152 ++++++++++++++++++++++++++++ lib/Gestures/RootTarget.vala | 4 + lib/Gestures/ToucheggBackend.vala | 2 +- lib/meson.build | 1 + 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 lib/Gestures/PanBackend.vala diff --git a/lib/Gestures/GestureBackend.vala b/lib/Gestures/GestureBackend.vala index b7b0e1261..81cabc9a1 100644 --- a/lib/Gestures/GestureBackend.vala +++ b/lib/Gestures/GestureBackend.vala @@ -6,6 +6,8 @@ */ private interface Gala.GestureBackend : Object { + public signal float request_travel_distance (); + public signal bool on_gesture_detected (Gesture gesture, uint32 timestamp); public signal void on_begin (double percentage, uint64 time); public signal void on_update (double percentage, uint64 time); diff --git a/lib/Gestures/GestureController.vala b/lib/Gestures/GestureController.vala index 66bdcf40b..50146a2af 100644 --- a/lib/Gestures/GestureController.vala +++ b/lib/Gestures/GestureController.vala @@ -89,6 +89,7 @@ public class Gala.GestureController : Object { private ToucheggBackend? touchegg_backend; private TouchpadBackend? touchpad_backend; + private PanBackend? pan_backend; private ScrollBackend? scroll_backend; private GestureBackend? recognizing_backend; @@ -134,6 +135,15 @@ public class Gala.GestureController : Object { touchegg_backend.on_end.connect (gesture_end); } + public void enable_touchscreen (Clutter.Actor actor) { + pan_backend = new PanBackend (wm, actor); + pan_backend.request_travel_distance.connect (on_request_travel_distance); + pan_backend.on_gesture_detected.connect (gesture_detected); + pan_backend.on_begin.connect (gesture_begin); + pan_backend.on_update.connect (gesture_update); + pan_backend.on_end.connect (gesture_end); + } + public void enable_scroll (Clutter.Actor actor, Clutter.Orientation orientation) { scroll_backend = new ScrollBackend (actor, orientation, new GestureSettings ()); scroll_backend.on_gesture_detected.connect (gesture_detected); @@ -142,6 +152,10 @@ public class Gala.GestureController : Object { scroll_backend.on_end.connect (gesture_end); } + private float on_request_travel_distance () { + return target.get_travel_distance (action); + } + private void prepare () { if (timeline != null) { timeline = null; diff --git a/lib/Gestures/PanBackend.vala b/lib/Gestures/PanBackend.vala new file mode 100644 index 000000000..6c16a666a --- /dev/null +++ b/lib/Gestures/PanBackend.vala @@ -0,0 +1,152 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +internal class Gala.PanBackend : Object, GestureBackend { + public WindowManager wm { get; construct; } + public Clutter.Actor actor { get; construct; } + + private ModalProxy? modal_proxy; + + private Clutter.PanAxis pan_axis; + private Clutter.PanAction pan_action; + + private GestureDirection direction; + + private float origin_x; + private float origin_y; + + private float current_x; + private float current_y; + + private float last_x_coord; + private float last_y_coord; + private uint last_n_points; + + public PanBackend (WindowManager wm, Clutter.Actor actor) { + Object (wm: wm, actor: actor); + } + + construct { + pan_action = new Clutter.PanAction () { + n_touch_points = 1 + }; + + actor.add_action_full ("pan-gesture", CAPTURE, pan_action); + + pan_action.gesture_begin.connect (on_gesture_begin); + pan_action.pan.connect (on_pan); + pan_action.gesture_end.connect (on_gesture_end); + pan_action.gesture_cancel.connect (on_gesture_end); + } + + ~PanBackend () { + actor.remove_action (pan_action); + } + + private bool on_gesture_begin () { + if (pan_action.get_last_event (0).get_source_device ().get_device_type () != TOUCHSCREEN_DEVICE) { + return false; + } + + float x_coord, y_coord; + pan_action.get_press_coords (0, out x_coord, out y_coord); + + origin_x = current_x = x_coord; + origin_y = current_y = y_coord; + + var time = pan_action.get_last_event (0).get_time (); + + var handled = on_gesture_detected (build_gesture (), time); + + if (!handled) { + reset (); + return false; + } + + modal_proxy = wm.push_modal (actor, true); + + on_begin (0, time); + + return true; + } + + private void on_gesture_end () { + if (modal_proxy != null) { + // Only emit on end if we actually began the gesture + on_end (calculate_percentage (), Meta.CURRENT_TIME); + } + + reset (); + } + + private void reset () { + if (modal_proxy != null) { + wm.pop_modal (modal_proxy); + modal_proxy = null; + } + + direction = GestureDirection.UNKNOWN; + last_n_points = 0; + last_x_coord = 0; + last_y_coord = 0; + } + + private bool on_pan (Clutter.PanAction pan_action, Clutter.Actor actor, bool interpolate) { + var time = pan_action.get_last_event (0).get_time (); + + float x, y; + pan_action.get_motion_coords (0, out x, out y); + + if (pan_action.get_n_current_points () == last_n_points) { + current_x += x - last_x_coord; + current_y += y - last_y_coord; + } + + last_x_coord = x; + last_y_coord = y; + last_n_points = pan_action.get_n_current_points (); + + on_update (calculate_percentage (), time); + + return true; + } + + private double calculate_percentage () { + float current, origin; + if (pan_axis == X_AXIS) { + current = direction == RIGHT ? float.max (current_x, origin_x) : float.min (current_x, origin_x); + origin = origin_x; + } else { + current = direction == DOWN ? float.max (current_y, origin_y) : float.min (current_y, origin_y); + origin = origin_y; + } + + return (current - origin).abs () / request_travel_distance (); + } + + private Gesture build_gesture () { + float delta_x, delta_y; + ((Clutter.GestureAction) pan_action).get_motion_delta (0, out delta_x, out delta_y); + + pan_axis = delta_x.abs () > delta_y.abs () ? Clutter.PanAxis.X_AXIS : Clutter.PanAxis.Y_AXIS; + + if (pan_axis == X_AXIS) { + direction = delta_x > 0 ? GestureDirection.RIGHT : GestureDirection.LEFT; + } else { + direction = delta_y > 0 ? GestureDirection.DOWN : GestureDirection.UP; + } + + return new Gesture () { + type = Clutter.EventType.TOUCHPAD_SWIPE, + direction = direction, + fingers = (int) pan_action.get_n_current_points (), + performed_on_device_type = Clutter.InputDeviceType.TOUCHSCREEN_DEVICE, + origin_x = origin_x, + origin_y = origin_y + }; + } +} diff --git a/lib/Gestures/RootTarget.vala b/lib/Gestures/RootTarget.vala index fac6afcaa..77956f4dd 100644 --- a/lib/Gestures/RootTarget.vala +++ b/lib/Gestures/RootTarget.vala @@ -12,6 +12,10 @@ public interface Gala.RootTarget : Object, GestureTarget { */ public abstract Clutter.Actor? actor { get; } + public virtual float get_travel_distance (GestureAction for_action) { + return 0.0f; + } + public void add_gesture_controller (GestureController controller) requires (controller.target == null) { controller.attached (this); weak_ref (controller.detached); diff --git a/lib/Gestures/ToucheggBackend.vala b/lib/Gestures/ToucheggBackend.vala index c280efc09..1dbb88fe9 100644 --- a/lib/Gestures/ToucheggBackend.vala +++ b/lib/Gestures/ToucheggBackend.vala @@ -180,7 +180,7 @@ private class Gala.ToucheggBackend : Object, GestureBackend { signal_params.get ("(uudiut)", out type, out direction, out percentage, out fingers, out performed_on_device_type, out elapsed_time); - if (Meta.Util.is_wayland_compositor () && performed_on_device_type != DeviceType.TOUCHSCREEN && type != PINCH) { + if (Meta.Util.is_wayland_compositor () && type != PINCH) { return; } diff --git a/lib/meson.build b/lib/meson.build index 94885b458..e3a70dcbd 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -31,6 +31,7 @@ gala_lib_sources = files( 'Gestures/GestureController.vala', 'Gestures/GestureSettings.vala', 'Gestures/GestureTarget.vala', + 'Gestures/PanBackend.vala', 'Gestures/PropertyTarget.vala', 'Gestures/RootTarget.vala', 'Gestures/ScrollBackend.vala', From 9dd97f56057b44461eeed61fba7df40d7c9a6ea7 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 12 Nov 2025 16:38:59 +0100 Subject: [PATCH 3/3] MultitaskingView: Use new touchscreen backend --- src/Widgets/MultitaskingView/MultitaskingView.vala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Widgets/MultitaskingView/MultitaskingView.vala b/src/Widgets/MultitaskingView/MultitaskingView.vala index 4ac4ba68f..c654d48fe 100644 --- a/src/Widgets/MultitaskingView/MultitaskingView.vala +++ b/src/Widgets/MultitaskingView/MultitaskingView.vala @@ -63,6 +63,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone multitasking_gesture_controller = new GestureController (MULTITASKING_VIEW, wm, MULTITASKING_VIEW); multitasking_gesture_controller.enable_touchpad (wm.stage); + multitasking_gesture_controller.enable_touchscreen (wm.stage); add_gesture_controller (multitasking_gesture_controller); add_target (ShellClientsManager.get_instance ()); // For hiding the panels @@ -74,6 +75,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone follow_natural_scroll = true, }; workspaces_gesture_controller.enable_touchpad (wm.stage); + workspaces_gesture_controller.enable_touchscreen (wm.stage); workspaces_gesture_controller.enable_scroll (this, HORIZONTAL); add_gesture_controller (workspaces_gesture_controller); @@ -254,6 +256,14 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone workspaces_gesture_controller.cancel_gesture (); } + public override float get_travel_distance (GestureAction for_action) { + switch (for_action) { + case MULTITASKING_VIEW: return primary_monitor_container.get_height () * 0.5f; + case SWITCH_WORKSPACE: return workspaces.get_first_child ().get_width (); + default: return 0; + } + } + public override void start_progress (GestureAction action) { if (!visible) { opened = true;