From 06ea6c5cd2a18fc5da0da73115dbaae52e3cf284 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 11 Aug 2026 15:13:41 +0200 Subject: [PATCH 1/6] Use TransitionBuilder for maximizing --- src/WindowManager.vala | 236 ++++++++++++++--------------------------- 1 file changed, 81 insertions(+), 155 deletions(-) diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 06ad43bd9..bc70784d2 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -18,6 +18,18 @@ namespace Gala { public class WindowManagerGala : Meta.Plugin, WindowManager { + private class SizeChangeInfo { + public Meta.SizeChange change; + public Mtk.Rectangle old_rect; + public Clutter.Actor snapshot; + + public SizeChangeInfo (Meta.SizeChange change, Mtk.Rectangle old_rect, Clutter.Actor snapshot) { + this.change = change; + this.old_rect = old_rect; + this.snapshot = snapshot; + } + } + private const string OPEN_MULTITASKING_VIEW = "dbus-send --session --dest=org.pantheon.gala --print-reply /org/pantheon/gala org.pantheon.gala.PerformAction int32:1"; private const string OPEN_APPLICATIONS_MENU = "io.elementary.wingpanel --toggle-indicator=app-launcher"; @@ -122,9 +134,7 @@ namespace Gala { private Gee.HashSet mapping = new Gee.HashSet (); private Gee.HashSet destroying = new Gee.HashSet (); private Gee.HashSet unminimizing = new Gee.HashSet (); - private Meta.SizeChange? which_change = null; - private Mtk.Rectangle old_rect_size_change; - private Clutter.Actor? latest_window_snapshot; + private Gee.HashMap pending_size_change = new Gee.HashMap (); private GLib.Settings behavior_settings; @@ -1118,49 +1128,59 @@ namespace Gala { // must wait for size_changed to get updated frame_rect // as which_change is not passed to size_changed, save it as instance variable - public override void size_change (Meta.WindowActor actor, Meta.SizeChange which_change_local, Mtk.Rectangle old_frame_rect, Mtk.Rectangle old_buffer_rect) { - which_change = which_change_local; - old_rect_size_change = old_frame_rect; + public override void size_change (Meta.WindowActor actor, Meta.SizeChange which_change, Mtk.Rectangle old_frame_rect, Mtk.Rectangle old_buffer_rect) { + if (actor.meta_window.window_type != NORMAL || !Meta.Prefs.get_gnome_animations ()) { + size_change_completed (actor); + return; + } + + var snapshot = Utils.get_window_actor_snapshot (actor, old_frame_rect); - if (Meta.Prefs.get_gnome_animations ()) { - latest_window_snapshot = Utils.get_window_actor_snapshot (actor, old_frame_rect); + if (snapshot == null) { + size_change_completed (actor); + return; } + + var info = new SizeChangeInfo (which_change, old_frame_rect, snapshot); + pending_size_change[actor] = info; } // size_changed gets called after frame_rect has updated public override void size_changed (Meta.WindowActor actor) { - if (which_change == null) { + SizeChangeInfo info; + if (!pending_size_change.unset (actor, out info)) { return; } unowned var window = actor.get_meta_window (); var new_rect = window.get_frame_rect (); - switch (which_change) { + var old_rect = info.old_rect; + + switch (info.change) { case Meta.SizeChange.MAXIMIZE: case Meta.SizeChange.FULLSCREEN: // don't animate resizing of two tiled windows with mouse drag if (window.get_tile_match () != null && !window.maximized_horizontally) { - var old_end = old_rect_size_change.x + old_rect_size_change.width; + var old_end = old_rect.x + old_rect.width; var new_end = new_rect.x + new_rect.width; // a tiled window is just resized (and not moved) if its start_x or its end_x stays the same - if (old_rect_size_change.x == new_rect.x || old_end == new_end) { + if (old_rect.x == new_rect.x || old_end == new_end) { break; } } - maximize (actor, new_rect.x, new_rect.y, new_rect.width, new_rect.height); + maximize.begin (actor, old_rect, new_rect, info.snapshot); break; case Meta.SizeChange.UNMAXIMIZE: case Meta.SizeChange.UNFULLSCREEN: - unmaximize (actor, new_rect.x, new_rect.y, new_rect.width, new_rect.height); + unmaximize.begin (actor, old_rect, new_rect, info.snapshot); break; default: break; } - which_change = null; size_change_completed (actor); } @@ -1221,90 +1241,37 @@ namespace Gala { } } - private void maximize (Meta.WindowActor actor, int ex, int ey, int ew, int eh) { - unowned var window = actor.get_meta_window (); - + private async void maximize (Meta.WindowActor actor, Mtk.Rectangle old_rect, Mtk.Rectangle new_rect, Clutter.Actor snapshot) { kill_window_effects (actor); - if (!Meta.Prefs.get_gnome_animations () || - latest_window_snapshot == null || - window.window_type != Meta.WindowType.NORMAL) { - return; - } - - var duration = AnimationDuration.SNAP; - maximizing.add (actor); - latest_window_snapshot.set_position (old_rect_size_change.x, old_rect_size_change.y); - - ui_group.add_child (latest_window_snapshot); - - // FIMXE that's a hacky part. There is a short moment right after maximized_completed - // where the texture is screwed up and shows things it's not supposed to show, - // resulting in flashing. Waiting here transparently shortly fixes that issue. There - // appears to be no signal that would inform when that moment happens. - // We can't spend arbitrary amounts of time transparent since the overlay fades away, - // about a third has proven to be a solid time. So this fix will only apply for - // durations >= FLASH_PREVENT_TIMEOUT*3 - const int FLASH_PREVENT_TIMEOUT = 80; - var delay = 0; - if (FLASH_PREVENT_TIMEOUT <= duration / 3) { - actor.opacity = 0; - delay = FLASH_PREVENT_TIMEOUT; - Timeout.add (FLASH_PREVENT_TIMEOUT, () => { - actor.opacity = 255; - return false; - }); - } + snapshot.set_position (old_rect.x, old_rect.y); - var scale_x = (double) ew / old_rect_size_change.width; - var scale_y = (double) 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.restore_easing_state (); - - // the opacity animation is special, since we have to wait for the - // FLASH_PREVENT_TIMEOUT to be done before we can safely fade away - latest_window_snapshot.save_easing_state (); - latest_window_snapshot.set_easing_delay (delay); - latest_window_snapshot.set_easing_duration (duration - delay); - latest_window_snapshot.opacity = 0; - latest_window_snapshot.restore_easing_state (); - - ulong maximize_old_handler_id = 0; - maximize_old_handler_id = latest_window_snapshot.transition_stopped.connect ((snapshot, name, is_finished) => { - snapshot.disconnect (maximize_old_handler_id); - - actor.set_translation (0.0f, 0.0f, 0.0f); - - unowned var parent = snapshot.get_parent (); - if (parent != null) { - parent.remove_child (snapshot); - } - }); + ui_group.add_child (snapshot); + + var scale_x = (double) new_rect.width / old_rect.width; + var scale_y = (double) new_rect.height / old_rect.height; - latest_window_snapshot = null; + snapshot.save_easing_state (); + snapshot.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT_QUAD); + snapshot.set_easing_duration (AnimationDuration.SNAP); + snapshot.set_position (new_rect.x, new_rect.y); + snapshot.set_scale (scale_x, scale_y); + snapshot.opacity = 0; + snapshot.restore_easing_state (); actor.set_pivot_point (0.0f, 0.0f); - actor.set_translation (old_rect_size_change.x - ex, old_rect_size_change.y - ey, 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 (); + var actor_transition_builder = new TransitionBuilder (actor, AnimationDuration.SNAP, EASE_IN_OUT_QUAD); + actor_transition_builder.add_property_with_from ("scale-x", 1.0 / scale_x, 1.0); + actor_transition_builder.add_property_with_from ("scale-y", 1.0 / scale_y, 1.0); + actor_transition_builder.add_property_with_from ("translation-x", (float) (old_rect.x - new_rect.x), 0.0f); + actor_transition_builder.add_property_with_from ("translation-y", (float) (old_rect.y - new_rect.y), 0.0f); - ulong handler_id = 0UL; - handler_id = actor.transitions_completed.connect (() => { - actor.disconnect (handler_id); - maximizing.remove (actor); - }); + yield actor_transition_builder.run (); + + ui_group.remove_child (snapshot); + maximizing.remove (actor); } public override void unminimize (Meta.WindowActor actor) { @@ -1475,81 +1442,36 @@ namespace Gala { destroy_completed (actor); } - private void unmaximize (Meta.WindowActor actor, int ex, int ey, int ew, int eh) { - unowned var window = actor.get_meta_window (); - + private async void unmaximize (Meta.WindowActor actor, Mtk.Rectangle old_rect, Mtk.Rectangle new_rect, Clutter.Actor snapshot) { kill_window_effects (actor); - if (!Meta.Prefs.get_gnome_animations () || - latest_window_snapshot == null || - window.window_type != Meta.WindowType.NORMAL) { - return; - } - - var duration = AnimationDuration.SNAP; - - float offset_x, offset_y; - var unmaximized_window_geometry = WindowListener.get_default ().get_unmaximized_state_geometry (window); - - if (unmaximized_window_geometry != null) { - offset_x = unmaximized_window_geometry.outer.x - unmaximized_window_geometry.inner.x; - offset_y = unmaximized_window_geometry.outer.y - unmaximized_window_geometry.inner.y; - } else { - offset_x = 0; - offset_y = 0; - } - unmaximizing.add (actor); - latest_window_snapshot.set_position (old_rect_size_change.x, old_rect_size_change.y); - - 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; + snapshot.set_position (old_rect.x, old_rect.y); - 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); - - unowned var parent = snapshot.get_parent (); - if (parent != null) { - parent.remove_child (snapshot); - } - }); + ui_group.add_child (snapshot); - latest_window_snapshot = null; + var scale_x = (float) new_rect.width / old_rect.width; + var scale_y = (float) new_rect.height / old_rect.height; - var buffer_rect = window.get_buffer_rect (); - var frame_rect = window.get_frame_rect (); - var real_actor_offset_x = frame_rect.x - buffer_rect.x; - var real_actor_offset_y = frame_rect.y - buffer_rect.y; + snapshot.save_easing_state (); + snapshot.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT_QUAD); + snapshot.set_easing_duration (AnimationDuration.SNAP); + snapshot.set_position (new_rect.x, new_rect.y); + snapshot.set_scale (scale_x, scale_y); + snapshot.opacity = 0U; + snapshot.restore_easing_state (); - 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); + var actor_transition_builder = new TransitionBuilder (actor, AnimationDuration.SNAP, EASE_IN_OUT_QUAD); + actor_transition_builder.add_property_with_from ("scale-x", 1.0 / scale_x, 1.0); + actor_transition_builder.add_property_with_from ("scale-y", 1.0 / scale_y, 1.0); + actor_transition_builder.add_property_with_from ("translation-x", (float) (old_rect.x - new_rect.x), 0.0f); + actor_transition_builder.add_property_with_from ("translation-y", (float) (old_rect.y - new_rect.y), 0.0f); - 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 (); + yield actor_transition_builder.run (); - ulong handler_id = 0UL; - handler_id = actor.transitions_completed.connect (() => { - actor.disconnect (handler_id); - unmaximizing.remove (actor); - }); + ui_group.remove_child (snapshot); + unmaximizing.remove (actor); } // Cancel attached animation of an actor and reset it @@ -1573,6 +1495,10 @@ namespace Gala { } public override void kill_window_effects (Meta.WindowActor actor) { + if (pending_size_change.unset (actor)) { + size_change_completed (actor); + } + if (end_animation (ref unminimizing, actor)) unminimize_completed (actor); if (end_animation (ref minimizing, actor)) From ef0da82ade508c6eb17f6b94a66e8cf65680b977 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 11 Aug 2026 15:32:46 +0200 Subject: [PATCH 2/6] Merge maximizing and unmaximizing --- src/WindowManager.vala | 49 +++++++----------------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/src/WindowManager.vala b/src/WindowManager.vala index bc70784d2..207e31742 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -129,12 +129,11 @@ namespace Gala { private Gee.LinkedList modal_stack = new Gee.LinkedList (); private Gee.HashSet minimizing = new Gee.HashSet (); - private Gee.HashSet maximizing = new Gee.HashSet (); - private Gee.HashSet unmaximizing = new Gee.HashSet (); private Gee.HashSet mapping = new Gee.HashSet (); private Gee.HashSet destroying = new Gee.HashSet (); private Gee.HashSet unminimizing = new Gee.HashSet (); private Gee.HashMap pending_size_change = new Gee.HashMap (); + private Gee.HashSet changing_size = new Gee.HashSet (); private GLib.Settings behavior_settings; @@ -1171,11 +1170,11 @@ namespace Gala { } } - maximize.begin (actor, old_rect, new_rect, info.snapshot); + animate_size_change.begin (actor, old_rect, new_rect, info.snapshot); break; case Meta.SizeChange.UNMAXIMIZE: case Meta.SizeChange.UNFULLSCREEN: - unmaximize.begin (actor, old_rect, new_rect, info.snapshot); + animate_size_change.begin (actor, old_rect, new_rect, info.snapshot); break; default: break; @@ -1241,10 +1240,11 @@ namespace Gala { } } - private async void maximize (Meta.WindowActor actor, Mtk.Rectangle old_rect, Mtk.Rectangle new_rect, Clutter.Actor snapshot) { + private async void animate_size_change (Meta.WindowActor actor, Mtk.Rectangle old_rect, Mtk.Rectangle new_rect, Clutter.Actor snapshot) { kill_window_effects (actor); - maximizing.add (actor); + changing_size.add (actor); + snapshot.set_position (old_rect.x, old_rect.y); ui_group.add_child (snapshot); @@ -1271,7 +1271,7 @@ namespace Gala { yield actor_transition_builder.run (); ui_group.remove_child (snapshot); - maximizing.remove (actor); + changing_size.remove (actor); } public override void unminimize (Meta.WindowActor actor) { @@ -1442,38 +1442,6 @@ namespace Gala { destroy_completed (actor); } - private async void unmaximize (Meta.WindowActor actor, Mtk.Rectangle old_rect, Mtk.Rectangle new_rect, Clutter.Actor snapshot) { - kill_window_effects (actor); - - unmaximizing.add (actor); - - snapshot.set_position (old_rect.x, old_rect.y); - - ui_group.add_child (snapshot); - - var scale_x = (float) new_rect.width / old_rect.width; - var scale_y = (float) new_rect.height / old_rect.height; - - snapshot.save_easing_state (); - snapshot.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT_QUAD); - snapshot.set_easing_duration (AnimationDuration.SNAP); - snapshot.set_position (new_rect.x, new_rect.y); - snapshot.set_scale (scale_x, scale_y); - snapshot.opacity = 0U; - snapshot.restore_easing_state (); - - var actor_transition_builder = new TransitionBuilder (actor, AnimationDuration.SNAP, EASE_IN_OUT_QUAD); - actor_transition_builder.add_property_with_from ("scale-x", 1.0 / scale_x, 1.0); - actor_transition_builder.add_property_with_from ("scale-y", 1.0 / scale_y, 1.0); - actor_transition_builder.add_property_with_from ("translation-x", (float) (old_rect.x - new_rect.x), 0.0f); - actor_transition_builder.add_property_with_from ("translation-y", (float) (old_rect.y - new_rect.y), 0.0f); - - yield actor_transition_builder.run (); - - ui_group.remove_child (snapshot); - unmaximizing.remove (actor); - } - // Cancel attached animation of an actor and reset it private bool end_animation (ref Gee.HashSet list, Meta.WindowActor actor) { if (!list.contains (actor)) @@ -1506,8 +1474,7 @@ namespace Gala { end_animation (ref mapping, actor); end_animation (ref destroying, actor); - end_animation (ref unmaximizing, actor); - end_animation (ref maximizing, actor); + end_animation (ref changing_size, actor); } public override void switch_workspace (int from, int to, Meta.MotionDirection direction) { From ba6c9c8a276c274ade5c9ed62c057167d8afc75e Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 11 Aug 2026 15:23:46 +0200 Subject: [PATCH 3/6] WindowListener: Remove unused unmaximized_state_geometry tracking --- src/Misc/WindowListener.vala | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Misc/WindowListener.vala b/src/Misc/WindowListener.vala index 5945aca1a..1f1b6582b 100644 --- a/src/Misc/WindowListener.vala +++ b/src/Misc/WindowListener.vala @@ -59,16 +59,10 @@ public class Gala.WindowListener : Object { WindowGeometry window_geometry = {}; window_geometry.inner = window.get_frame_rect (); window_geometry.outer = window.get_buffer_rect (); - - unmaximized_state_geometry.@set (window, window_geometry); } public signal void window_fullscreen_changed (Meta.Window window); - private Gee.HashMap unmaximized_state_geometry; - - private WindowListener () { - unmaximized_state_geometry = new Gee.HashMap (); - } + private WindowListener () { } private void monitor_window (Meta.Window window) { window.notify.connect (window_notify); @@ -96,10 +90,6 @@ public class Gala.WindowListener : Object { } } - public WindowGeometry? get_unmaximized_state_geometry (Meta.Window window) { - return unmaximized_state_geometry.@get (window); - } - private void window_removed (Meta.Window window) { window.notify.disconnect (window_notify); window.unmanaged.disconnect (window_removed); From dab4ce55a78f58d3f921254642be36cdf1d76c18 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 10 Aug 2026 15:27:46 +0200 Subject: [PATCH 4/6] WindowManager: Use TransitionBuilder to minimize windows --- src/WindowManager.vala | 51 +++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 207e31742..69f11a984 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -1184,8 +1184,11 @@ namespace Gala { } public override void minimize (Meta.WindowActor actor) { - if (!Meta.Prefs.get_gnome_animations () || - actor.get_meta_window ().window_type != Meta.WindowType.NORMAL) { + animate_minimize.begin (actor); + } + + private async void animate_minimize (Meta.WindowActor actor) { + if (actor.get_meta_window ().window_type != NORMAL) { minimize_completed (actor); return; } @@ -1193,6 +1196,8 @@ namespace Gala { kill_window_effects (actor); minimizing.add (actor); + var builder = new TransitionBuilder (actor, AnimationDuration.HIDE, EASE_IN_EXPO); + Mtk.Rectangle icon = {}; if (actor.get_meta_window ().get_icon_geometry (out icon)) { // Fix icon position and size according to ui scaling factor. @@ -1207,37 +1212,22 @@ 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); - }); + builder.add_property ("scale-x", (double) (icon.width / actor.width)); + builder.add_property ("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); - }); + builder.add_property ("scale-x", 0.0); + builder.add_property ("scale-y", 0.0); } + + builder.add_property ("opacity", 0u); + + yield builder.run (); + + actor.set_pivot_point (0.0f, 0.0f); + minimizing.remove (actor); + minimize_completed (actor); } private async void animate_size_change (Meta.WindowActor actor, Mtk.Rectangle old_rect, Mtk.Rectangle new_rect, Clutter.Actor snapshot) { @@ -1469,9 +1459,8 @@ namespace Gala { if (end_animation (ref unminimizing, actor)) unminimize_completed (actor); - if (end_animation (ref minimizing, actor)) - minimize_completed (actor); + end_animation (ref minimizing, actor); end_animation (ref mapping, actor); end_animation (ref destroying, actor); end_animation (ref changing_size, actor); From 6c3de10caaabcae6cc24fb10ad89abd6e87df319 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 10 Aug 2026 15:36:55 +0200 Subject: [PATCH 5/6] WindowManager: Use TransitionBuilder for unminimize --- src/WindowManager.vala | 53 +++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 69f11a984..6483ec263 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -1265,45 +1265,32 @@ namespace Gala { } public override void unminimize (Meta.WindowActor actor) { - if (!Meta.Prefs.get_gnome_animations ()) { - actor.show (); + animate_unminimize.begin (actor); + } + + private async void animate_unminimize (Meta.WindowActor actor) { + actor.show (); + + if (actor.meta_window.window_type != NORMAL) { unminimize_completed (actor); return; } - var duration = AnimationDuration.HIDE; - unowned var window = actor.get_meta_window (); - actor.remove_all_transitions (); - actor.show (); - switch (window.window_type) { - case Meta.WindowType.NORMAL: - unminimizing.add (actor); + unminimizing.add (actor); - actor.set_pivot_point (0.5f, 1.0f); - actor.set_scale (0.01f, 0.1f); - actor.opacity = 0U; - - actor.save_easing_state (); - actor.set_easing_mode (Clutter.AnimationMode.EASE_OUT_EXPO); - actor.set_easing_duration (duration); - actor.set_scale (1.0f, 1.0f); - actor.opacity = 255U; - actor.restore_easing_state (); - - ulong unminimize_handler_id = 0UL; - unminimize_handler_id = actor.transitions_completed.connect (() => { - actor.disconnect (unminimize_handler_id); - unminimizing.remove (actor); - unminimize_completed (actor); - }); + actor.set_pivot_point (0.5f, 1.0f); - break; - default: - unminimize_completed (actor); - break; - } + var builder = new TransitionBuilder (actor, AnimationDuration.HIDE, EASE_OUT_EXPO); + builder.add_property_with_from ("scale-x", 0.01, 1.0); + builder.add_property_with_from ("scale-y", 0.1, 1.0); + builder.add_property_with_from ("opacity", 0U, 255U); + + yield builder.run (); + + unminimizing.remove (actor); + unminimize_completed (actor); } public override void map (Meta.WindowActor actor) { @@ -1457,9 +1444,7 @@ namespace Gala { size_change_completed (actor); } - if (end_animation (ref unminimizing, actor)) - unminimize_completed (actor); - + end_animation (ref unminimizing, actor); end_animation (ref minimizing, actor); end_animation (ref mapping, actor); end_animation (ref destroying, actor); From f3283b497d7afe1247d0c0207e8476969de86e52 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 11 Aug 2026 16:52:47 +0200 Subject: [PATCH 6/6] WindowManager: Move WindowEffects into a separate class --- src/Misc/NotificationStack.vala | 14 +- src/Misc/WindowEffects.vala | 253 +++++++++++++++++++++++++++++ src/WindowManager.vala | 273 +++----------------------------- src/meson.build | 1 + 4 files changed, 281 insertions(+), 260 deletions(-) create mode 100644 src/Misc/WindowEffects.vala diff --git a/src/Misc/NotificationStack.vala b/src/Misc/NotificationStack.vala index 23ba6593e..393fcf8dd 100644 --- a/src/Misc/NotificationStack.vala +++ b/src/Misc/NotificationStack.vala @@ -163,17 +163,15 @@ public class Gala.NotificationStack : Object { } } - public void destroy_notification (Meta.WindowActor notification) { - notification.save_easing_state (); - notification.set_easing_duration (Utils.get_animation_duration (AnimationDuration.CLOSE)); - notification.set_easing_mode (Clutter.AnimationMode.EASE_IN_QUAD); - notification.opacity = 0; - - notification.x += stack_width; - notification.restore_easing_state (); + public async void destroy_notification (Meta.WindowActor notification) { + var builder = new TransitionBuilder (notification, AnimationDuration.CLOSE, EASE_IN_QUAD); + builder.add_property ("opacity", 0u); + builder.add_property ("x", (float) (notification.x + stack_width)); notifications.remove (notification); update_positions (); + + yield builder.run (); } /** diff --git a/src/Misc/WindowEffects.vala b/src/Misc/WindowEffects.vala new file mode 100644 index 000000000..28796b2a3 --- /dev/null +++ b/src/Misc/WindowEffects.vala @@ -0,0 +1,253 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.WindowEffects : Object { + public Clutter.Actor ui_group { get; construct; } + public NotificationStack notification_stack { get; construct; } + + private Gee.HashSet minimizing = new Gee.HashSet (); + private Gee.HashSet mapping = new Gee.HashSet (); + private Gee.HashSet destroying = new Gee.HashSet (); + private Gee.HashSet unminimizing = new Gee.HashSet (); + private Gee.HashSet changing_size = new Gee.HashSet (); + + public WindowEffects (Clutter.Actor ui_group, NotificationStack notification_stack) { + Object (ui_group: ui_group, notification_stack: notification_stack); + } + + public async void animate_map (Meta.WindowActor actor) { + var window = actor.meta_window; + + mapping.add (actor); + + switch (window.window_type) { + case Meta.WindowType.NORMAL: + if (window.maximized_vertically || window.maximized_horizontally) { + var outer_rect = window.get_frame_rect (); + actor.set_position (outer_rect.x, outer_rect.y); + } + + actor.set_pivot_point (0.5f, 1.0f); + + var builder = new TransitionBuilder (actor, AnimationDuration.HIDE, EASE_OUT_EXPO); + builder.add_property_with_from ("scale-x", 0.01, 1.0); + builder.add_property_with_from ("scale-y", 0.1, 1.0); + builder.add_property_with_from ("opacity", 0U, 255U); + yield builder.run (); + break; + + case Meta.WindowType.MODAL_DIALOG: + case Meta.WindowType.DIALOG: + dim_parent_window (window); + actor.set_pivot_point (0.5f, 0.5f); + + var builder = new TransitionBuilder (actor, 200, EASE_OUT_QUAD); + builder.add_property_with_from ("scale-x", 1.05, 1.0); + builder.add_property_with_from ("scale-y", 1.05, 1.0); + builder.add_property_with_from ("opacity", 0U, 255U); + yield builder.run (); + break; + + default: + break; + } + + mapping.remove (actor); + } + + private void dim_parent_window (Meta.Window window) { + if (window.window_type != MODAL_DIALOG) { + return; + } + + unowned var transient = window.get_transient_for (); + if (transient == null || transient == window) { + warning ("No transient found"); + return; + } + + unowned var transient_actor = (Meta.WindowActor) transient.get_compositor_private (); + var dark_effect = new Clutter.BrightnessContrastEffect (); + dark_effect.set_brightness (-0.4f); + transient_actor.add_effect_with_name ("dim-parent", dark_effect); + + window.unmanaged.connect (() => { + if (transient_actor != null && transient_actor.get_effect ("dim-parent") != null) { + transient_actor.remove_effect_by_name ("dim-parent"); + } + }); + } + + public async void animate_destroy (Meta.WindowActor actor) { + var window = actor.meta_window; + + destroying.add (actor); + + switch (window.window_type) { + case Meta.WindowType.NORMAL: + actor.set_pivot_point (0.5f, 0.5f); + actor.show (); + + var builder = new TransitionBuilder (actor, AnimationDuration.CLOSE, LINEAR); + builder.add_property ("scale-x", 0.8); + builder.add_property ("scale-y", 0.8); + builder.add_property ("opacity", 0U); + yield builder.run (); + + Utils.clear_window_cache (window); + break; + + case Meta.WindowType.MODAL_DIALOG: + case Meta.WindowType.DIALOG: + actor.set_pivot_point (0.5f, 0.5f); + + var builder = new TransitionBuilder (actor, 150, EASE_OUT_QUAD); + builder.add_property ("scale-x", 1.05); + builder.add_property ("scale-y", 1.05); + builder.add_property ("opacity", 0U); + yield builder.run (); + break; + + default: + if (NotificationStack.is_notification (window)) { + yield notification_stack.destroy_notification (actor); + } + + break; + } + + destroying.remove (actor); + } + + public async void animate_size_change (Meta.WindowActor actor, Mtk.Rectangle old_rect, Mtk.Rectangle new_rect, Clutter.Actor snapshot) { + kill_window_effects (actor); + + changing_size.add (actor); + + snapshot.set_position (old_rect.x, old_rect.y); + + ui_group.add_child (snapshot); + + var scale_x = (double) new_rect.width / old_rect.width; + var scale_y = (double) new_rect.height / old_rect.height; + + snapshot.save_easing_state (); + snapshot.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT_QUAD); + snapshot.set_easing_duration (AnimationDuration.SNAP); + snapshot.set_position (new_rect.x, new_rect.y); + snapshot.set_scale (scale_x, scale_y); + snapshot.opacity = 0; + snapshot.restore_easing_state (); + + actor.set_pivot_point (0.0f, 0.0f); + + var actor_transition_builder = new TransitionBuilder (actor, AnimationDuration.SNAP, EASE_IN_OUT_QUAD); + actor_transition_builder.add_property_with_from ("scale-x", 1.0 / scale_x, 1.0); + actor_transition_builder.add_property_with_from ("scale-y", 1.0 / scale_y, 1.0); + actor_transition_builder.add_property_with_from ("translation-x", (float) (old_rect.x - new_rect.x), 0.0f); + actor_transition_builder.add_property_with_from ("translation-y", (float) (old_rect.y - new_rect.y), 0.0f); + + yield actor_transition_builder.run (); + + ui_group.remove_child (snapshot); + changing_size.remove (actor); + } + + public async void animate_minimize (Meta.WindowActor actor) { + if (actor.get_meta_window ().window_type != NORMAL) { + return; + } + + kill_window_effects (actor); + minimizing.add (actor); + + var builder = new TransitionBuilder (actor, AnimationDuration.HIDE, EASE_IN_EXPO); + + Mtk.Rectangle icon = {}; + if (actor.get_meta_window ().get_icon_geometry (out icon)) { + var display = actor.meta_window.display; + + // Fix icon position and size according to ui scaling factor. + var ui_scale = display.get_monitor_scale (display.get_monitor_index_for_rect (icon)); + icon.x = Utils.scale_to_int (icon.x, ui_scale); + icon.y = Utils.scale_to_int (icon.y, ui_scale); + icon.width = Utils.scale_to_int (icon.width, ui_scale); + icon.height = Utils.scale_to_int (icon.height, ui_scale); + + actor.set_pivot_point ( + (actor.x - icon.x) / (icon.width - actor.width), + (actor.y - icon.y) / (icon.height - actor.height) + ); + + builder.add_property ("scale-x", (double) (icon.width / actor.width)); + builder.add_property ("scale-y", (double) (icon.height / actor.height)); + } else { + actor.set_pivot_point (0.5f, 1.0f); + + builder.add_property ("scale-x", 0.0); + builder.add_property ("scale-y", 0.0); + } + + builder.add_property ("opacity", 0u); + + yield builder.run (); + + actor.set_pivot_point (0.0f, 0.0f); + minimizing.remove (actor); + } + + public async void animate_unminimize (Meta.WindowActor actor) { + actor.show (); + + if (actor.meta_window.window_type != NORMAL) { + return; + } + + actor.remove_all_transitions (); + + unminimizing.add (actor); + + actor.set_pivot_point (0.5f, 1.0f); + + var builder = new TransitionBuilder (actor, AnimationDuration.HIDE, EASE_OUT_EXPO); + builder.add_property_with_from ("scale-x", 0.01, 1.0); + builder.add_property_with_from ("scale-y", 0.1, 1.0); + builder.add_property_with_from ("opacity", 0U, 255U); + + yield builder.run (); + + unminimizing.remove (actor); + } + + public void kill_window_effects (Meta.WindowActor actor) { + end_animation (ref unminimizing, actor); + end_animation (ref minimizing, actor); + end_animation (ref mapping, actor); + end_animation (ref destroying, actor); + end_animation (ref changing_size, actor); + } + + // Cancel attached animation of an actor and reset it + private bool end_animation (ref Gee.HashSet list, Meta.WindowActor actor) { + if (!list.contains (actor)) + return false; + + if (actor.is_destroyed ()) { + list.remove (actor); + return false; + } + + actor.remove_all_transitions (); + actor.opacity = 255U; + actor.set_scale (1.0f, 1.0f); + actor.rotation_angle_x = 0.0f; + actor.set_pivot_point (0.0f, 0.0f); + + list.remove (actor); + return true; + } +} diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 6483ec263..5fb6be59a 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -30,6 +30,8 @@ namespace Gala { } } + private const Meta.WindowType[] ANIMATABLE_WINDOW_TYPES = { NORMAL, MODAL_DIALOG, DIALOG }; + private const string OPEN_MULTITASKING_VIEW = "dbus-send --session --dest=org.pantheon.gala --print-reply /org/pantheon/gala org.pantheon.gala.PerformAction int32:1"; private const string OPEN_APPLICATIONS_MENU = "io.elementary.wingpanel --toggle-indicator=app-launcher"; @@ -128,12 +130,8 @@ namespace Gala { private Gee.LinkedList modal_stack = new Gee.LinkedList (); - private Gee.HashSet minimizing = new Gee.HashSet (); - private Gee.HashSet mapping = new Gee.HashSet (); - private Gee.HashSet destroying = new Gee.HashSet (); - private Gee.HashSet unminimizing = new Gee.HashSet (); + private WindowEffects window_effects; private Gee.HashMap pending_size_change = new Gee.HashMap (); - private Gee.HashSet changing_size = new Gee.HashSet (); private GLib.Settings behavior_settings; @@ -165,6 +163,7 @@ namespace Gala { AccessDialog.watch_portal (); + window_effects = new WindowEffects (ui_group, notification_stack); filter_manager = new FilterManager (this); notifications_manager = new NotificationsManager (); @@ -740,29 +739,6 @@ namespace Gala { display.unset_input_focus (display.get_current_time ()); } - private void dim_parent_window (Meta.Window window) { - if (window.window_type != MODAL_DIALOG) { - return; - } - - unowned var transient = window.get_transient_for (); - if (transient == null || transient == window) { - warning ("No transient found"); - return; - } - - unowned var transient_actor = (Meta.WindowActor) transient.get_compositor_private (); - var dark_effect = new Clutter.BrightnessContrastEffect (); - dark_effect.set_brightness (-0.4f); - transient_actor.add_effect_with_name ("dim-parent", dark_effect); - - window.unmanaged.connect (() => { - if (transient_actor != null && transient_actor.get_effect ("dim-parent") != null) { - transient_actor.remove_effect_by_name ("dim-parent"); - } - }); - } - private void set_grab_trigger (Meta.Window window, Meta.GrabOp op) { var proxy = push_modal (stage, true); @@ -1170,11 +1146,11 @@ namespace Gala { } } - animate_size_change.begin (actor, old_rect, new_rect, info.snapshot); + window_effects.animate_size_change.begin (actor, old_rect, new_rect, info.snapshot); break; case Meta.SizeChange.UNMAXIMIZE: case Meta.SizeChange.UNFULLSCREEN: - animate_size_change.begin (actor, old_rect, new_rect, info.snapshot); + window_effects.animate_size_change.begin (actor, old_rect, new_rect, info.snapshot); break; default: break; @@ -1184,113 +1160,21 @@ namespace Gala { } public override void minimize (Meta.WindowActor actor) { - animate_minimize.begin (actor); - } - - private async void animate_minimize (Meta.WindowActor actor) { - if (actor.get_meta_window ().window_type != NORMAL) { + if (!can_animate (actor)) { minimize_completed (actor); return; } - kill_window_effects (actor); - minimizing.add (actor); - - var builder = new TransitionBuilder (actor, AnimationDuration.HIDE, EASE_IN_EXPO); - - Mtk.Rectangle icon = {}; - if (actor.get_meta_window ().get_icon_geometry (out icon)) { - // Fix icon position and size according to ui scaling factor. - var ui_scale = get_display ().get_monitor_scale (get_display ().get_monitor_index_for_rect (icon)); - icon.x = Utils.scale_to_int (icon.x, ui_scale); - icon.y = Utils.scale_to_int (icon.y, ui_scale); - icon.width = Utils.scale_to_int (icon.width, ui_scale); - icon.height = Utils.scale_to_int (icon.height, ui_scale); - - actor.set_pivot_point ( - (actor.x - icon.x) / (icon.width - actor.width), - (actor.y - icon.y) / (icon.height - actor.height) - ); - - builder.add_property ("scale-x", (double) (icon.width / actor.width)); - builder.add_property ("scale-y", (double) (icon.height / actor.height)); - } else { - actor.set_pivot_point (0.5f, 1.0f); - - builder.add_property ("scale-x", 0.0); - builder.add_property ("scale-y", 0.0); - } - - builder.add_property ("opacity", 0u); - - yield builder.run (); - - actor.set_pivot_point (0.0f, 0.0f); - minimizing.remove (actor); - minimize_completed (actor); - } - - private async void animate_size_change (Meta.WindowActor actor, Mtk.Rectangle old_rect, Mtk.Rectangle new_rect, Clutter.Actor snapshot) { - kill_window_effects (actor); - - changing_size.add (actor); - - snapshot.set_position (old_rect.x, old_rect.y); - - ui_group.add_child (snapshot); - - var scale_x = (double) new_rect.width / old_rect.width; - var scale_y = (double) new_rect.height / old_rect.height; - - snapshot.save_easing_state (); - snapshot.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT_QUAD); - snapshot.set_easing_duration (AnimationDuration.SNAP); - snapshot.set_position (new_rect.x, new_rect.y); - snapshot.set_scale (scale_x, scale_y); - snapshot.opacity = 0; - snapshot.restore_easing_state (); - - actor.set_pivot_point (0.0f, 0.0f); - - var actor_transition_builder = new TransitionBuilder (actor, AnimationDuration.SNAP, EASE_IN_OUT_QUAD); - actor_transition_builder.add_property_with_from ("scale-x", 1.0 / scale_x, 1.0); - actor_transition_builder.add_property_with_from ("scale-y", 1.0 / scale_y, 1.0); - actor_transition_builder.add_property_with_from ("translation-x", (float) (old_rect.x - new_rect.x), 0.0f); - actor_transition_builder.add_property_with_from ("translation-y", (float) (old_rect.y - new_rect.y), 0.0f); - - yield actor_transition_builder.run (); - - ui_group.remove_child (snapshot); - changing_size.remove (actor); + window_effects.animate_minimize.begin (actor, () => minimize_completed (actor)); } public override void unminimize (Meta.WindowActor actor) { - animate_unminimize.begin (actor); - } - - private async void animate_unminimize (Meta.WindowActor actor) { - actor.show (); - - if (actor.meta_window.window_type != NORMAL) { + if (!can_animate (actor)) { unminimize_completed (actor); return; } - actor.remove_all_transitions (); - - unminimizing.add (actor); - - actor.set_pivot_point (0.5f, 1.0f); - - var builder = new TransitionBuilder (actor, AnimationDuration.HIDE, EASE_OUT_EXPO); - builder.add_property_with_from ("scale-x", 0.01, 1.0); - builder.add_property_with_from ("scale-y", 0.1, 1.0); - builder.add_property_with_from ("opacity", 0U, 255U); - - yield builder.run (); - - unminimizing.remove (actor); - unminimize_completed (actor); + window_effects.animate_unminimize.begin (actor, () => unminimize_completed (actor)); } public override void map (Meta.WindowActor actor) { @@ -1302,141 +1186,30 @@ namespace Gala { actor.show (); // Notifications initial animation is handled by the notification stack - if (NotificationStack.is_notification (window) || !Meta.Prefs.get_gnome_animations ()) { - dim_parent_window (window); + if (NotificationStack.is_notification (window) || !can_animate (actor)) { map_completed (actor); return; } - animate_map.begin (actor); - } - - private async void animate_map (Meta.WindowActor actor) { - var window = actor.meta_window; - - mapping.add (actor); - - switch (window.window_type) { - case Meta.WindowType.NORMAL: - if (window.maximized_vertically || window.maximized_horizontally) { - var outer_rect = window.get_frame_rect (); - actor.set_position (outer_rect.x, outer_rect.y); - } - - actor.set_pivot_point (0.5f, 1.0f); - - var builder = new TransitionBuilder (actor, AnimationDuration.HIDE, EASE_OUT_EXPO); - builder.add_property_with_from ("scale-x", 0.01, 1.0); - builder.add_property_with_from ("scale-y", 0.1, 1.0); - builder.add_property_with_from ("opacity", 0U, 255U); - yield builder.run (); - break; - - case Meta.WindowType.MODAL_DIALOG: - case Meta.WindowType.DIALOG: - dim_parent_window (window); - actor.set_pivot_point (0.5f, 0.5f); - - var builder = new TransitionBuilder (actor, 200, EASE_OUT_QUAD); - builder.add_property_with_from ("scale-x", 1.05, 1.0); - builder.add_property_with_from ("scale-y", 1.05, 1.0); - builder.add_property_with_from ("opacity", 0U, 255U); - yield builder.run (); - break; - - default: - break; - } - - mapping.remove (actor); - map_completed (actor); + window_effects.animate_map.begin (actor, () => map_completed (actor)); } public override void destroy (Meta.WindowActor actor) { - unowned var window = actor.get_meta_window (); - actor.remove_all_transitions (); - if (NotificationStack.is_notification (window)) { - if (Meta.Prefs.get_gnome_animations ()) { - destroying.add (actor); - } - - notification_stack.destroy_notification (actor); - - if (Meta.Prefs.get_gnome_animations ()) { - ulong destroy_handler_id = 0UL; - destroy_handler_id = actor.transitions_completed.connect (() => { - actor.disconnect (destroy_handler_id); - destroying.remove (actor); - destroy_completed (actor); - }); - } else { - destroy_completed (actor); - } - + if (!can_animate (actor)) { + destroy_completed (actor); return; } - animate_destroy.begin (actor); + window_effects.animate_destroy.begin (actor, () => destroy_completed (actor)); } - private async void animate_destroy (Meta.WindowActor actor) { - var window = actor.meta_window; - - destroying.add (actor); - - switch (window.window_type) { - case Meta.WindowType.NORMAL: - actor.set_pivot_point (0.5f, 0.5f); - actor.show (); - - var builder = new TransitionBuilder (actor, AnimationDuration.CLOSE, LINEAR); - builder.add_property ("scale-x", 0.8); - builder.add_property ("scale-y", 0.8); - builder.add_property ("opacity", 0U); - yield builder.run (); - - Utils.clear_window_cache (window); - break; - - case Meta.WindowType.MODAL_DIALOG: - case Meta.WindowType.DIALOG: - actor.set_pivot_point (0.5f, 0.5f); - - var builder = new TransitionBuilder (actor, 150, EASE_OUT_QUAD); - builder.add_property ("scale-x", 1.05); - builder.add_property ("scale-y", 1.05); - builder.add_property ("opacity", 0U); - yield builder.run (); - break; - - default: - break; - } - - destroying.remove (actor); - destroy_completed (actor); - } - - // Cancel attached animation of an actor and reset it - private bool end_animation (ref Gee.HashSet list, Meta.WindowActor actor) { - if (!list.contains (actor)) - return false; - - if (actor.is_destroyed ()) { - list.remove (actor); - return false; - } - - actor.remove_all_transitions (); - actor.opacity = 255U; - actor.set_scale (1.0f, 1.0f); - actor.rotation_angle_x = 0.0f; - actor.set_pivot_point (0.0f, 0.0f); - - list.remove (actor); - return true; + private bool can_animate (Meta.WindowActor actor) { + /* For other window types mutter expects that we call the corresponding completed method + immediately otherwise it can lead to crashes. However async methods will always + only return in the next main loop iteration. So we have to guard here already. */ + return actor.meta_window.window_type in ANIMATABLE_WINDOW_TYPES; } public override void kill_window_effects (Meta.WindowActor actor) { @@ -1444,11 +1217,7 @@ namespace Gala { size_change_completed (actor); } - end_animation (ref unminimizing, actor); - end_animation (ref minimizing, actor); - end_animation (ref mapping, actor); - end_animation (ref destroying, actor); - end_animation (ref changing_size, actor); + window_effects.kill_window_effects (actor); } public override void switch_workspace (int from, int to, Meta.MotionDirection direction) { diff --git a/src/meson.build b/src/meson.build index c6bd9d9ca..93f6f9fa3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -38,6 +38,7 @@ gala_bin_sources = files( 'Misc/SessionManager.vala', 'Misc/WindowAttentionTracker.vala', 'Misc/WindowDragProvider.vala', + 'Misc/WindowEffects.vala', 'Misc/WindowListener.vala', 'Misc/WindowMover.vala', 'Misc/WindowStateSaver.vala',