Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 23 additions & 9 deletions lib/Gestures/GestureController.vala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
* 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 with the same id.
* 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 with the same id handle one of
* the gestures.
*/
public class Gala.GestureController : Object {
/**
Expand All @@ -34,6 +38,7 @@ public class Gala.GestureController : Object {

public GestureAction action { get; construct; }
public WindowManager wm { get; construct; }
public string? id { get; construct; }

private unowned RootTarget? _target;
public RootTarget target {
Expand Down Expand Up @@ -77,7 +82,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 +96,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, string? id = null) {
Object (action: action, wm: wm, id: id);
}

/**
Expand All @@ -107,12 +113,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, id);
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
155 changes: 155 additions & 0 deletions lib/Gestures/TouchpadBackend.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* 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 string? id { 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, string? id) {
Object (actor: actor, id: id);
}

~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 && id != null) {
foreach (var instance in instances) {
if (instance != this && instance.id == id) {
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
12 changes: 7 additions & 5 deletions src/Widgets/MultitaskingView/MultitaskingView.vala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableComponent {
public const int ANIMATION_DURATION = 250;

private const string GESTURE_CONTROLLER_ID = "multitaskingview";

private GestureController workspaces_gesture_controller;
private GestureController multitasking_gesture_controller;

Expand Down Expand Up @@ -58,18 +60,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, GESTURE_CONTROLLER_ID);
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, GESTURE_CONTROLLER_ID) {
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 +254,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