From 3324c9353d2e3684947998a45827abe9995a9cea Mon Sep 17 00:00:00 2001 From: lenemter Date: Mon, 28 Jul 2025 19:38:14 +0900 Subject: [PATCH 1/3] Add Blur API --- protocol/pantheon-desktop-shell-v1.xml | 18 +++ protocol/pantheon-desktop-shell.vapi | 6 + src/BackgroundBlurEffect.vala | 6 +- src/BlurManager.vala | 165 +++++++++++++++++++++++++ src/PantheonShell.vala | 36 ++++++ src/WindowManager.vala | 1 + src/meson.build | 1 + 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/BlurManager.vala diff --git a/protocol/pantheon-desktop-shell-v1.xml b/protocol/pantheon-desktop-shell-v1.xml index e19030905..fcd0f1758 100644 --- a/protocol/pantheon-desktop-shell-v1.xml +++ b/protocol/pantheon-desktop-shell-v1.xml @@ -104,6 +104,24 @@ Tell the shell that the panel would like to be visible in the multitasking view. + + + + Tell the window manager to add background blur. + + + + + + + + + + + + Tell the window manager to remove blur that was set in set_blur_region. + + diff --git a/protocol/pantheon-desktop-shell.vapi b/protocol/pantheon-desktop-shell.vapi index b026f2650..778d1ca42 100644 --- a/protocol/pantheon-desktop-shell.vapi +++ b/protocol/pantheon-desktop-shell.vapi @@ -42,6 +42,8 @@ namespace Pantheon.Desktop { public SetSize set_size; public SetHideMode set_hide_mode; public RequestVisibleInMultitaskingView request_visible_in_multitasking_view; + public AddBlur add_blur; + public RemoveBlur remove_blur; } [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "struct io_elementary_pantheon_widget_v1_interface")] @@ -78,6 +80,10 @@ namespace Pantheon.Desktop { [CCode (has_target = false, has_typedef = false)] public delegate void RequestVisibleInMultitaskingView (Wl.Client client, Wl.Resource resource); [CCode (has_target = false, has_typedef = false)] + public delegate void AddBlur (Wl.Client client, Wl.Resource resource, uint left, uint right, uint top, uint bottom, uint clip_radius); + [CCode (has_target = false, has_typedef = false)] + public delegate void RemoveBlur (Wl.Client client, Wl.Resource resource); + [CCode (has_target = false, has_typedef = false)] public delegate void SetKeepAbove (Wl.Client client, Wl.Resource resource); [CCode (has_target = false, has_typedef = false)] public delegate void MakeCentered (Wl.Client client, Wl.Resource resource); diff --git a/src/BackgroundBlurEffect.vala b/src/BackgroundBlurEffect.vala index ab250c85f..eafb66a10 100644 --- a/src/BackgroundBlurEffect.vala +++ b/src/BackgroundBlurEffect.vala @@ -4,6 +4,10 @@ */ public class Gala.BackgroundBlurEffect : Clutter.Effect { + /** + * Our rounded corners effect is antialiased, so we need to add a small offset to have proper corners + */ + private const int CLIP_RADIUS_OFFSET = 2; private const float MIN_DOWNSCALE_SIZE = 256.0f; private const float MAX_RADIUS = 12.0f; private const int FORCE_REFRESH_FRAMES = 2; @@ -145,7 +149,7 @@ public class Gala.BackgroundBlurEffect : Clutter.Effect { } private void update_clip_radius () { - float[] _clip_radius = { clip_radius * monitor_scale }; + float[] _clip_radius = { clip_radius * monitor_scale + CLIP_RADIUS_OFFSET }; round_pipeline.set_uniform_float (round_clip_radius_location, 1, 1, _clip_radius); } diff --git a/src/BlurManager.vala b/src/BlurManager.vala new file mode 100644 index 000000000..fafe1d11d --- /dev/null +++ b/src/BlurManager.vala @@ -0,0 +1,165 @@ +/* +* SPDX-License-Identifier: GPL-3.0-or-later +* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Gala.BlurManager : Object { + private struct BlurData { + Clutter.Actor actor; + BackgroundBlurEffect blur_effect; + uint left; + uint right; + uint top; + uint bottom; + uint clip_radius; + } + + private const int BLUR_RADIUS = 12; + + private static BlurManager instance; + + public static void init (WindowManagerGala wm) { + if (instance != null) { + return; + } + + instance = new BlurManager (wm); + } + + public static unowned BlurManager? get_instance () { + return instance; + } + + public WindowManagerGala wm { get; construct; } + + private GLib.HashTable blurred_windows = new GLib.HashTable (null, null); + + private BlurManager (WindowManagerGala wm) { + Object (wm: wm); + } + + construct { + wm.get_display ().window_created.connect ((window) => { + window.notify["mutter-hints"].connect ((obj, pspec) => parse_mutter_hints ((Meta.Window) obj)); + parse_mutter_hints (window); + }); + + unowned var monitor_manager = wm.get_display ().get_context ().get_backend ().get_monitor_manager (); + monitor_manager.monitors_changed.connect (update_monitors); + } + + /** + * Blurs the given region of the given window. + */ + public void add_blur (Meta.Window window, uint left, uint right, uint top, uint bottom, uint clip_radius) { + unowned var window_actor = (Meta.WindowActor) window.get_compositor_private (); + if (window_actor == null) { + critical ("Cannot blur actor: Actor is null"); + return; + } + + var monitor_scaling_factor = wm.get_display ().get_monitor_scale (window.get_monitor ()); + + var blur_data = blurred_windows[window]; + if (blur_data == null) { + var blur_effect = new BackgroundBlurEffect (BLUR_RADIUS, clip_radius, monitor_scaling_factor); + + var blurred_actor = new Clutter.Actor (); + blurred_actor.add_effect (blur_effect); + window_actor.insert_child_below (blurred_actor, null); + + blur_data = { blurred_actor, blur_effect, left, right, top, bottom, clip_radius }; + blurred_windows[window] = blur_data; + + window.size_changed.connect (on_size_changed); + } + + var buffer_rect = window.get_buffer_rect (); + var frame_rect = window.get_frame_rect (); + var x_shadow_size = (frame_rect.x - buffer_rect.x) / monitor_scaling_factor; + var y_shadow_size = (frame_rect.y - buffer_rect.y) / monitor_scaling_factor; + + blur_data.actor.set_position (x_shadow_size + left, y_shadow_size + top); + blur_data.actor.set_size (frame_rect.width - left - right, frame_rect.height - top - bottom); + blur_data.blur_effect.monitor_scale = monitor_scaling_factor; + } + + public void remove_blur (Meta.Window window) { + var blur_data = blurred_windows[window]; + if (blur_data == null) { + return; + } + + var actor = blur_data.actor; + actor.remove_effect (blur_data.blur_effect); + + unowned var parent = actor.get_parent (); + if (parent != null) { + parent.remove_child (actor); + } + + blurred_windows.remove (window); + } + + private void on_size_changed (Meta.Window window) { + var blur_data = blurred_windows[window]; + if (blur_data == null) { + return; + } + + add_blur (window, blur_data.left, blur_data.right, blur_data.top, blur_data.bottom, blur_data.clip_radius); + } + + private void update_monitors () { + foreach (unowned var window in blurred_windows.get_keys ()) { + var blur_data = blurred_windows[window]; + + var monitor_scaling_factor = window.display.get_monitor_scale (window.get_monitor ()); + blur_data.blur_effect.monitor_scale = monitor_scaling_factor; + } + } + + //X11 only + private void parse_mutter_hints (Meta.Window window) { + if (window.mutter_hints == null) { + return; + } + + var mutter_hints = window.mutter_hints.split (":"); + foreach (var mutter_hint in mutter_hints) { + var split = mutter_hint.split ("="); + + if (split.length != 2) { + continue; + } + + var key = split[0]; + var val = split[1]; + + switch (key) { + case "blur": + var split_val = val.split (","); + if (split_val.length != 5) { + break; + } + + uint parsed_left = 0, parsed_right = 0, parsed_top = 0, parsed_bottom = 0, parsed_clip_radius = 0; + if ( + uint.try_parse (split_val[0], out parsed_left) && + uint.try_parse (split_val[1], out parsed_right) && + uint.try_parse (split_val[2], out parsed_top) && + uint.try_parse (split_val[3], out parsed_bottom) && + uint.try_parse (split_val[4], out parsed_clip_radius) + ) { + add_blur (window, parsed_left, parsed_right, parsed_top, parsed_bottom, parsed_clip_radius); + } else { + warning ("Failed to parse %s as width and height", val); + } + + break; + default: + break; + } + } + } +} diff --git a/src/PantheonShell.vala b/src/PantheonShell.vala index bc2782ad5..936f6eb57 100644 --- a/src/PantheonShell.vala +++ b/src/PantheonShell.vala @@ -40,6 +40,8 @@ namespace Gala { set_size, set_hide_mode, request_visible_in_multitasking_view, + add_blur, + remove_blur, }; wayland_pantheon_widget_interface = { @@ -310,6 +312,40 @@ namespace Gala { ShellClientsManager.get_instance ().request_visible_in_multitasking_view (window); } + internal static void add_blur (Wl.Client client, Wl.Resource resource, uint left, uint right, uint top, uint bottom, uint clip_radius) { + unowned PanelSurface? panel_surface = resource.get_user_data (); + if (panel_surface.wayland_surface == null) { + warning ("Window tried to set blur region but wayland surface is null."); + return; + } + + Meta.Window? window; + panel_surface.wayland_surface.get ("window", out window, null); + if (window == null) { + warning ("Window tried to set blur region but wayland surface had no associated window."); + return; + } + + BlurManager.get_instance ().add_blur (window, left, right, top, bottom, clip_radius); + } + + internal static void remove_blur (Wl.Client client, Wl.Resource resource) { + unowned PanelSurface? panel_surface = resource.get_user_data (); + if (panel_surface.wayland_surface == null) { + warning ("Window tried to remove blur but wayland surface is null."); + return; + } + + Meta.Window? window; + panel_surface.wayland_surface.get ("window", out window, null); + if (window == null) { + warning ("Window tried to remove blur but wayland surface had no associated window."); + return; + } + + BlurManager.get_instance ().remove_blur (window); + } + internal static void set_keep_above (Wl.Client client, Wl.Resource resource) { unowned ExtendedBehaviorSurface? eb_surface = resource.get_user_data (); if (eb_surface.wayland_surface == null) { diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 0ab8e0968..3a2fd7983 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -127,6 +127,7 @@ namespace Gala { public override void start () { ShellClientsManager.init (this); + BlurManager.init (this); daemon_manager = new DaemonManager (get_display ()); show_stage (); diff --git a/src/meson.build b/src/meson.build index e9bc57c69..709fa2b56 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,6 @@ gala_bin_sources = files( 'BackgroundBlurEffect.vala', + 'BlurManager.vala', 'DBus.vala', 'DBusAccelerator.vala', 'DaemonManager.vala', From 495598caa6b75e704aba015268c41ce05d748c54 Mon Sep 17 00:00:00 2001 From: lenemter Date: Mon, 28 Jul 2025 20:24:04 +0900 Subject: [PATCH 2/3] Don't scale BackgroundBlurEffect --- src/BackgroundBlurEffect.vala | 16 ++++++---------- src/BlurManager.vala | 21 +++------------------ 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/src/BackgroundBlurEffect.vala b/src/BackgroundBlurEffect.vala index eafb66a10..011ead20c 100644 --- a/src/BackgroundBlurEffect.vala +++ b/src/BackgroundBlurEffect.vala @@ -4,10 +4,6 @@ */ public class Gala.BackgroundBlurEffect : Clutter.Effect { - /** - * Our rounded corners effect is antialiased, so we need to add a small offset to have proper corners - */ - private const int CLIP_RADIUS_OFFSET = 2; private const float MIN_DOWNSCALE_SIZE = 256.0f; private const float MAX_RADIUS = 12.0f; private const int FORCE_REFRESH_FRAMES = 2; @@ -72,8 +68,8 @@ public class Gala.BackgroundBlurEffect : Clutter.Effect { uniform float clip_radius; float rounded_rect_coverage (vec2 p) { - float center_left = clip_radius + 1.5; - float center_right = actor_size.x - clip_radius - 0.55; + float center_left = clip_radius; + float center_right = actor_size.x - clip_radius; float center_x; if (p.x < center_left) { @@ -84,8 +80,8 @@ public class Gala.BackgroundBlurEffect : Clutter.Effect { return 1.0; } - float center_top = clip_radius + 1.5; - float center_bottom = actor_size.y - clip_radius - 0.55; + float center_top = clip_radius; + float center_bottom = actor_size.y - clip_radius; float center_y; if (p.y < center_top) { @@ -111,7 +107,7 @@ public class Gala.BackgroundBlurEffect : Clutter.Effect { return 1.0; } // Only pixels on the edge of the curve need expensive antialiasing - return outer_radius - sqrt (dist_squared); + return smoothstep (outer_radius, inner_radius, sqrt (dist_squared)); } """, @@ -149,7 +145,7 @@ public class Gala.BackgroundBlurEffect : Clutter.Effect { } private void update_clip_radius () { - float[] _clip_radius = { clip_radius * monitor_scale + CLIP_RADIUS_OFFSET }; + float[] _clip_radius = { clip_radius * monitor_scale }; round_pipeline.set_uniform_float (round_clip_radius_location, 1, 1, _clip_radius); } diff --git a/src/BlurManager.vala b/src/BlurManager.vala index fafe1d11d..447bc397b 100644 --- a/src/BlurManager.vala +++ b/src/BlurManager.vala @@ -43,9 +43,6 @@ public class Gala.BlurManager : Object { window.notify["mutter-hints"].connect ((obj, pspec) => parse_mutter_hints ((Meta.Window) obj)); parse_mutter_hints (window); }); - - unowned var monitor_manager = wm.get_display ().get_context ().get_backend ().get_monitor_manager (); - monitor_manager.monitors_changed.connect (update_monitors); } /** @@ -58,11 +55,9 @@ public class Gala.BlurManager : Object { return; } - var monitor_scaling_factor = wm.get_display ().get_monitor_scale (window.get_monitor ()); - var blur_data = blurred_windows[window]; if (blur_data == null) { - var blur_effect = new BackgroundBlurEffect (BLUR_RADIUS, clip_radius, monitor_scaling_factor); + var blur_effect = new BackgroundBlurEffect (BLUR_RADIUS, (int) clip_radius, 1.0f); var blurred_actor = new Clutter.Actor (); blurred_actor.add_effect (blur_effect); @@ -76,12 +71,11 @@ public class Gala.BlurManager : Object { var buffer_rect = window.get_buffer_rect (); var frame_rect = window.get_frame_rect (); - var x_shadow_size = (frame_rect.x - buffer_rect.x) / monitor_scaling_factor; - var y_shadow_size = (frame_rect.y - buffer_rect.y) / monitor_scaling_factor; + var x_shadow_size = frame_rect.x - buffer_rect.x; + var y_shadow_size = frame_rect.y - buffer_rect.y; blur_data.actor.set_position (x_shadow_size + left, y_shadow_size + top); blur_data.actor.set_size (frame_rect.width - left - right, frame_rect.height - top - bottom); - blur_data.blur_effect.monitor_scale = monitor_scaling_factor; } public void remove_blur (Meta.Window window) { @@ -110,15 +104,6 @@ public class Gala.BlurManager : Object { add_blur (window, blur_data.left, blur_data.right, blur_data.top, blur_data.bottom, blur_data.clip_radius); } - private void update_monitors () { - foreach (unowned var window in blurred_windows.get_keys ()) { - var blur_data = blurred_windows[window]; - - var monitor_scaling_factor = window.display.get_monitor_scale (window.get_monitor ()); - blur_data.blur_effect.monitor_scale = monitor_scaling_factor; - } - } - //X11 only private void parse_mutter_hints (Meta.Window window) { if (window.mutter_hints == null) { From 50907faf120b32e7283068ede886161d2a9fe4d7 Mon Sep 17 00:00:00 2001 From: lenemter Date: Sun, 14 Sep 2025 11:19:02 +0300 Subject: [PATCH 3/3] Fix rare crash --- src/BackgroundBlurEffect.vala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/BackgroundBlurEffect.vala b/src/BackgroundBlurEffect.vala index 011ead20c..9f911ec84 100644 --- a/src/BackgroundBlurEffect.vala +++ b/src/BackgroundBlurEffect.vala @@ -340,6 +340,11 @@ public class Gala.BackgroundBlurEffect : Clutter.Effect { var width = (int) actor_box.get_width (); var height = (int) actor_box.get_height (); + if (width < 0 || height < 0) { + warning ("BackgroundBlurEffect: Couldn't update framebuffers, incorrect size"); + return false; + } + var downscale_factor = calculate_downscale_factor (width, height, real_blur_radius); var updated = update_actor_fbo (width, height, downscale_factor) && update_rounded_fbo (width, height, downscale_factor) && update_background_fbo (width, height);