Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce PrimaryMonitorClone #2266

Closed
wants to merge 5 commits into from
Closed
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
92 changes: 92 additions & 0 deletions src/Gestures/ActorTarget.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2025 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

/**
* An {@link GestureTarget} implementation that derives from a {@link Clutter.Actor}.
* It will propagate gesture events to all direct descendants that are also {@link ActorTarget}s.
* If a new child (or target via {@link add_target}) is added, its progress will be synced.
*/
public class Gala.ActorTarget : Clutter.Actor, GestureTarget {
public Clutter.Actor? actor {
get {
return this;
}
}

private HashTable<string, double?> current_progress;
private Gee.List<GestureTarget> targets;

construct {
current_progress = new HashTable<string, double?> (str_hash, str_equal);
targets = new Gee.ArrayList<GestureTarget> ();

child_added.connect (on_child_added);
}

private void sync_target (GestureTarget target) {
foreach (var id in current_progress.get_keys ()) {
target.propagate (UPDATE, id, current_progress[id]);
}
}

public void add_target (GestureTarget target) {
targets.add (target);
sync_target (target);
}

public void remove_target (GestureTarget target) {
targets.remove (target);
}

public void remove_all_targets () {
targets.clear ();
}

public double get_current_progress (string id) {
return current_progress[id] ?? 0;
}

public virtual void start_progress (string id) {}
public virtual void update_progress (string id, double progress) {}
public virtual void commit_progress (string id, double to) {}
public virtual void end_progress (string id) {}

public override void propagate (UpdateType update_type, string id, double progress) {
current_progress[id] = progress;

switch (update_type) {
case START:
start_progress (id);
break;
case UPDATE:
update_progress (id, progress);
break;
case COMMIT:
commit_progress (id, progress);
break;
case END:
end_progress (id);
break;
}

foreach (var target in targets) {
target.propagate (update_type, id, progress);
}

for (var child = get_first_child (); child != null; child = child.get_next_sibling ()) {
if (child is ActorTarget) {
child.propagate (update_type, id, progress);
}
}
}

private void on_child_added (Clutter.Actor child) {
if (child is ActorTarget) {
sync_target ((GestureTarget) child);
}
}
}
8 changes: 8 additions & 0 deletions src/Gestures/Gesture.vala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ namespace Gala {
OUT = 6,
}

public enum GestureAction {
NONE,
SWITCH_WORKSPACE,
MOVE_TO_WORKSPACE,
SWITCH_WINDOWS,
MULTITASKING_VIEW
}

