diff --git a/src/Gestures/GestureBackend.vala b/src/Gestures/GestureBackend.vala new file mode 100644 index 000000000..1b24cac81 --- /dev/null +++ b/src/Gestures/GestureBackend.vala @@ -0,0 +1,23 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public 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 virtual void prepare_gesture_handling () { } + + /** + * The gesture should be cancelled. The implementation should stop emitting + * signals and reset any internal state. In particular it should not emit on_end. + * The implementation has to make sure that any further events from the same gesture will + * will be ignored. Once the gesture ends a new gesture should be treated as usual. + */ + public virtual void cancel_gesture () { } +} diff --git a/src/Gestures/GesturePropertyTransition.vala b/src/Gestures/GesturePropertyTransition.vala deleted file mode 100644 index cea6f536b..000000000 --- a/src/Gestures/GesturePropertyTransition.vala +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright 2024 elementary, Inc. (https://elementary.io) - * SPDX-License-Identifier: GPL-3.0-or-later - * - * Authored by: Leonhard Kargl - */ - -/** - * A class that will animate a property of a {@link Clutter.Actor} one to one with a gesture or - * with easing without a gesture. Respects the enable animation setting. - */ -public class Gala.GesturePropertyTransition : Object { - public delegate void DoneCallback (); - - /** - * The actor whose property will be animated. - */ - public Clutter.Actor actor { get; construct; } - - public GestureTracker gesture_tracker { get; construct; } - - /** - * The property that will be animated. To be properly animated it has to be marked as - * animatable in the Clutter documentation and should be numeric. - */ - public string property { get; construct; } - - /** - * The starting value of the animation or null to use the current value. The value - * has to be of the same type as the property. - */ - public Value? from_value { get; construct set; } - - /** - * The value to animate to. It has to be of the same type as the property. - */ - public Value to_value { get; construct set; } - - /** - * The lower max overshoot. The gesture percentage by which #this animates the property is bounded - * by this property on the lower end. If it is in the form X.YY with Y not 0 the animation will be linear - * until X and then take another 100% to animate until X.YY (instead of YY%). - * Default is 0. - */ - public double overshoot_lower_clamp { get; set; default = 0; } - /** - * Same as {@link overshoot_lower_clamp} but for the upper limit. - * If this is less than 1 and the transition is started without a gesture it will animate to - * the {@link to_value} by this percent and then back to the {@link from_value}. - * Default is 1. - */ - public double overshoot_upper_clamp { get; set; default = 1; } - - /** - * This is the from value that's actually used when calculating the animation movement. - * If {@link from_value} isn't null this will be the same, otherwise it will be set to the current - * value of the target property, when calling {@link start}. - */ - private Value actual_from_value; - private float from_value_float; // Only valid in the time between start () and finish () - private float to_value_float; // Only valid in the time between start () and finish () - - private DoneCallback? done_callback; - - public GesturePropertyTransition ( - Clutter.Actor actor, - GestureTracker gesture_tracker, - string property, - Value? from_value, - Value to_value - ) { - Object ( - actor: actor, - gesture_tracker: gesture_tracker, - property: property, - from_value: from_value, - to_value: to_value - ); - } - - /** - * Starts animating the property from {@link from_value} to {@link to_value}. If with_gesture is true - * it will connect to the gesture trackers signals and animate according to the input finishing with an easing - * to the final position. If with_gesture is false it will just ease to the {@link to_value}. - * #this will keep itself alive until the animation finishes so it is safe to immediatly unref it after creation and calling start. - * - * @param done_callback a callback for when the transition finishes. This shouldn't be used for setting state, instead state should - * be set immediately on {@link GestureTracker.OnEnd} not only once the animation ends to allow for interrupting the animation by starting a new gesture. - * done_callback will only be called if the animation finishes, not if it is interrupted e.g. by starting a new animation for the same property, - * destroying the actor or removing the transition. - * - * @return If a transition is currently in progress for the actor and the property the percentage how far the current value - * is towards the to_value given the final value of the ongoing transition is returned. This is usally the case if a gesture ended but was - * started again before the animation finished so this should be used to set {@link GestureTracker.initial_percentage}. If no transition - * is in progress 0 is returned. - */ - public double start (bool with_gesture, owned DoneCallback? done_callback = null) { - ref (); - - this.done_callback = (owned) done_callback; - - Value current_value = {}; - actor.get_property (property, ref current_value); - - Value initial_value; - - unowned var old_transition = actor.get_transition (property); - if (old_transition != null) { - initial_value = old_transition.interval.final; - } else { - initial_value = current_value; - } - - actual_from_value = from_value ?? initial_value; - - if (actual_from_value.type () != current_value.type ()) { - warning ("from_value of type %s is not of the same type as the property %s which is %s. Can't animate.", from_value.type_name (), property, current_value.type_name ()); - finish (); - return 0; - } - - if (current_value.type () != to_value.type ()) { - warning ("to_value of type %s is not of the same type as the property %s which is %s. Can't animate.", to_value.type_name (), property, current_value.type_name ()); - finish (); - return 0; - } - - // Pre calculate some things, so we don't have to do it on every update - from_value_float = value_to_float (actual_from_value); - to_value_float = value_to_float (to_value); - - var current_value_double = (double) value_to_float (current_value); - var initial_value_double = (double) value_to_float (initial_value); - - var initial_percentage = ((to_value_float - initial_value_double) - (to_value_float - current_value_double)) / (to_value_float - initial_value_double); - - GestureTracker.OnBegin on_animation_begin = (percentage) => { - var animation_value = GestureTracker.animation_value (from_value_float, to_value_float, percentage, false); - actor.set_property (property, value_from_float (animation_value)); - }; - - GestureTracker.OnUpdate on_animation_update = (percentage) => { - var lower_clamp_int = (int) overshoot_lower_clamp; - var upper_clamp_int = (int) overshoot_upper_clamp; - - double stretched_percentage = 0; - if (percentage < lower_clamp_int) { - stretched_percentage = (percentage - lower_clamp_int) * - (overshoot_lower_clamp - lower_clamp_int); - } else if (percentage > upper_clamp_int) { - stretched_percentage = (percentage - upper_clamp_int) * (overshoot_upper_clamp - upper_clamp_int); - } - - percentage = percentage.clamp (lower_clamp_int, upper_clamp_int); - - var animation_value = GestureTracker.animation_value (from_value_float, to_value_float, percentage, false); - - if (stretched_percentage != 0) { - animation_value += (float) stretched_percentage * (to_value_float - from_value_float); - } - - actor.set_property (property, value_from_float (animation_value)); - }; - - GestureTracker.OnEnd on_animation_end = (percentage, completions, calculated_duration) => { - completions = completions.clamp ((int) overshoot_lower_clamp, (int) overshoot_upper_clamp); - var target_value = from_value_float + completions * (to_value_float - from_value_float); - - actor.save_easing_state (); - actor.set_easing_mode (EASE_OUT_QUAD); - actor.set_easing_duration (Utils.get_animation_duration (calculated_duration)); - actor.set_property (property, value_from_float (target_value)); - actor.restore_easing_state (); - - unowned var transition = actor.get_transition (property); - if (transition == null) { - finish (); - } else { - transition.stopped.connect (finish); - } - }; - - if (with_gesture && Meta.Prefs.get_gnome_animations ()) { - gesture_tracker.connect_handlers (on_animation_begin, on_animation_update, on_animation_end); - } else { - on_animation_begin (0); - if (overshoot_upper_clamp < 1) { - actor.save_easing_state (); - actor.set_easing_mode (EASE_OUT_QUAD); - actor.set_easing_duration (Utils.get_animation_duration (gesture_tracker.min_animation_duration)); - actor.set_property (property, value_from_float ((float) overshoot_upper_clamp * (to_value_float - from_value_float) + from_value_float)); - actor.restore_easing_state (); - - unowned var transition = actor.get_transition (property); - if (transition == null) { - on_animation_end (1, 1, gesture_tracker.min_animation_duration); - } else { - transition.stopped.connect (() => on_animation_end (1, 1, gesture_tracker.min_animation_duration)); - } - } else { - on_animation_end (1, 1, gesture_tracker.min_animation_duration); - } - } - - return initial_percentage; - } - - private void finish (bool callback = true) { - if (done_callback != null && callback) { - done_callback (); - } - - unref (); - } - - private float value_to_float (Value val) { - Value float_val = Value (typeof (float)); - if (val.transform (ref float_val)) { - return float_val.get_float (); - } - - critical ("Non numeric property specified"); - return 0; - } - - private Value value_from_float (float f) { - var float_val = Value (typeof (float)); - float_val.set_float (f); - - var val = Value (actual_from_value.type ()); - - if (!float_val.transform (ref val)) { - warning ("Failed to transform float to give type"); - } - - return val; - } -} diff --git a/src/Gestures/GestureTracker.vala b/src/Gestures/GestureTracker.vala deleted file mode 100644 index 7bb948170..000000000 --- a/src/Gestures/GestureTracker.vala +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright 2021 elementary, Inc (https://elementary.io) - * 2021 José Expósito - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -public 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 virtual void prepare_gesture_handling () { } - - /** - * The gesture should be cancelled. The implementation should stop emitting - * signals and reset any internal state. In particular it should not emit on_end. - * The implementation has to make sure that any further events from the same gesture will - * will be ignored. Once the gesture ends a new gesture should be treated as usual. - */ - public virtual void cancel_gesture () { } -} - -/** - * Allow to use multi-touch gestures from different sources (backends). - * Usage: - * - * * Create a new instance of the class - * * Use the enable_* methods to enable different backends\ - * * Connect the on_gesture_detected to your code - * * When on_gesture_detected is emitted, if you want to handle the gesture, call connect_handlers to start receiving events - * * on_begin will be emitted once right after on_gesture_detected - * * on_update will be emitted 0 or more times - * * on_end will be emitted once when the gesture end - * * When on_end is emitted, the handler connected with connect_handlers will be automatically disconnected and you will only receive on_gesture_detected signals - * * The enabled flag is usually disabled on_end and re-enabled once the end animation finish. In this way, new gestures are not received while animating - */ -public class Gala.GestureTracker : Object { - /** - * Percentage of the animation to be completed to apply the action. - */ - private const double SUCCESS_PERCENTAGE_THRESHOLD = 0.2; - - /** - * 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; - - /** - * When a gesture ends with less velocity that this constant, this velocity is used instead. - */ - private const double ANIMATION_BASE_VELOCITY = 0.002; - - /** - * Maximum velocity allowed on gesture update. - */ - private const double MAX_VELOCITY = 0.01; - - /** - * Multiplier used to match libhandy's animation duration. - */ - private const int DURATION_MULTIPLIER = 3; - - public GestureSettings settings { get; construct; } - public int min_animation_duration { get; construct; } - public int max_animation_duration { get; construct; } - - /** - * Property to control when event signals are emitted or not. - */ - public bool enabled { get; set; default = true; } - - public bool recognizing { get; private set; } - - /** - * Emitted when a new gesture is detected. - * This should only be used to determine whether the gesture should be handled. This shouldn't - * do any preparations instead those should be done in {@link on_gesture_handled}. This is because - * the backend might have to do some preparations itself before you are allowed to do some to avoid - * conflicts. - * @param gesture Information about the gesture. - * @return true if the gesture will be handled false otherwise. If false is returned the other - * signals may still be emitted but aren't guaranteed to be. - */ - public signal bool on_gesture_detected (Gesture gesture); - - /** - * Emitted if true was returned form {@link on_gesture_detected}. This should - * be used to do any preparations for gesture handling and to call {@link connect_handlers} to - * start receiving updates. - * @param gesture the same gesture as in {@link on_gesture_detected} - * @param timestamp the timestamp of the event that initiated the gesture or {@link Meta.CURRENT_TIME}. - * @return the initial percentage that should already be preapplied. This is useful - * if an animation was still ongoing when the gesture was started. - */ - public signal double on_gesture_handled (Gesture gesture, uint32 timestamp); - - /** - * Emitted right after on_gesture_detected with the initial gesture information. - * @param percentage Value between 0 and 1. - */ - public signal void on_begin (double percentage); - - /** - * Called every time the percentage changes. - * @param percentage Value between 0 and 1. - */ - public signal void on_update (double percentage); - - /** - * @param percentage Value between 0 and 1. - * @param completions The number of times a full cycle of the gesture was completed in this go. Can be - * negative if the gesture was started in one direction but ended in the other. This is used to update - * the UI to the according state. 0 for example means that the UI should go back to the same state - * it was in before the gesture started. - */ - public signal void on_end (double percentage, int completions, int calculated_duration); - - public delegate void OnBegin (double percentage); - public delegate void OnUpdate (double percentage); - public delegate void OnEnd (double percentage, int completions, int calculated_duration); - - /** - * Backend used if enable_touchpad is called. - */ - private ToucheggBackend touchpad_backend; - - /** - * Scroll backend used if enable_scroll is called. - */ - private ScrollBackend scroll_backend; - - private Gee.ArrayList handlers; - - private double applied_percentage; - private double previous_percentage; - private uint64 previous_time; - private double previous_delta; - private double velocity; - // Used to check whether to cancel. Necessary because on_end is often called - // with the same percentage as the last update so this is the one before the last update. - private double old_previous; - - construct { - settings = new GestureSettings (); - - handlers = new Gee.ArrayList (); - applied_percentage = 0; - previous_percentage = 0; - previous_time = 0; - previous_delta = 0; - velocity = 0; - old_previous = 0; - } - - public GestureTracker (int min_animation_duration, int max_animation_duration) { - Object (min_animation_duration: min_animation_duration, max_animation_duration: max_animation_duration); - } - - /** - * Allow to receive touchpad multi-touch gestures. - */ - 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); - } - - /** - * Allow to receive scroll gestures. - * @param actor Clutter actor that will receive the scroll events. - * @param orientation If we are interested in the horizontal or vertical axis. - */ - public void enable_scroll (Clutter.Actor actor, Clutter.Orientation orientation) { - scroll_backend = new ScrollBackend (actor, orientation, settings); - 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); - } - - public void connect_handlers (owned OnBegin? on_begin_handler, owned OnUpdate? on_update_handler, owned OnEnd? on_end_handler) { - if (on_begin_handler != null) { - ulong handler_id = on_begin.connect ((percentage) => on_begin_handler (percentage)); - handlers.add (handler_id); - } - - if (on_update_handler != null) { - ulong handler_id = on_update.connect ((percentage) => on_update_handler (percentage)); - handlers.add (handler_id); - } - - if (on_end_handler != null) { - ulong handler_id = on_end.connect ((percentage, cancel_action, duration) => on_end_handler (percentage, cancel_action, duration)); - handlers.add (handler_id); - } - } - - /** - * Connects a callback that will be called as soon as the gesture finishes. - * If with_gesture is false it will be called immediately, otherwise once {@link on_end} is emitted. - */ - public void add_end_callback (bool with_gesture, owned OnEnd callback) { - if (!with_gesture) { - callback (1, 1, min_animation_duration); - } else { - ulong handler_id = on_end.connect ((percentage, cancel_action, duration) => - callback (percentage, cancel_action, duration) - ); - handlers.add (handler_id); - } - } - - private void disconnect_all_handlers () { - foreach (var handler in handlers) { - disconnect (handler); - } - - handlers.clear (); - } - - /** - * Utility method to calculate the current animation value based on the percentage of the - * gesture performed. - * Animations are always linear, as they are 1:1 to the user's movement. - * @param initial_value Animation start value. - * @param target_value Animation end value. - * @param percentage Current animation percentage. - * @param rounded If the returned value should be rounded to match physical pixels. - * Default to false because some animations, like for example scaling an actor, use intermediate - * values not divisible by physical pixels. - * @return The linear animation value at the specified percentage. - */ - public static float animation_value (float initial_value, float target_value, double percentage, bool rounded = false) { - float value = initial_value; - - if (initial_value != target_value) { - value = ((target_value - initial_value) * (float) percentage) + initial_value; - } - - if (rounded) { - value = Math.roundf (value); - } - - return value; - } - - private bool gesture_detected (GestureBackend backend, Gesture gesture, uint32 timestamp) { - if (enabled && on_gesture_detected (gesture)) { - backend.prepare_gesture_handling (); - applied_percentage = on_gesture_handled (gesture, timestamp); - return true; - } - - return false; - } - - private void gesture_begin (double percentage, uint64 elapsed_time) { - if (enabled) { - on_begin (applied_percentage); - } - - recognizing = true; - previous_percentage = percentage; - previous_time = elapsed_time; - } - - private void gesture_update (double percentage, uint64 elapsed_time) { - 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; - } - } - - applied_percentage += calculate_applied_delta (percentage, updated_delta); - - if (enabled) { - on_update (applied_percentage); - } - - old_previous = previous_percentage; - previous_percentage = percentage; - previous_time = elapsed_time; - previous_delta = updated_delta; - } - - private void gesture_end (double percentage, uint64 elapsed_time) { - applied_percentage += calculate_applied_delta (percentage, previous_delta); - - int completions = (int) applied_percentage; - - var remaining_percentage = applied_percentage - completions; - - bool cancel_action = ((percentage - completions).abs () < SUCCESS_PERCENTAGE_THRESHOLD) && (velocity.abs () < SUCCESS_VELOCITY_THRESHOLD) - || ((percentage.abs () < old_previous.abs ()) && (velocity.abs () > SUCCESS_VELOCITY_THRESHOLD)); - - int calculated_duration = calculate_end_animation_duration (remaining_percentage, cancel_action); - - if (!cancel_action) { - completions += applied_percentage < 0 ? -1 : 1; - } - - if (enabled) { - on_end (applied_percentage, completions, calculated_duration); - } - - disconnect_all_handlers (); - recognizing = false; - applied_percentage = 0; - previous_percentage = 0; - previous_time = 0; - previous_delta = 0; - velocity = 0; - old_previous = 0; - } - - /** - * Calculate the delta between the new percentage and the previous one while taking into account - * the velocity delta which makes sure we don't go over the MAX_VELOCITY. The velocity delta we use - * for the calculation shouldn't be confused with this delta. - */ - private inline double calculate_applied_delta (double percentage, double percentage_delta) { - return (percentage - percentage_delta) - (previous_percentage - previous_delta); - } - - /** - * Calculates the end animation duration using the current gesture velocity. - */ - private int calculate_end_animation_duration (double end_percentage, bool cancel_action) { - double animation_velocity = (velocity > ANIMATION_BASE_VELOCITY) - ? velocity - : ANIMATION_BASE_VELOCITY; - - double pending_percentage = cancel_action ? end_percentage : 1 - end_percentage; - - int duration = ((int)(pending_percentage / animation_velocity).abs () * DURATION_MULTIPLIER) - .clamp (min_animation_duration, max_animation_duration); - return duration; - } - } diff --git a/src/meson.build b/src/meson.build index 0a8b958ad..4a31ece55 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,11 +37,10 @@ gala_bin_sources = files( 'Dialogs/InhibitShortcutsDialog.vala', 'Gestures/ActorTarget.vala', 'Gestures/Gesture.vala', + 'Gestures/GestureBackend.vala', 'Gestures/GestureController.vala', - 'Gestures/GesturePropertyTransition.vala', 'Gestures/GestureSettings.vala', 'Gestures/GestureTarget.vala', - 'Gestures/GestureTracker.vala', 'Gestures/PropertyTarget.vala', 'Gestures/ScrollBackend.vala', 'Gestures/SpringTimeline.vala',