diff --git a/lib/PropertyAnimator.vala b/lib/PropertyAnimator.vala new file mode 100644 index 000000000..770e42e2b --- /dev/null +++ b/lib/PropertyAnimator.vala @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Gala.PropertyAnimator : Object { + public struct AnimatableProperty { + string property; + Type value_type; + Value? from_value; + Value target_value; + } + + public delegate void OnStopped (Clutter.Actor actor); + + private Clutter.Actor actor; + private OnStopped on_stopped; + + public PropertyAnimator ( + Clutter.Actor actor, + uint duration, + Clutter.AnimationMode animation_mode, + AnimatableProperty[] properties, + OnStopped on_stopped + ) { + this.actor = actor; + this.on_stopped = on_stopped; + + ref (); + + if (!Meta.Prefs.get_gnome_animations ()) { + call_on_stopped (); + return; + } + + var attached_to_stopped = false; + for (var i = 0; i < properties.length; i++) { + var property = properties[i]; + + if (actor.find_property (property.property) == null) { + warning ("PropertyAnimator: Can't find property '%s'", property.property); + continue; + } + + Value actor_current_property = {}; + actor.get_property (property.property, ref actor_current_property); + + var transition = new Clutter.PropertyTransition (property.property) { + progress_mode = animation_mode, + remove_on_complete = true, + duration = duration, + interval = new Clutter.Interval.with_values ( + property.value_type, + property.from_value ?? actor_current_property, + property.target_value + ) + }; + + if (!attached_to_stopped) { + transition.stopped.connect (call_on_stopped); + attached_to_stopped = true; + } + + actor.add_transition (property.property, transition); + } + } + + private void call_on_stopped () { + on_stopped (actor); + unref (); + } +} diff --git a/lib/meson.build b/lib/meson.build index 94885b458..d003ab9a3 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -19,6 +19,7 @@ gala_lib_sources = files( 'Drawing/Utilities.vala', 'Image.vala', 'Plugin.vala', + 'PropertyAnimator.vala', 'RoundedCornersEffect.vala', 'ShadowEffect.vala', 'Text.vala', diff --git a/src/WindowManager.vala b/src/WindowManager.vala index f8b2ec257..49561ffce 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -975,20 +975,24 @@ namespace Gala { unowned Meta.WindowActor window_actor = window.get_compositor_private () as Meta.WindowActor; window_group.set_child_below_sibling (tile_preview, window_actor); - var duration = Utils.get_animation_duration (AnimationDuration.SNAP / 2U); - var rect = window.get_frame_rect (); tile_preview.set_position (rect.x, rect.y); tile_preview.set_size (rect.width, rect.height); tile_preview.show (); - tile_preview.save_easing_state (); - tile_preview.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT_QUAD); - tile_preview.set_easing_duration (duration); - tile_preview.opacity = 255U; - tile_preview.set_position (tile_rect.x, tile_rect.y); - tile_preview.set_size (tile_rect.width, tile_rect.height); - tile_preview.restore_easing_state (); + new PropertyAnimator ( + tile_preview, + AnimationDuration.SNAP / 2, + Clutter.AnimationMode.EASE_IN_OUT_QUAD, + { + { "opacity", typeof (uint), null, 255u }, + { "x", typeof (float), null, (float) tile_rect.x }, + { "y", typeof (float), null, (float) tile_rect.y }, + { "width", typeof (float), null, (float) tile_rect.width }, + { "height", typeof (float), null, (float) tile_rect.height } + }, + () => {} + ); } public override void hide_tile_preview () { @@ -1082,8 +1086,7 @@ namespace Gala { } public override void minimize (Meta.WindowActor actor) { - if (!Meta.Prefs.get_gnome_animations () || - actor.get_meta_window ().window_type != Meta.WindowType.NORMAL) { + if (!Utils.get_window_is_normal (actor.meta_window)) { minimize_completed (actor); return; } @@ -1091,6 +1094,7 @@ namespace Gala { kill_window_effects (actor); minimizing.add (actor); + var scale_x = 0.0, scale_y = 0.0; Mtk.Rectangle icon = {}; if (actor.get_meta_window ().get_icon_geometry (out icon)) { // Fix icon position and size according to ui scaling factor. @@ -1105,37 +1109,26 @@ namespace Gala { (actor.y - icon.y) / (icon.height - actor.height) ); - actor.save_easing_state (); - actor.set_easing_mode (Clutter.AnimationMode.EASE_IN_EXPO); - actor.set_easing_duration (AnimationDuration.HIDE); - actor.set_scale (icon.width / actor.width, icon.height / actor.height); - actor.opacity = 0; - actor.restore_easing_state (); - - ulong minimize_handler_id = 0; - minimize_handler_id = actor.transitions_completed.connect (() => { - actor.disconnect (minimize_handler_id); - minimize_completed (actor); - minimizing.remove (actor); - }); + scale_x = (double) icon.width / actor.width; + scale_y = (double) icon.height / actor.height; } else { actor.set_pivot_point (0.5f, 1.0f); - - actor.save_easing_state (); - actor.set_easing_mode (Clutter.AnimationMode.EASE_IN_EXPO); - actor.set_easing_duration (AnimationDuration.HIDE); - actor.set_scale (0.0, 0.0); - actor.opacity = 0; - actor.restore_easing_state (); - - ulong minimize_handler_id = 0; - minimize_handler_id = actor.transitions_completed.connect (() => { - actor.disconnect (minimize_handler_id); - actor.set_pivot_point (0.0f, 0.0f); - minimize_completed (actor); - minimizing.remove (actor); - }); } + + new PropertyAnimator ( + actor, + AnimationDuration.HIDE, + Clutter.AnimationMode.EASE_IN_EXPO, + { + { "scale-x", typeof (double), null, scale_x }, + { "scale-y", typeof (double), null, scale_y }, + { "opacity", typeof (uint), null, 0u } + }, + (actor) => { + minimize_completed ((Meta.WindowActor) actor); + minimizing.remove ((Meta.WindowActor) actor); + } + ); } private void maximize (Meta.WindowActor actor, int ex, int ey, int ew, int eh) { @@ -1501,26 +1494,27 @@ namespace Gala { ui_group.add_child (latest_window_snapshot); - var scale_x = (float) ew / old_rect_size_change.width; - var scale_y = (float) eh / old_rect_size_change.height; - - latest_window_snapshot.save_easing_state (); - latest_window_snapshot.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT_QUAD); - latest_window_snapshot.set_easing_duration (duration); - latest_window_snapshot.set_position (ex, ey); - latest_window_snapshot.set_scale (scale_x, scale_y); - latest_window_snapshot.opacity = 0U; - latest_window_snapshot.restore_easing_state (); - - ulong unmaximize_old_handler_id = 0; - unmaximize_old_handler_id = latest_window_snapshot.transition_stopped.connect ((snapshot, name, is_finished) => { - snapshot.disconnect (unmaximize_old_handler_id); + var scale_x = (double) ew / old_rect_size_change.width; + var scale_y = (double) eh / old_rect_size_change.height; - unowned var parent = snapshot.get_parent (); - if (parent != null) { - parent.remove_child (snapshot); + new PropertyAnimator ( + latest_window_snapshot, + duration, + Clutter.AnimationMode.EASE_IN_OUT_QUAD, + { + { "x", typeof (float), null, (float) ex }, + { "y", typeof (float), null, (float) ey }, + { "scale-x", typeof (double), null, scale_x }, + { "scale-y", typeof (double), null, scale_y }, + { "opacity", typeof (uint), null, 0u } + }, + (actor) => { + unowned var parent = actor.get_parent (); + if (parent != null) { + parent.remove_child (actor); + } } - }); + ); latest_window_snapshot = null; @@ -1531,21 +1525,27 @@ namespace Gala { actor.set_pivot_point (0.0f, 0.0f); actor.set_position (ex - real_actor_offset_x, ey - real_actor_offset_y); - actor.set_translation (-ex + offset_x * (1.0f / scale_x - 1.0f) + old_rect_size_change.x, -ey + offset_y * (1.0f / scale_y - 1.0f) + old_rect_size_change.y, 0.0f); - actor.set_scale (1.0f / scale_x, 1.0f / scale_y); - - actor.save_easing_state (); - actor.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT_QUAD); - actor.set_easing_duration (duration); - actor.set_scale (1.0f, 1.0f); - actor.set_translation (0.0f, 0.0f, 0.0f); - actor.restore_easing_state (); - - ulong handler_id = 0UL; - handler_id = actor.transitions_completed.connect (() => { - actor.disconnect (handler_id); - unmaximizing.remove (actor); - }); + actor.set_translation ( + (float) (-ex + offset_x * (1.0f / scale_x - 1.0f) + old_rect_size_change.x), + (float) (-ey + offset_y * (1.0f / scale_y - 1.0f) + old_rect_size_change.y), + 0.0f + ); + actor.set_scale (1.0 / scale_x, 1.0 / scale_y); + + new PropertyAnimator ( + actor, + duration, + Clutter.AnimationMode.EASE_IN_OUT_QUAD, + { + { "scale-x", typeof (double), null, 1.0 }, + { "scale-y", typeof (double), null, 1.0 }, + { "translation-x", typeof (float), null, 0.0f }, + { "translation-y", typeof (float), null, 0.0f } + }, + (actor) => { + unmaximizing.remove ((Meta.WindowActor) actor); + } + ); } private void move_window_to_next_ws (Meta.Window window) { diff --git a/vapi/mutter-clutter.vapi b/vapi/mutter-clutter.vapi index b87669553..66a074ce3 100644 --- a/vapi/mutter-clutter.vapi +++ b/vapi/mutter-clutter.vapi @@ -7694,7 +7694,7 @@ namespace Clutter { } [CCode (cheader_filename = "clutter/clutter.h", type_cname = "ClutterAnimatableInterface", type_id = "clutter_animatable_get_type ()")] public interface Animatable : GLib.Object { - public abstract unowned GLib.ParamSpec find_property (string property_name); + public abstract unowned GLib.ParamSpec? find_property (string property_name); public abstract unowned Clutter.Actor get_actor (); public abstract void get_initial_state (string property_name, GLib.Value value); public abstract bool interpolate_value (string property_name, Clutter.Interval interval, double progress, out GLib.Value value);