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..9f911ec84 100644
--- a/src/BackgroundBlurEffect.vala
+++ b/src/BackgroundBlurEffect.vala
@@ -68,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) {
@@ -80,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) {
@@ -107,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));
}
""",
@@ -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);
diff --git a/src/BlurManager.vala b/src/BlurManager.vala
new file mode 100644
index 000000000..447bc397b
--- /dev/null
+++ b/src/BlurManager.vala
@@ -0,0 +1,150 @@
+/*
+* 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);
+ });
+ }
+
+ /**
+ * 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 blur_data = blurred_windows[window];
+ if (blur_data == null) {
+ var blur_effect = new BackgroundBlurEffect (BLUR_RADIUS, (int) clip_radius, 1.0f);
+
+ 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;
+ 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);
+ }
+
+ 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);
+ }
+
+ //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',