diff --git a/lib/Gestures/GestureBackend.vala b/lib/Gestures/GestureBackend.vala index 0b3192837..b7b0e1261 100644 --- a/lib/Gestures/GestureBackend.vala +++ b/lib/Gestures/GestureBackend.vala @@ -7,9 +7,9 @@ private interface Gala.GestureBackend : Object { public signal bool on_gesture_detected (Gesture gesture, uint32 timestamp); - public signal void on_begin (double delta, uint64 time); - public signal void on_update (double delta, uint64 time); - public signal void on_end (double delta, uint64 time); + public signal void on_begin (double percentage, uint64 time); + public signal void on_update (double percentage, uint64 time); + public signal void on_end (double percentage, uint64 time); public virtual void prepare_gesture_handling () { } diff --git a/lib/Gestures/GestureController.vala b/lib/Gestures/GestureController.vala index 32ebc2f34..625bf0e9c 100644 --- a/lib/Gestures/GestureController.vala +++ b/lib/Gestures/GestureController.vala @@ -19,8 +19,17 @@ * it will be a hard boundary, if they are fractional it will slow the gesture progress when over the * limit simulating a kind of spring that pushes against it. * Note that the progress snaps to full integer values after a gesture ends. + * Events are always shared between all GestureControllers in the same group (except for the group NONE). + * This means that two gestures that can be done in one motion (e.g. horizontal and vertical swipe) + * can be done simultaneously if each of two GestureControllers in the same group handle one of + * the gestures. */ public class Gala.GestureController : Object { + public enum Group { + NONE, + MULTITASKING_VIEW, + } + /** * When a gesture ends with a velocity greater than this constant, the action is not cancelled, * even if the animation threshold has not been reached. @@ -34,6 +43,7 @@ public class Gala.GestureController : Object { public GestureAction action { get; construct; } public WindowManager wm { get; construct; } + public Group group { get; construct; } private unowned RootTarget? _target; public RootTarget target { @@ -77,7 +87,8 @@ public class Gala.GestureController : Object { public bool recognizing { get; private set; } - private ToucheggBackend? touchpad_backend; + private ToucheggBackend? touchegg_backend; + private TouchpadBackend? touchpad_backend; private ScrollBackend? scroll_backend; private GestureBackend? recognizing_backend; @@ -90,8 +101,8 @@ public class Gala.GestureController : Object { private SpringTimeline? timeline; - public GestureController (GestureAction action, WindowManager wm) { - Object (action: action, wm: wm); + public GestureController (GestureAction action, WindowManager wm, Group group = NONE) { + Object (action: action, wm: wm, group: group); } /** @@ -107,12 +118,20 @@ public class Gala.GestureController : Object { unref (); } - public void enable_touchpad () { - touchpad_backend = ToucheggBackend.get_default (); - touchpad_backend.on_gesture_detected.connect (gesture_detected); - touchpad_backend.on_begin.connect (gesture_begin); - touchpad_backend.on_update.connect (gesture_update); - touchpad_backend.on_end.connect (gesture_end); + public void enable_touchpad (Clutter.Actor actor) { + if (Meta.Util.is_wayland_compositor ()) { + touchpad_backend = new TouchpadBackend (actor, group); + touchpad_backend.on_gesture_detected.connect (gesture_detected); + touchpad_backend.on_begin.connect (gesture_begin); + touchpad_backend.on_update.connect (gesture_update); + touchpad_backend.on_end.connect (gesture_end); + } + + touchegg_backend = ToucheggBackend.get_default (); // Will automatically filter events on wayland + touchegg_backend.on_gesture_detected.connect (gesture_detected); + touchegg_backend.on_begin.connect (gesture_begin); + touchegg_backend.on_update.connect (gesture_update); + touchegg_backend.on_end.connect (gesture_end); } public void enable_scroll (Clutter.Actor actor, Clutter.Orientation orientation) { diff --git a/lib/Gestures/ToucheggBackend.vala b/lib/Gestures/ToucheggBackend.vala index 46e2d4f1f..86cfefd11 100644 --- a/lib/Gestures/ToucheggBackend.vala +++ b/lib/Gestures/ToucheggBackend.vala @@ -192,6 +192,10 @@ 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) { + return; + } + var delta = percentage * DELTA_MULTIPLIER; switch (signal_name) { diff --git a/lib/Gestures/TouchpadBackend.vala b/lib/Gestures/TouchpadBackend.vala new file mode 100644 index 000000000..f7883c5c5 --- /dev/null +++ b/lib/Gestures/TouchpadBackend.vala @@ -0,0 +1,156 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +private class Gala.TouchpadBackend : Object, GestureBackend { + private const int TOUCHPAD_BASE_HEIGHT = 300; + private const int TOUCHPAD_BASE_WIDTH = 400; + private const int DRAG_THRESHOLD_DISTANCE = 16; + + private enum State { + NONE, + IGNORED, + IGNORED_HORIZONTAL, + IGNORED_VERTICAL, + ONGOING + } + + public Clutter.Actor actor { get; construct; } + public GestureController.Group group { get; construct; } + + private static List instances = new List (); + + private State state = NONE; + private GestureDirection direction = UNKNOWN; + private double distance_x = 0; + private double distance_y = 0; + private double distance = 0; + + public TouchpadBackend (Clutter.Actor actor, GestureController.Group group) { + Object (actor: actor, group: group); + } + + ~TouchpadBackend () { + instances.remove (this); + } + + construct { + actor.captured_event.connect (on_captured_event); + + instances.append (this); + } + + public override void cancel_gesture () { + state = IGNORED; + } + + private bool on_captured_event (Clutter.Event event) { + return handle_event (event, true); + } + + private bool handle_event (Clutter.Event event, bool main_handler) { + if (event.get_type () != TOUCHPAD_SWIPE) { + return Clutter.EVENT_PROPAGATE; + } + + if (state == IGNORED) { + if (event.get_gesture_phase () == END || event.get_gesture_phase () == CANCEL) { + reset (); + } + + return Clutter.EVENT_PROPAGATE; + } + + double delta_x, delta_y; + event.get_gesture_motion_delta_unaccelerated (out delta_x, out delta_y); + + if (state != ONGOING) { + distance_x += delta_x; + distance_y += delta_y; + + Gesture? gesture = null; + State state_if_ignored = NONE; + + var threshold = main_handler ? DRAG_THRESHOLD_DISTANCE : DRAG_THRESHOLD_DISTANCE * 4; + + if (state != IGNORED_HORIZONTAL && distance_x.abs () >= threshold) { + gesture = new Gesture (); + gesture.direction = direction = distance_x > 0 ? GestureDirection.RIGHT : GestureDirection.LEFT; + state_if_ignored = IGNORED_HORIZONTAL; + } else if (state != IGNORED_VERTICAL && distance_y.abs () >= threshold) { + gesture = new Gesture (); + gesture.direction = direction = distance_y > 0 ? GestureDirection.DOWN : GestureDirection.UP; + state_if_ignored = IGNORED_VERTICAL; + } else { + return Clutter.EVENT_PROPAGATE; + } + + gesture.type = event.get_type (); + gesture.fingers = (int) event.get_touchpad_gesture_finger_count (); + gesture.performed_on_device_type = event.get_device ().get_device_type (); + + if (!on_gesture_detected (gesture, event.get_time ())) { + if (state == NONE) { + state = state_if_ignored; + } else { // Both directions were ignored, so stop trying + state = IGNORED; + } + return Clutter.EVENT_PROPAGATE; + } + + state = ONGOING; + on_begin (0, event.get_time ()); + } else if (main_handler && group != NONE) { + foreach (var instance in instances) { + if (instance != this && instance.group == group) { + instance.handle_event (event, false); + } + } + } + + distance += get_value_for_direction (delta_x, delta_y); + + var percentage = get_percentage (distance); + + switch (event.get_gesture_phase ()) { + case BEGIN: + // We don't rely on the begin phase because we delay activation until the drag threshold is reached + break; + + case UPDATE: + on_update (percentage, event.get_time ()); + break; + + case END: + case CANCEL: + on_end (percentage, event.get_time ()); + reset (); + break; + } + + return Clutter.EVENT_STOP; + } + + private void reset () { + state = NONE; + distance = 0; + direction = UNKNOWN; + distance_x = 0; + distance_y = 0; + } + + private double get_percentage (double value) { + return value / (direction == LEFT || direction == RIGHT ? TOUCHPAD_BASE_WIDTH : TOUCHPAD_BASE_HEIGHT); + } + + private double get_value_for_direction (double delta_x, double delta_y) { + if (direction == LEFT || direction == RIGHT) { + return direction == LEFT ? -delta_x : delta_x; + } else { + return direction == UP ? -delta_y : delta_y; + } + } +} diff --git a/lib/meson.build b/lib/meson.build index 8fddfe9dd..5365300ea 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -30,7 +30,8 @@ gala_lib_sources = files( 'Gestures/RootTarget.vala', 'Gestures/ScrollBackend.vala', 'Gestures/SpringTimeline.vala', - 'Gestures/ToucheggBackend.vala' + 'Gestures/ToucheggBackend.vala', + 'Gestures/TouchpadBackend.vala' ) gala_resources = gnome.compile_resources( diff --git a/src/Widgets/MultitaskingView/MultitaskingView.vala b/src/Widgets/MultitaskingView/MultitaskingView.vala index b81c5155e..c2d7d2c2c 100644 --- a/src/Widgets/MultitaskingView/MultitaskingView.vala +++ b/src/Widgets/MultitaskingView/MultitaskingView.vala @@ -58,18 +58,18 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone opened = false; display = wm.get_display (); - multitasking_gesture_controller = new GestureController (MULTITASKING_VIEW, wm); - multitasking_gesture_controller.enable_touchpad (); + multitasking_gesture_controller = new GestureController (MULTITASKING_VIEW, wm, MULTITASKING_VIEW); + multitasking_gesture_controller.enable_touchpad (wm.stage); add_gesture_controller (multitasking_gesture_controller); add_target (ShellClientsManager.get_instance ()); // For hiding the panels workspaces = new WorkspaceRow (display); - workspaces_gesture_controller = new GestureController (SWITCH_WORKSPACE, wm) { + workspaces_gesture_controller = new GestureController (SWITCH_WORKSPACE, wm, MULTITASKING_VIEW) { overshoot_upper_clamp = 0.1 }; - workspaces_gesture_controller.enable_touchpad (); + workspaces_gesture_controller.enable_touchpad (wm.stage); workspaces_gesture_controller.enable_scroll (this, HORIZONTAL); add_gesture_controller (workspaces_gesture_controller); @@ -252,7 +252,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone show (); grab_key_focus (); - modal_proxy = wm.push_modal (this); + modal_proxy = wm.push_modal (get_stage ()); modal_proxy.set_keybinding_filter (keybinding_filter); modal_proxy.allow_actions ({ MULTITASKING_VIEW, SWITCH_WORKSPACE, ZOOM }); diff --git a/src/Widgets/WindowSwitcher/WindowSwitcher.vala b/src/Widgets/WindowSwitcher/WindowSwitcher.vala index 8fadfd9f4..25cb22871 100644 --- a/src/Widgets/WindowSwitcher/WindowSwitcher.vala +++ b/src/Widgets/WindowSwitcher/WindowSwitcher.vala @@ -65,7 +65,7 @@ public class Gala.WindowSwitcher : CanvasActor, GestureTarget, RootTarget { overshoot_lower_clamp = int.MIN, snap = false }; - gesture_controller.enable_touchpad (); + gesture_controller.enable_touchpad (wm.stage); gesture_controller.notify["recognizing"].connect (recognizing_changed); add_gesture_controller (gesture_controller); @@ -448,7 +448,7 @@ public class Gala.WindowSwitcher : CanvasActor, GestureTarget, RootTarget { } private void push_modal () { - modal_proxy = wm.push_modal (this); + modal_proxy = wm.push_modal (get_stage ()); modal_proxy.allow_actions ({ SWITCH_WINDOWS }); modal_proxy.set_keybinding_filter ((binding) => { var action = Meta.Prefs.get_keybinding_action (binding.get_name ()); diff --git a/src/Zoom.vala b/src/Zoom.vala index 70120f019..77f3643ca 100644 --- a/src/Zoom.vala +++ b/src/Zoom.vala @@ -37,7 +37,7 @@ public class Gala.Zoom : Object, GestureTarget, RootTarget { gesture_controller = new GestureController (ZOOM, wm) { snap = false }; - gesture_controller.enable_touchpad (); + gesture_controller.enable_touchpad (wm.stage); add_gesture_controller (gesture_controller); behavior_settings = new GLib.Settings ("io.elementary.desktop.wm.behavior");