diff --git a/lib/CloseButton.vala b/lib/CloseButton.vala new file mode 100644 index 00000000..e8635789 --- /dev/null +++ b/lib/CloseButton.vala @@ -0,0 +1,135 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +public class Gala.CloseButton : Clutter.Actor { + private const uint ANIMATION_DURATION = 100; + private static Gee.HashMap close_pixbufs; + + public signal void triggered (uint32 timestamp); + public float scale { get; construct set; } + + // used to avoid changing hitbox of the button + private Clutter.Actor pixbuf_actor; + private bool is_pressed = false; + + public CloseButton (float scale) { + Object (scale: scale); + } + + static construct { + close_pixbufs = new Gee.HashMap (); + } + + construct { + reactive = true; + + pixbuf_actor = new Clutter.Actor () { + pivot_point = { 0.5f, 0.5f } + }; + add_child (pixbuf_actor); + + var pixbuf = get_close_button_pixbuf (scale); + if (pixbuf != null) { + try { + var image = new Clutter.Image (); + Cogl.PixelFormat pixel_format = (pixbuf.get_has_alpha () ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888); + image.set_data (pixbuf.get_pixels (), pixel_format, pixbuf.width, pixbuf.height, pixbuf.rowstride); + + pixbuf_actor.set_content (image); + pixbuf_actor.set_size (pixbuf.width, pixbuf.height); + set_size (pixbuf.width, pixbuf.height); + } catch (Error e) { + create_error_texture (); + } + } else { + create_error_texture (); + } + } + + private static Gdk.Pixbuf? get_close_button_pixbuf (float scale) { + var height = Utils.scale_to_int (36, scale); + + if (close_pixbufs[height] == null) { + try { + close_pixbufs[height] = new Gdk.Pixbuf.from_resource_at_scale ( + Config.RESOURCEPATH + "/buttons/close.svg", + -1, + height, + true + ); + } catch (Error e) { + critical (e.message); + return null; + } + } + + return close_pixbufs[height]; + } + + private void create_error_texture () { + // we'll just make this red so there's at least something as an + // indicator that loading failed. Should never happen and this + // works as good as some weird fallback-image-failed-to-load pixbuf + critical ("Could not create close button"); + + var size = Utils.scale_to_int (36, scale); + pixbuf_actor.set_size (size, size); + pixbuf_actor.background_color = { 255, 0, 0, 255 }; + } + +#if HAS_MUTTER45 + public override bool button_press_event (Clutter.Event e) { +#else + public override bool button_press_event (Clutter.ButtonEvent e) { +#endif + var estimated_duration = (uint) (ANIMATION_DURATION * (scale_x - 0.8) / 0.2); + + pixbuf_actor.save_easing_state (); + pixbuf_actor.set_easing_duration (estimated_duration); + pixbuf_actor.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT); + pixbuf_actor.set_scale (0.8, 0.8); + pixbuf_actor.restore_easing_state (); + + is_pressed = true; + + return Clutter.EVENT_STOP; + } + +#if HAS_MUTTER45 + public override bool button_release_event (Clutter.Event e) { +#else + public override bool button_release_event (Clutter.ButtonEvent e) { +#endif + reset_scale (); + + if (is_pressed) { + triggered (e.get_time ()); + is_pressed = false; + } + + return Clutter.EVENT_STOP; + } + +#if HAS_MUTTER45 + public override bool leave_event (Clutter.Event event) { +#else + public override bool leave_event (Clutter.CrossingEvent event) { +#endif + reset_scale (); + is_pressed = false; + + return Clutter.EVENT_PROPAGATE; + } + + private void reset_scale () { + var estimated_duration = (uint) (ANIMATION_DURATION * (1.0 - scale_x) / 0.2); + + pixbuf_actor.save_easing_state (); + pixbuf_actor.set_easing_duration (estimated_duration); + pixbuf_actor.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT); + pixbuf_actor.set_scale (1.0, 1.0); + pixbuf_actor.restore_easing_state (); + } +} diff --git a/lib/Utils.vala b/lib/Utils.vala index 394269e6..7c22b746 100644 --- a/lib/Utils.vala +++ b/lib/Utils.vala @@ -24,7 +24,6 @@ namespace Gala { } private static Gee.HashMap? resize_pixbufs = null; - private static Gee.HashMap? close_pixbufs = null; private static Gee.HashMultiMap icon_cache; private static Gee.HashMap window_to_desktop_cache; @@ -332,70 +331,11 @@ namespace Gala { #endif } - /** - * Returns the pixbuf that is used for close buttons throughout gala at a - * size of 36px - * - * @return the close button pixbuf or null if it failed to load - */ - public static Gdk.Pixbuf? get_close_button_pixbuf (float scale) { - var height = scale_to_int (36, scale); - - if (close_pixbufs == null) { - close_pixbufs = new Gee.HashMap (); - } - - if (close_pixbufs[height] == null) { - try { - close_pixbufs[height] = new Gdk.Pixbuf.from_resource_at_scale ( - Config.RESOURCEPATH + "/buttons/close.svg", - -1, - height, - true - ); - } catch (Error e) { - warning (e.message); - return null; - } - } - - return close_pixbufs[height]; - } - - /** - * Creates a new reactive ClutterActor at 36px with the close pixbuf - * - * @return The close button actor - */ - public static Clutter.Actor create_close_button (float scale) { - var texture = new Clutter.Actor (); - var pixbuf = get_close_button_pixbuf (scale); - - texture.reactive = true; - - if (pixbuf != null) { - try { - var image = new Clutter.Image (); - Cogl.PixelFormat pixel_format = (pixbuf.get_has_alpha () ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888); - image.set_data (pixbuf.get_pixels (), pixel_format, pixbuf.width, pixbuf.height, pixbuf.rowstride); - texture.set_content (image); - texture.set_size (pixbuf.width, pixbuf.height); - } catch (Error e) {} - } else { - // we'll just make this red so there's at least something as an - // indicator that loading failed. Should never happen and this - // works as good as some weird fallback-image-failed-to-load pixbuf - texture.set_size (scale_to_int (36, scale), scale_to_int (36, scale)); - texture.background_color = { 255, 0, 0, 255 }; - } - - return texture; - } /** * Returns the pixbuf that is used for resize buttons throughout gala at a * size of 36px * - * @return the close button pixbuf or null if it failed to load + * @return the resize button pixbuf or null if it failed to load */ public static Gdk.Pixbuf? get_resize_button_pixbuf (float scale) { var height = scale_to_int (36, scale); diff --git a/lib/meson.build b/lib/meson.build index 25c5432e..8e4f5608 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -3,6 +3,7 @@ gala_lib_sources = files( 'App.vala', 'AppCache.vala', 'AppSystem.vala', + 'CloseButton.vala', 'Constants.vala', 'DragDropAction.vala', 'Drawing/BufferSurface.vala', diff --git a/plugins/pip/PopupWindow.vala b/plugins/pip/PopupWindow.vala index 270b71c8..535ecd26 100644 --- a/plugins/pip/PopupWindow.vala +++ b/plugins/pip/PopupWindow.vala @@ -23,9 +23,8 @@ public class Gala.Plugins.PIP.PopupWindow : Clutter.Actor { private Clutter.Actor clone; private Clutter.Actor container; - private Clutter.Actor close_button; + private Gala.CloseButton close_button; private Clutter.Actor resize_button; - private Clutter.ClickAction close_action; private DragDropAction move_action; private float begin_resize_width = 0.0f; @@ -111,16 +110,13 @@ public class Gala.Plugins.PIP.PopupWindow : Clutter.Actor { set_position (x_position, y_position); - close_action = new Clutter.ClickAction (); - close_action.clicked.connect (on_close_click_clicked); - - close_button = Gala.Utils.create_close_button (scale); - close_button.opacity = 0; - close_button.reactive = true; + close_button = new Gala.CloseButton (scale) { + opacity = 0 + }; // TODO: Check if close button should be on the right close_button.add_constraint (new Clutter.AlignConstraint (this, Clutter.AlignAxis.X_AXIS, 0.0f)); close_button.add_constraint (new Clutter.AlignConstraint (this, Clutter.AlignAxis.Y_AXIS, 0.0f)); - close_button.add_action (close_action); + close_button.triggered.connect (on_close_click_clicked); resize_button = Utils.create_resize_button (scale); resize_button.opacity = 0; diff --git a/src/Widgets/WindowClone.vala b/src/Widgets/WindowClone.vala index a58dc2eb..21454a5f 100644 --- a/src/Widgets/WindowClone.vala +++ b/src/Widgets/WindowClone.vala @@ -91,7 +91,7 @@ public class Gala.WindowClone : Clutter.Actor { private ulong check_confirm_dialog_cb = 0; private bool in_slot_animation = false; - private Clutter.Actor close_button; + private Gala.CloseButton close_button; private ActiveShape active_shape; private Clutter.Actor window_icon; private Tooltip window_title; @@ -158,15 +158,10 @@ public class Gala.WindowClone : Clutter.Actor { private void reallocate () { var window_frame_rect = window.get_frame_rect (); - var close_button_action = new Clutter.ClickAction (); - close_button_action.clicked.connect (() => { - close_window (); - }); - close_button = Utils.create_close_button (monitor_scale_factor); - close_button.opacity = 0; - // block propagation of button release event to window clone - close_button.button_release_event.connect (() => { return Clutter.EVENT_STOP; }); - close_button.add_action (close_button_action); + close_button = new Gala.CloseButton (monitor_scale_factor) { + opacity = 0 + }; + close_button.triggered.connect (close_window); window_icon = new WindowIcon (window, WINDOW_ICON_SIZE, (int)Math.round (monitor_scale_factor)); window_icon.opacity = 0; @@ -577,11 +572,11 @@ public class Gala.WindowClone : Clutter.Actor { * dialog of the window we were going to delete. If that's the case, we request * to select our window. */ - private void close_window () { - unowned Meta.Display display = window.get_display (); + private void close_window (uint32 timestamp) { + unowned var display = window.get_display (); check_confirm_dialog_cb = display.window_entered_monitor.connect (check_confirm_dialog); - window.@delete (display.get_current_time ()); + window.@delete (timestamp); } private void check_confirm_dialog (int monitor, Meta.Window new_window) { @@ -624,7 +619,7 @@ public class Gala.WindowClone : Clutter.Actor { selected (); break; case Clutter.Button.MIDDLE: - close_window (); + close_window (wm.get_display ().get_current_time ()); break; } }