public class Gesture {
public const float INVALID_COORD = float.MAX;

Expand Down
270 changes: 270 additions & 0 deletions src/Gestures/GestureController.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*
* Copyright 2025 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public interface Gala.GestureTarget : Object {
public enum UpdateType {
START,
UPDATE,
COMMIT,
END
}

/**
* The actor manipulated by the gesture. The associated frame clock
* will be used for animation timelines.
*/
public abstract Clutter.Actor? actor { get; }

public virtual void propagate (UpdateType update_type, string id, double progress) { }
}

/**
* The class responsible for handling gestures and updating the target. It has a persistent
* double progress that is either updated by a gesture that is configured with the given
* {@link GestureAction} from various backends (see the enable_* methods) or manually
* by calling {@link goto} or setting {@link progress} directly.
* You shouldn't connect a notify to the progress directly though, but rather use a
* {@link GestureTarget} implementation.
* The {@link progress} can be seen as representing the state that the UI the gesture affects
* is currently in (e.g. 0 for multitasking view closed, 1 for it opend, or 0 for first workspace,
* -1 for second, -2 for third, etc.). Therefore the progress often needs boundaries which can be
* set with {@link overshoot_lower_clamp} and {@link overshoot_upper_clamp}. If the values are integers
* 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.
*/
public class Gala.GestureController : Object {
/**
* 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.
*/
private const double SUCCESS_VELOCITY_THRESHOLD = 0.003;

/**
* Maximum velocity allowed on gesture update.
*/
private const double MAX_VELOCITY = 0.01;

public string id { get; construct; }
public GestureAction action { get; construct; }

private GestureTarget? _target;
public GestureTarget target {
get { return _target; }
set {
_target = value;
target.propagate (UPDATE, id, calculate_bounded_progress ());
}
}

public double distance { get; construct set; }
public double overshoot_lower_clamp { get; construct set; default = 0d; }
public double overshoot_upper_clamp { get; construct set; default = 1d; }

private double _progress = 0;
public double progress {
get { return _progress; }
set {
_progress = value;
target.propagate (UPDATE, id, calculate_bounded_progress ());
}
}

private bool _enabled = true;
public bool enabled {
get { return _enabled; }
set {
cancel_gesture ();
_enabled = value;
}
}

public bool recognizing { get; private set; }

private ToucheggBackend? touchpad_backend;
private ScrollBackend? scroll_backend;

private GestureBackend? recognizing_backend;
private double previous_percentage;
private uint64 previous_time;
private double previous_delta;
private double velocity;
private int direction_multiplier;

private Clutter.Timeline? timeline;

public GestureController (string id, GestureAction action, GestureTarget target) {
Object (id: id, action: action, target: target);
}

private double calculate_bounded_progress () {
var lower_clamp_int = (int) overshoot_lower_clamp;
var upper_clamp_int = (int) overshoot_upper_clamp;

double stretched_percentage = 0;
if (_progress < lower_clamp_int) {
stretched_percentage = (_progress - lower_clamp_int) * - (overshoot_lower_clamp - lower_clamp_int);
} else if (_progress > upper_clamp_int) {
stretched_percentage = (_progress - upper_clamp_int) * (overshoot_upper_clamp - upper_clamp_int);
}

var clamped = _progress.clamp (lower_clamp_int, upper_clamp_int);

return clamped + stretched_percentage;
}

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_scroll (Clutter.Actor actor, Clutter.Orientation orientation) {
scroll_backend = new ScrollBackend (actor, orientation, new GestureSettings ());
scroll_backend.on_gesture_detected.connect (gesture_detected);
scroll_backend.on_begin.connect (gesture_begin);
scroll_backend.on_update.connect (gesture_update);
scroll_backend.on_end.connect (gesture_end);
}

private void prepare () {
if (timeline != null) {
timeline.stop ();
timeline = null;
}

target.propagate (START, id, calculate_bounded_progress ());
}

private bool gesture_detected (GestureBackend backend, Gesture gesture, uint32 timestamp) {
recognizing = enabled && (GestureSettings.get_action (gesture) == action
|| backend == scroll_backend && GestureSettings.get_action (gesture) == NONE);

if (recognizing) {
if (gesture.direction == UP || gesture.direction == RIGHT) {
direction_multiplier = 1;
} else {
direction_multiplier = -1;
}

recognizing_backend = backend;
}

return recognizing;
}

private void gesture_begin (double percentage, uint64 elapsed_time) {
if (!recognizing) {
return;
}

prepare ();

previous_percentage = percentage;
previous_time = elapsed_time;
}

private void gesture_update (double percentage, uint64 elapsed_time) {
if (!recognizing) {
return;
}

var updated_delta = previous_delta;
if (elapsed_time != previous_time) {
double distance = percentage - previous_percentage;
double time = (double)(elapsed_time - previous_time);
velocity = (distance / time);

if (velocity > MAX_VELOCITY) {
velocity = MAX_VELOCITY;
var used_percentage = MAX_VELOCITY * time + previous_percentage;
updated_delta += percentage - used_percentage;
}
}

progress += calculate_applied_delta (percentage, updated_delta);

previous_percentage = percentage;
previous_time = elapsed_time;
previous_delta = updated_delta;
}

private void gesture_end (double percentage, uint64 elapsed_time) {
if (!recognizing) {
return;
}

progress += calculate_applied_delta (percentage, previous_delta);

int completions = (int) Math.round (progress);

if (velocity.abs () > SUCCESS_VELOCITY_THRESHOLD) {
completions += velocity > 0 ? direction_multiplier : -direction_multiplier;
}

var lower_clamp_int = (int) overshoot_lower_clamp;
var upper_clamp_int = (int) overshoot_upper_clamp;

completions = completions.clamp (lower_clamp_int, upper_clamp_int);

recognizing = false;

finish (velocity, (double) completions);

previous_percentage = 0;
previous_time = 0;
previous_delta = 0;
velocity = 0;
direction_multiplier = 0;
}

private inline double calculate_applied_delta (double percentage, double percentage_delta) {
return ((percentage - percentage_delta) - (previous_percentage - previous_delta)) * direction_multiplier;
}

private void finish (double velocity, double to) {
target.propagate (COMMIT, id, to);

if (progress == to) {
target.propagate (END, id, calculate_bounded_progress ());
return;
}

var spring = new SpringTimeline (target.actor, progress, to, velocity, 1, 0.5, 500);
spring.progress.connect ((value) => progress = value);
spring.stopped.connect (() => {
target.propagate (END, id, calculate_bounded_progress ());
timeline = null;
});

timeline = spring;
}

/**
* Animates to the given progress value.
* If the gesture is currently recognizing, it will do nothing.
* If that's not what you want, you should call {@link cancel_gesture} first.
* If you don't want animation but an immediate jump, you should set {@link progress} directly.
*/
public void goto (double to) {
if (progress == to || recognizing) {
return;
}

prepare ();
finish (0.005, to);
}

public void cancel_gesture () {
if (recognizing) {
recognizing_backend.cancel_gesture ();
gesture_end (previous_percentage, previous_time);
}
}
}
8 changes: 0 additions & 8 deletions src/Gestures/GestureSettings.vala
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@
* Utility class to access the gesture settings. Easily accessible through GestureTracker.settings.
*/
public class Gala.GestureSettings : Object {
public enum GestureAction {
NONE,
SWITCH_WORKSPACE,
MOVE_TO_WORKSPACE,
SWITCH_WINDOWS,
MULTITASKING_VIEW
}

private static GLib.Settings gala_settings;
private static GLib.Settings touchpad_settings;

Expand Down
Loading