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',