Skip to content

Commit ec5d806

Browse files
authored
Merge branch 'main' into lenemter/fix-dameon-windows-1
2 parents 38657d6 + fb10168 commit ec5d806

37 files changed

+549
-1195
lines changed

lib/DragDropAction.vala

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ namespace Gala {
5555
*/
5656
public signal void destination_crossed (Clutter.Actor destination, bool hovered);
5757

58+
/**
59+
* Emitted on the source when we are hovering over a destination.
60+
*
61+
* @param destination The destination actor that we are hovering over
62+
* @param x The x coordinate relative to the destination actor
63+
* @param y The y coordinate relative to the destination actor
64+
*/
65+
public signal void destination_motion (Clutter.Actor destination, float x, float y);
66+
5867
/**
5968
* The source has been clicked, but the movement was not larger than
6069
* the drag threshold. Useful if the source is also activatable.
@@ -341,7 +350,7 @@ namespace Gala {
341350
last_y = y;
342351

343352
var stage = actor.get_stage ();
344-
var actor = stage.get_actor_at_pos (Clutter.PickMode.REACTIVE, (int) x, (int) y);
353+
var actor = stage.get_actor_at_pos (NONE, (int) x, (int) y);
345354
DragDropAction action = null;
346355
// if we're allowed to bubble and this actor is not a destination, check its parents
347356
if (actor != null && (action = get_drag_drop_action (actor)) == null && allow_bubbling) {
@@ -352,8 +361,12 @@ namespace Gala {
352361
}
353362

354363
// didn't change, no need to do anything
355-
if (actor == hovered)
364+
if (actor == hovered) {
365+
if (hovered != null) {
366+
destination_motion (hovered, x - hovered.x, y - hovered.y);
367+
}
356368
return Clutter.EVENT_STOP;
369+
}
357370

358371
if (action == null) {
359372
// apparently we left ours if we had one before
@@ -436,12 +449,12 @@ namespace Gala {
436449
}
437450

438451
private void finish () {
452+
drag_end (hovered);
453+
439454
// make sure they reset the style or whatever they changed when hovered
440455
emit_crossed (hovered, false);
441456

442457
cleanup ();
443-
444-
drag_end (hovered);
445458
}
446459

447460
private void cleanup () {

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: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,17 @@
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 in the same group (except for the group NONE).
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 in the same group handle one of
25+
* the gestures.
2226
*/
2327
public class Gala.GestureController : Object {
28+
public enum Group {
29+
NONE,
30+
MULTITASKING_VIEW,
31+
}
32+
2433
/**
2534
* When a gesture ends with a velocity greater than this constant, the action is not cancelled,
2635
* even if the animation threshold has not been reached.
@@ -34,6 +43,7 @@ public class Gala.GestureController : Object {
3443

3544
public GestureAction action { get; construct; }
3645
public WindowManager wm { get; construct; }
46+
public Group group { get; construct; }
3747

3848
private unowned RootTarget? _target;
3949
public RootTarget target {
@@ -50,6 +60,7 @@ public class Gala.GestureController : Object {
5060
public double distance { get; construct set; }
5161
public double overshoot_lower_clamp { get; construct set; default = 0d; }
5262
public double overshoot_upper_clamp { get; construct set; default = 1d; }
63+
public bool follow_natural_scroll { get; set; default = false; }
5364

5465
/**
5566
* When disabled gesture progress will stay where the gesture ended and not snap to full integers values.
@@ -77,7 +88,8 @@ public class Gala.GestureController : Object {
7788

7889
public bool recognizing { get; private set; }
7990

80-
private ToucheggBackend? touchpad_backend;
91+
private ToucheggBackend? touchegg_backend;
92+
private TouchpadBackend? touchpad_backend;
8193
private ScrollBackend? scroll_backend;
8294

8395
private GestureBackend? recognizing_backend;
@@ -90,8 +102,8 @@ public class Gala.GestureController : Object {
90102

91103
private SpringTimeline? timeline;
92104

93-
public GestureController (GestureAction action, WindowManager wm) {
94-
Object (action: action, wm: wm);
105+
public GestureController (GestureAction action, WindowManager wm, Group group = NONE) {
106+
Object (action: action, wm: wm, group: group);
95107
}
96108

97109
/**
@@ -107,12 +119,20 @@ public class Gala.GestureController : Object {
107119
unref ();
108120
}
109121

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);
122+
public void enable_touchpad (Clutter.Actor actor) {
123+
if (Meta.Util.is_wayland_compositor ()) {
124+
touchpad_backend = new TouchpadBackend (actor, group);
125+
touchpad_backend.on_gesture_detected.connect (gesture_detected);
126+
touchpad_backend.on_begin.connect (gesture_begin);
127+
touchpad_backend.on_update.connect (gesture_update);
128+
touchpad_backend.on_end.connect (gesture_end);
129+
}
130+
131+
touchegg_backend = ToucheggBackend.get_default (); // Will automatically filter events on wayland
132+
touchegg_backend.on_gesture_detected.connect (gesture_detected);
133+
touchegg_backend.on_begin.connect (gesture_begin);
134+
touchegg_backend.on_update.connect (gesture_update);
135+
touchegg_backend.on_end.connect (gesture_end);
116136
}
117137

118138
public void enable_scroll (Clutter.Actor actor, Clutter.Orientation orientation) {
@@ -147,6 +167,12 @@ public class Gala.GestureController : Object {
147167
direction_multiplier = -1;
148168
}
149169

170+
if (follow_natural_scroll &&
171+
!GestureSettings.is_natural_scroll_enabled (gesture.performed_on_device_type)
172+
) {
173+
direction_multiplier *= -1;
174+
}
175+
150176
if (snap && !Meta.Prefs.get_gnome_animations ()) {
151177
recognizing = false;
152178
prepare ();

lib/Gestures/GestureSettings.vala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ private class Gala.GestureSettings : Object {
2828
touchpad_settings = new GLib.Settings ("org.gnome.desktop.peripherals.touchpad");
2929
}
3030

31-
public bool is_natural_scroll_enabled (Clutter.InputDeviceType device_type) {
31+
public static bool is_natural_scroll_enabled (Clutter.InputDeviceType device_type) {
3232
return (device_type == Clutter.InputDeviceType.TOUCHSCREEN_DEVICE)
3333
? true
3434
: touchpad_settings.get_boolean ("natural-scroll");

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

lib/WindowManager.vala

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,20 @@ namespace Gala {
139139
public abstract Meta.BackgroundGroup background_group { get; protected set; }
140140

141141
/**
142-
* Enters the modal mode, which means that all events are directed to the stage instead
143-
* of the windows. This is the only way to receive keyboard events besides shortcut listeners.
142+
* Enters the modal mode, which will block keybindings and gestures. See {@link ModalProxy} for
143+
* how to allow certain gestures and keybindings.
144+
* If {@link grab} is true all events will be redirected to the given {@link Clutter.Actor}.
145+
* If {@link grab} is false other actors and shell windows may still receive events.
146+
* Normal windows will never receive keyboard focus though they will still receive pointer events
147+
* if {@link grab} is false and their {@link Meta.WindowActor} is visible.
148+
*
149+
* @param actor The actor to grab events for
150+
* @param grab Whether to grab all events onto the actor
144151
*
145152
* @return a {@link ModalProxy} which is needed to end the modal mode again and provides some
146153
* some basic control on the behavior of the window manager while it is in modal mode.
147154
*/
148-
public abstract ModalProxy push_modal (Clutter.Actor actor);
155+
public abstract ModalProxy push_modal (Clutter.Actor actor, bool grab);
149156

150157
/**
151158
* May exit the modal mode again, unless another component has called {@link push_modal}

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(

plugins/pip/SelectionArea.vala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public class Gala.Plugins.PIP.SelectionArea : CanvasActor {
126126
wm.get_display ().set_cursor (Meta.Cursor.CROSSHAIR);
127127
grab_key_focus ();
128128

129-
modal_proxy = wm.push_modal (this);
129+
modal_proxy = wm.push_modal (this, true);
130130
}
131131

132132
private void get_selection_rectangle (out int x, out int y, out int width, out int height) {

0 commit comments

Comments
 (0)