Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/Gestures/GestureBackend.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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 () { }

Expand Down
37 changes: 28 additions & 9 deletions lib/Gestures/GestureController.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}

/**
Expand All @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions lib/Gestures/ToucheggBackend.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
156 changes: 156 additions & 0 deletions lib/Gestures/TouchpadBackend.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright 2025 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <[email protected]>
*/

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<TouchpadBackend> instances = new List<TouchpadBackend> ();

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;
}
}
}
3 changes: 2 additions & 1 deletion lib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 5 additions & 5 deletions src/Widgets/MultitaskingView/MultitaskingView.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 });

Expand Down
4 changes: 2 additions & 2 deletions src/Widgets/WindowSwitcher/WindowSwitcher.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 ());
Expand Down
2 changes: 1 addition & 1 deletion src/Zoom.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down