diff --git a/src/AnimatableWidget.vala b/src/AnimatableWidget.vala new file mode 100644 index 00000000..ed1fa03e --- /dev/null +++ b/src/AnimatableWidget.vala @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) + */ + + /** + * Custom Granite.Bin subclass that implements common animation properties + * for ease of use with Adw.PropertyAnimationTarget. + */ +public class Dock.AnimatableWidget : Granite.Bin { + private double _translation_x = 0.0; + public double translation_x { + private get { return _translation_x; } + set { + _translation_x = value; + update_allocation (); + + } + } + + private double _translation_y = 0.0; + public double translation_y { + private get { return _translation_y; } + set { + _translation_y = value; + update_allocation (); + } + } + + private double _scale = 1.0; + public double scale { + private get { return _scale; } + set { + _scale = value; + update_allocation (); + } + } + + private void update_allocation () { + var width = get_width (); + var height = get_height (); + + var center_x = (width - (scale * width)) / 2.0; + var center_y = (height - (scale * height)) / 2.0; + + allocate ( + width, height, -1, + new Gsk.Transform () + .scale ((float) scale, (float) scale) + .translate (Graphene.Point ().init ( + (int) (center_x + translation_x), + (int) (center_y + translation_y) + )) + ); + } +} diff --git a/src/AppSystem/Background/BackgroundItem.vala b/src/AppSystem/Background/BackgroundItem.vala index d547b964..52b1cee6 100644 --- a/src/AppSystem/Background/BackgroundItem.vala +++ b/src/AppSystem/Background/BackgroundItem.vala @@ -86,8 +86,4 @@ public class Dock.BackgroundItem : BaseIconGroup { public void load () { monitor.load (); } - - public override void cleanup () { - // Do nothing here since we reuse this item - } } diff --git a/src/AppSystem/Launcher.vala b/src/AppSystem/Launcher.vala index 05c93ae3..eedf9aa7 100644 --- a/src/AppSystem/Launcher.vala +++ b/src/AppSystem/Launcher.vala @@ -27,9 +27,10 @@ public class Dock.Launcher : BaseItem { public App app { get; construct; } + private AnimatableWidget animatable_overlay; private Gtk.Box running_box; private Gtk.Image image; - private Gtk.Label badge; + private AnimatableWidget animatable_badge; private Gtk.Revealer progress_revealer; private Gtk.Revealer running_revealer; private Adw.TimedAnimation badge_fade; @@ -52,8 +53,6 @@ public class Dock.Launcher : BaseItem { } } - private Binding current_count_binding; - private int drag_offset_x = 0; private int drag_offset_y = 0; @@ -110,12 +109,16 @@ public class Dock.Launcher : BaseItem { image.gicon = new ThemedIcon ("application-default-icon"); } - badge = new Gtk.Label ("!"); + var badge = new Gtk.Label ("!"); badge.add_css_class (Granite.STYLE_CLASS_BADGE); + animatable_badge = new AnimatableWidget () { + child = badge + }; + var badge_container = new Granite.Bin () { can_target = false, - child = badge, + child = animatable_badge, halign = END, valign = START, overflow = VISIBLE @@ -126,10 +129,18 @@ public class Dock.Launcher : BaseItem { transition_type = CROSSFADE }; - overlay.child = image; + var overlay = new Gtk.Overlay () { + child = image + }; overlay.add_overlay (badge_container); overlay.add_overlay (progress_revealer); + animatable_overlay = new AnimatableWidget () { + child = overlay + }; + + main_bin.child = animatable_overlay; + var running_indicator = new Gtk.Image.from_icon_name ("pager-checked-symbolic"); running_indicator.add_css_class ("running-indicator"); @@ -158,22 +169,12 @@ public class Dock.Launcher : BaseItem { app.launched.connect (animate_launch); - var bounce_animation_target = new Adw.CallbackAnimationTarget ((val) => { - var height = overlay.get_height (); - var width = overlay.get_width (); - - overlay.allocate ( - width, height, -1, - new Gsk.Transform ().translate (Graphene.Point () { y = (int) val }) - ); - }); - bounce_down = new Adw.TimedAnimation ( - this, + animatable_overlay, 0, 0, 600, - bounce_animation_target + new Adw.PropertyAnimationTarget (animatable_overlay, "translation-y") ) { easing = EASE_OUT_BOUNCE }; @@ -184,58 +185,37 @@ public class Dock.Launcher : BaseItem { }); bounce_up = new Adw.TimedAnimation ( - this, + animatable_overlay, 0, 0, 200, - bounce_animation_target + new Adw.PropertyAnimationTarget (animatable_overlay, "translation-y") ) { easing = EASE_IN_OUT_QUAD }; bounce_up.done.connect (bounce_down.play); shake = new Adw.TimedAnimation ( - this, + animatable_overlay, 0, 0, 70, - new Adw.CallbackAnimationTarget ((val) => { - var height = overlay.get_height (); - var width = overlay.get_width (); - - overlay.allocate ( - width, height, -1, - new Gsk.Transform ().translate (Graphene.Point () { x = (int) val }) - ); - }) + new Adw.PropertyAnimationTarget (animatable_overlay, "translation-x") ) { easing = EASE_OUT_CIRC, reverse = true }; badge_scale = new Adw.TimedAnimation ( - badge, 0.25, 1, + animatable_badge, 0.25, 1, Granite.TRANSITION_DURATION_OPEN, - new Adw.CallbackAnimationTarget ((val) => { - var height = badge_container.get_height (); - var width = badge_container.get_width (); - - var x = (float) (width - (val * width)) / 2; - var y = (float) (height - (val * height)) / 2; - - badge.allocate ( - width, height, -1, - new Gsk.Transform ().scale ((float) val, (float) val).translate (Graphene.Point ().init (x, y)) - ); - }) + new Adw.PropertyAnimationTarget (animatable_badge, "scale") ); badge_fade = new Adw.TimedAnimation ( - badge, 0, 1, + animatable_badge, 0, 1, Granite.TRANSITION_DURATION_OPEN, - new Adw.CallbackAnimationTarget ((val) => { - badge.opacity = val; - }) + new Adw.PropertyAnimationTarget (animatable_badge, "opacity") ) { easing = EASE_IN_OUT_QUAD }; @@ -263,19 +243,7 @@ public class Dock.Launcher : BaseItem { app.notify["count-visible"].connect (update_badge_revealed); update_badge_revealed (); - current_count_binding = app.bind_property ("current_count", badge, "label", SYNC_CREATE, - (binding, srcval, ref targetval) => { - var src = (int64) srcval; - - if (src > 0) { - targetval.set_string ("%lld".printf (src)); - } else { - targetval.set_string ("!"); - } - - return true; - }, null - ); + app.bind_property ("current_count", badge, "label", SYNC_CREATE, app_badge_count_to_badge_string); if (notify_settings != null) { notify_settings.changed["do-not-disturb"].connect (update_badge_revealed); @@ -306,17 +274,7 @@ public class Dock.Launcher : BaseItem { ~Launcher () { popover_menu.unparent (); popover_menu.dispose (); - } - /** - * {@inheritDoc} - */ - public override void cleanup () { - base.cleanup (); - bounce_down = null; - bounce_up = null; - shake = null; - current_count_binding.unbind (); remove_dnd_cycle (); } @@ -349,7 +307,7 @@ public class Dock.Launcher : BaseItem { return; } - bounce_up.value_to = -0.5 * overlay.get_height (); + bounce_up.value_to = -0.5 * animatable_overlay.get_height (); bounce_down.value_from = bounce_up.value_to; bounce_up.play (); @@ -360,7 +318,7 @@ public class Dock.Launcher : BaseItem { return; } - shake.value_to = -0.1 * overlay.get_width (); + shake.value_to = -0.1 * animatable_overlay.get_width (); shake.play (); int repeat_count = 0; @@ -453,7 +411,7 @@ public class Dock.Launcher : BaseItem { badge_scale.skip (); // Avoid a stutter at the beginning - badge.opacity = 0; + animatable_badge.opacity = 0; if (app.count_visible && (notify_settings == null || !notify_settings.get_boolean ("do-not-disturb"))) { badge_fade.duration = Granite.TRANSITION_DURATION_OPEN; @@ -497,4 +455,10 @@ public class Dock.Launcher : BaseItem { multiple_windows_open = app.windows.length > 1; } } + + private static bool app_badge_count_to_badge_string (Binding binding, Value from_value, ref Value to_value) { + var src = from_value.get_int64 (); + to_value.set_string (src > 0 ? "%lld".printf (src) : "!"); + return true; + } } diff --git a/src/BaseItem.vala b/src/BaseItem.vala index 95437092..64804034 100644 --- a/src/BaseItem.vala +++ b/src/BaseItem.vala @@ -31,9 +31,17 @@ public class Dock.BaseItem : Gtk.Box { public Group group { get; construct; } public int icon_size { get; set; } - public double current_pos { get; set; } public new string tooltip_text { get; set; } + private double _current_pos = 0.0; + public double current_pos { + get { return _current_pos; } + set { + _current_pos = value; + ((Gtk.Fixed) parent).move (this, value, 0); + } + } + private bool _moving; public bool moving { get { return _moving; } @@ -48,7 +56,7 @@ public class Dock.BaseItem : Gtk.Box { bin.height_request = -1; } - overlay.visible = !value; + main_bin.visible = !value; } } @@ -70,10 +78,10 @@ public class Dock.BaseItem : Gtk.Box { * It's needed because top margin messes with dnd offsets and gsk transform. */ protected Gtk.Box actionable_box; - protected Gtk.Overlay overlay; + protected Granite.Bin main_bin; protected Gtk.GestureClick gesture_click; - protected Granite.Bin bin { get; private set; } + protected AnimatableWidget bin { get; private set; } private Adw.TimedAnimation fade; private Adw.TimedAnimation reveal; @@ -92,11 +100,11 @@ public class Dock.BaseItem : Gtk.Box { construct { orientation = VERTICAL; - overlay = new Gtk.Overlay (); + main_bin = new Granite.Bin (); - // We need the bin because we need the animation to run even if the overlay is not visible - bin = new Granite.Bin () { - child = overlay + // We need the bin because we need the animation to run even if the child is not visible + bin = new AnimatableWidget () { + child = main_bin }; actionable_box = new Gtk.Box (VERTICAL, 0); @@ -134,9 +142,7 @@ public class Dock.BaseItem : Gtk.Box { fade = new Adw.TimedAnimation ( this, 0, 1, Granite.TRANSITION_DURATION_OPEN, - new Adw.CallbackAnimationTarget ((val) => { - opacity = val; - }) + new Adw.PropertyAnimationTarget (this, "opacity") ) { easing = EASE_IN_OUT_QUAD }; @@ -144,26 +150,17 @@ public class Dock.BaseItem : Gtk.Box { reveal = new Adw.TimedAnimation ( bin, icon_size, 0, Granite.TRANSITION_DURATION_OPEN, - new Adw.CallbackAnimationTarget ((val) => { - bin.allocate (icon_size, icon_size, -1, - new Gsk.Transform ().translate (Graphene.Point () { y = (float) val } - )); - }) + new Adw.PropertyAnimationTarget (bin, "translation-y") ); reveal.done.connect (set_revealed_finish); - var animation_target = new Adw.CallbackAnimationTarget ((val) => { - ((Gtk.Fixed) parent).move (this, val, 0); - current_pos = val; - }); - timed_animation = new Adw.TimedAnimation ( this, 0, 0, 200, - animation_target + new Adw.PropertyAnimationTarget (this, "current-pos") ) { easing = EASE_IN_OUT_QUAD }; @@ -262,15 +259,6 @@ public class Dock.BaseItem : Gtk.Box { timed_animation.play (); } - /** - * If the icon group isn't needed anymore call this otherwise it won't be freed. - */ - public virtual void cleanup () { - fade = null; - reveal = null; - timed_animation = null; - } - private Gdk.ContentProvider? on_drag_prepare (double x, double y) { drag_offset_x = (int) x; drag_offset_y = (int) y; @@ -281,7 +269,7 @@ public class Dock.BaseItem : Gtk.Box { } private void on_drag_begin (Gtk.DragSource drag_source, Gdk.Drag drag) { - var paintable = new Gtk.WidgetPaintable (overlay); + var paintable = new Gtk.WidgetPaintable (main_bin); drag_source.set_icon (paintable.get_current_image (), drag_offset_x, drag_offset_y); moving = true; diff --git a/src/ContainerItem.vala b/src/ContainerItem.vala index 4442ea74..b945003f 100644 --- a/src/ContainerItem.vala +++ b/src/ContainerItem.vala @@ -18,7 +18,7 @@ public abstract class Dock.ContainerItem : BaseItem { bind_property ("icon-size", container, "width-request", SYNC_CREATE); bind_property ("icon-size", container, "height-request", SYNC_CREATE); - overlay.child = container; + main_bin.child = container; notify["state"].connect (() => { if ((state != HIDDEN) && !moving) { diff --git a/src/meson.build b/src/meson.build index 51ab51dd..a6a6890c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,4 +1,5 @@ sources = [ + 'AnimatableWidget.vala', 'Application.vala', 'BaseIconGroup.vala', 'BaseItem.vala',