diff --git a/data/gala.css b/data/gala.css index 5ee35d9ac..25d920af1 100644 --- a/data/gala.css +++ b/data/gala.css @@ -62,3 +62,18 @@ 0 3px 14px 2px alpha(#000, 0.12), 0 5px 5px -3px alpha(#000, 0.4); } + +.workspace-switcher.decoration { + border-radius: 6px; + box-shadow: + 0 3px 4px alpha(#000, 0.25), + 0 3px 3px -3px alpha(#000, 0.45); +} + +.workspace-switcher-dnd.decoration { + border-radius: 6px; + box-shadow: + 0 8px 10px 1px alpha(#000, 0.14), + 0 3px 14px 2px alpha(#000, 0.12), + 0 5px 5px -3px alpha(#000, 0.4); +} diff --git a/src/Background/BlurEffect.vala b/src/Background/BlurEffect.vala new file mode 100644 index 000000000..f396d3d84 --- /dev/null +++ b/src/Background/BlurEffect.vala @@ -0,0 +1,292 @@ +/* + * Copyright 2023 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +public class Gala.BlurEffect : Clutter.Effect { + private const float MIN_DOWNSCALE_SIZE = 256.0f; + private const float MAX_RADIUS = 12.0f; + + public new Clutter.Actor actor { get; construct; } + public float radius { get; construct; } + + private bool actor_painted = false; + private bool blur_applied = false; + private int texture_width; + private int texture_height; + private float downscale_factor; + + private Cogl.Framebuffer actor_framebuffer; + private Cogl.Pipeline actor_pipeline; + private Cogl.Texture actor_texture; + + private Cogl.Framebuffer framebuffer; + private Cogl.Pipeline pipeline; + private Cogl.Texture texture; + + public BlurEffect (Clutter.Actor actor, float radius) { + Object (actor: actor, radius: radius); + } + + construct { + unowned var ctx = Clutter.get_default_backend ().get_cogl_context (); + + actor_pipeline = new Cogl.Pipeline (ctx); + actor_pipeline.set_layer_null_texture (0); + actor_pipeline.set_layer_filters (0, Cogl.PipelineFilter.LINEAR, Cogl.PipelineFilter.LINEAR); + actor_pipeline.set_layer_wrap_mode (0, Cogl.PipelineWrapMode.CLAMP_TO_EDGE); + + pipeline = new Cogl.Pipeline (ctx); + pipeline.set_layer_null_texture (0); + pipeline.set_layer_filters (0, Cogl.PipelineFilter.LINEAR, Cogl.PipelineFilter.LINEAR); + pipeline.set_layer_wrap_mode (0, Cogl.PipelineWrapMode.CLAMP_TO_EDGE); + } + + private bool needs_repaint (Clutter.EffectPaintFlags flags) { + var actor_dirty = (flags & Clutter.EffectPaintFlags.ACTOR_DIRTY) != 0; + + return actor_dirty || !blur_applied || !actor_painted; + } + + private Clutter.ActorBox update_actor_box (Clutter.PaintContext paint_context) { + var actor_allocation_box = actor.get_allocation_box (); + Clutter.ActorBox.clamp_to_pixel (ref actor_allocation_box); + + return actor_allocation_box; + } + + private float calculate_downscale_factor (float width, float height, float radius) { + float downscale_factor = 1.0f; + float scaled_width = width; + float scaled_height = height; + float scaled_radius = radius; + + /* This is the algorithm used by Firefox; keep downscaling until either the + * blur radius is lower than the threshold, or the downscaled texture is too + * small. + */ + while ( + scaled_radius > MAX_RADIUS && + scaled_width > MIN_DOWNSCALE_SIZE && + scaled_height > MIN_DOWNSCALE_SIZE + ) { + downscale_factor *= 2.0f; + + scaled_width = width / downscale_factor; + scaled_height = height / downscale_factor; + scaled_radius = radius / downscale_factor; + } + + return downscale_factor; + } + + private void setup_projection_matrix (Cogl.Framebuffer framebuffer, float width, float height) { + Graphene.Matrix projection = {}; + projection.init_translate ({ -width / 2.0f, -height / 2.0f, 0.0f }); + projection.scale (2.0f / width, -2.0f / height, 1.0f); + + framebuffer.set_projection_matrix (projection); + } + + private bool update_general_fbo (int width, int height, float downscale_factor) { + if ( + texture_width == width && + texture_height == height && + this.downscale_factor == downscale_factor && + framebuffer != null + ) { + return true; + } + + unowned var ctx = Clutter.get_default_backend ().get_cogl_context (); + + framebuffer = null; + texture = null; + + var new_width = (int) Math.floorf (width / downscale_factor); + var new_height = (int) Math.floorf (height / downscale_factor); + + var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, new_width, new_height); + + try { + texture = new Cogl.Texture2D.from_data (ctx, new_width, new_height, Cogl.PixelFormat.BGRA_8888_PRE, surface.get_stride (), surface.get_data ()); + } catch (GLib.Error e) { + warning (e.message); + return false; + } + + pipeline.set_layer_texture (0, texture); + + framebuffer = new Cogl.Offscreen.with_texture (texture); + + setup_projection_matrix (framebuffer, new_width, new_height); + + return true; + } + + private bool update_actor_fbo (int width, int height, float downscale_factor) { + if ( + texture_width == width && + texture_height == height && + this.downscale_factor == downscale_factor && + actor_framebuffer != null + ) { + return true; + } + + actor_painted = false; + + unowned var ctx = Clutter.get_default_backend ().get_cogl_context (); + + actor_framebuffer = null; + actor_texture = null; + + var new_width = (int) Math.floorf (width / downscale_factor); + var new_height = (int) Math.floorf (height / downscale_factor); + + var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, new_width, new_height); + + try { + actor_texture = new Cogl.Texture2D.from_data (ctx, new_width, new_height, Cogl.PixelFormat.BGRA_8888_PRE, surface.get_stride (), surface.get_data ()); + } catch (GLib.Error e) { + warning (e.message); + return false; + } + + actor_pipeline.set_layer_texture (0, actor_texture); + + actor_framebuffer = (Cogl.Framebuffer) new Cogl.Offscreen.with_texture (actor_texture); + + setup_projection_matrix (actor_framebuffer, new_width, new_height); + + return true; + } + + private bool update_framebuffers (Clutter.PaintContext paint_context, Clutter.ActorBox actor_box) { + var width = (int) actor_box.get_width (); + var height = (int) actor_box.get_height (); + + var downscale_factor = calculate_downscale_factor (width, height, radius); + + var updated = update_actor_fbo (width, height, downscale_factor) && update_general_fbo (width, height, downscale_factor); + + texture_width = width; + texture_height = height; + this.downscale_factor = downscale_factor; + + return updated; + } + + private Clutter.PaintNode create_blur_nodes (Clutter.PaintNode node) { + float width, height; + actor.get_size (out width, out height); + + var general_node = new Clutter.LayerNode.to_framebuffer (framebuffer, pipeline); + node.add_child (general_node); + general_node.add_rectangle ({ 0.0f, 0.0f, width, height }); + + var blur_node = new Clutter.BlurNode ( + (uint) (texture_width / downscale_factor), + (uint) (texture_height / downscale_factor), + radius / downscale_factor + ); + general_node.add_child (blur_node); + blur_node.add_rectangle ({ + 0.0f, + 0.0f, + texture.get_width (), + texture.get_height () + }); + + blur_applied = true; + + return blur_node; + } + + private void paint_actor_offscreen (Clutter.PaintNode node, Clutter.EffectPaintFlags flags) { + var actor_dirty = (flags & Clutter.EffectPaintFlags.ACTOR_DIRTY) != 0; + + /* The actor offscreen framebuffer is updated already */ + if (actor_dirty || !actor_painted) { + /* Layer node */ + var layer_node = new Clutter.LayerNode.to_framebuffer (actor_framebuffer, actor_pipeline); + node.add_child (layer_node); + layer_node.add_rectangle ({ + 0.0f, + 0.0f, + texture_width / downscale_factor, + texture_height / downscale_factor + }); + + /* Transform node */ + Graphene.Matrix transform = {}; + transform.init_scale ( + 1.0f / downscale_factor, + 1.0f / downscale_factor, + 1.0f + ); + var transform_node = new Clutter.TransformNode (transform); + layer_node.add_child (transform_node); + + /* Actor node */ + add_actor_node (transform_node); + + actor_painted = true; + } else { + Clutter.PaintNode pipeline_node = null; + + pipeline_node = new Clutter.PipelineNode (actor_pipeline); + node.add_child (pipeline_node); + pipeline_node.add_rectangle ({ + 0.0f, + 0.0f, + texture_width / downscale_factor, + texture_height / downscale_factor, + }); + } + } + + private void add_actor_node (Clutter.PaintNode node) { + var actor_node = new Clutter.ActorNode (actor, 255); + node.add_child (actor_node); + } + + private void add_blurred_pipeline (Clutter.PaintNode node) { + Clutter.PaintNode pipeline_node = null; + float width, height; + + /* Use the untransformed actor size here, since the framebuffer itself already + * has the actor transform matrix applied. + */ + actor.get_size (out width, out height); + + pipeline_node = new Clutter.PipelineNode (pipeline); + node.add_child (pipeline_node); + + pipeline_node.add_rectangle ({ 0.0f, 0.0f, width, height }); + } + + public override void paint_node (Clutter.PaintNode node, Clutter.PaintContext paint_context, Clutter.EffectPaintFlags flags) { + if (radius <= 0) { + // fallback to drawing actor + add_actor_node (node); + return; + } + + if (needs_repaint (flags)) { + var actor_box = update_actor_box (paint_context); + + if (!update_framebuffers (paint_context, actor_box)) { + // fallback to drawing actor + add_actor_node (node); + return; + } + + var blur_node = create_blur_nodes (node); + paint_actor_offscreen (blur_node, flags); + } else { + /* Use the cached pipeline if no repaint is needed */ + add_blurred_pipeline (node); + } + } +} diff --git a/src/Widgets/IconGroup.vala b/src/Widgets/IconGroup.vala index 0529e4f17..51d6ba93e 100644 --- a/src/Widgets/IconGroup.vala +++ b/src/Widgets/IconGroup.vala @@ -13,8 +13,8 @@ namespace Gala { public class IconGroup : Clutter.Actor { public const int SIZE = 64; - private const int PLUS_SIZE = 8; - private const int PLUS_WIDTH = 24; + private const int PLUS_SIZE = 6; + private const int PLUS_WIDTH = 26; private const int BACKDROP_ABSOLUTE_OPACITY = 40; /** @@ -216,6 +216,7 @@ namespace Gala { * by relayouting in the same function, as it's only ever called when we invalidate it. */ private bool draw (Cairo.Context cr) { + clear_effects (); cr.set_operator (Cairo.Operator.CLEAR); cr.paint (); cr.set_operator (Cairo.Operator.OVER); @@ -233,43 +234,45 @@ namespace Gala { // more than one => we need a folder Drawing.Utilities.cairo_rounded_rectangle ( cr, - 0.5 * scale_factor, - 0.5 * scale_factor, - (int) width - InternalUtils.scale_to_int (1, scale_factor), - (int) height - InternalUtils.scale_to_int (1, scale_factor), + 0, + 0, + (int) width, + (int) height, InternalUtils.scale_to_int (5, scale_factor) ); + var shadow_effect = new ShadowEffect (40) { + scale_factor = scale_factor + }; + + var granite_settings = Granite.Settings.get_default (); + + if (granite_settings.prefers_color_scheme == DARK) { + const double BG_COLOR = 35.0 / 255.0; + if (drag_action.dragging) { + cr.set_source_rgba (BG_COLOR, BG_COLOR, BG_COLOR, 0.8); + } else { + cr.set_source_rgba (BG_COLOR , BG_COLOR , BG_COLOR , 0.5); + shadow_effect.shadow_opacity = 200; + } + } else { + if (drag_action.dragging) { + cr.set_source_rgba (255, 255, 255, 0.8); + } else { + cr.set_source_rgba (255, 255, 255, 0.3); + shadow_effect.shadow_opacity = 100; + } + } + if (drag_action.dragging) { - const double BG_COLOR = 53.0 / 255.0; - cr.set_source_rgba (BG_COLOR, BG_COLOR, BG_COLOR, 0.7); + shadow_effect.css_class = "workspace-switcher-dnd"; } else { - cr.set_source_rgba (0, 0, 0, 0.1); + shadow_effect.css_class = "workspace-switcher"; } + add_effect (shadow_effect); cr.fill_preserve (); - cr.set_line_width (InternalUtils.scale_to_int (1, scale_factor)); - - var grad = new Cairo.Pattern.linear (0, 0, 0, height); - grad.add_color_stop_rgba (0.8, 0, 0, 0, 0); - grad.add_color_stop_rgba (1.0, 1, 1, 1, 0.1); - - cr.set_source (grad); - cr.stroke (); - - Drawing.Utilities.cairo_rounded_rectangle ( - cr, - 1.5 * scale_factor, - 1.5 * scale_factor, - (int) width - InternalUtils.scale_to_int (3, scale_factor), - (int) height - InternalUtils.scale_to_int (3, scale_factor), - InternalUtils.scale_to_int (5, scale_factor) - ); - - cr.set_source_rgba (0, 0, 0, 0.3); - cr.stroke (); - // it's not safe to to call meta_workspace_index() here, we may be still animating something // while the workspace is already gone, which would result in a crash. unowned Meta.WorkspaceManager manager = workspace.get_display ().get_workspace_manager (); @@ -292,27 +295,36 @@ namespace Gala { var offset = scaled_size / 2 - InternalUtils.scale_to_int (PLUS_WIDTH, scale_factor) / 2; buffer.context.rectangle ( - InternalUtils.scale_to_int (PLUS_WIDTH / 2, scale_factor) - InternalUtils.scale_to_int (PLUS_SIZE / 2, scale_factor) + 0.5 + offset, - 0.5 + offset, - InternalUtils.scale_to_int (PLUS_SIZE, scale_factor) - 1, - InternalUtils.scale_to_int (PLUS_WIDTH, scale_factor) - 1 + InternalUtils.scale_to_int (PLUS_WIDTH / 2, scale_factor) - InternalUtils.scale_to_int (PLUS_SIZE / 2, scale_factor) + offset, + offset, + InternalUtils.scale_to_int (PLUS_SIZE, scale_factor), + InternalUtils.scale_to_int (PLUS_WIDTH, scale_factor) ); - buffer.context.rectangle (0.5 + offset, - InternalUtils.scale_to_int (PLUS_WIDTH / 2, scale_factor) - InternalUtils.scale_to_int (PLUS_SIZE / 2, scale_factor) + 0.5 + offset, - InternalUtils.scale_to_int (PLUS_WIDTH, scale_factor) - 1, - InternalUtils.scale_to_int (PLUS_SIZE, scale_factor) - 1 + buffer.context.rectangle (offset, + InternalUtils.scale_to_int (PLUS_WIDTH / 2, scale_factor) - InternalUtils.scale_to_int (PLUS_SIZE / 2, scale_factor) + offset, + InternalUtils.scale_to_int (PLUS_WIDTH, scale_factor), + InternalUtils.scale_to_int (PLUS_SIZE, scale_factor) ); - buffer.context.set_source_rgb (0, 0, 0); - buffer.context.fill_preserve (); - buffer.exponential_blur (5); - - buffer.context.set_source_rgb (1, 1, 1); - buffer.context.set_line_width (1); - buffer.context.stroke_preserve (); + if (granite_settings.prefers_color_scheme == DARK) { + buffer.context.move_to (0, 1 * scale_factor); + buffer.context.set_source_rgb (0, 0, 0); + buffer.context.fill_preserve (); + buffer.exponential_blur (2); + + buffer.context.move_to (0, 0); + buffer.context.set_source_rgba (1, 1, 1, 0.95); + } else { + buffer.context.move_to (0, 1 * scale_factor); + buffer.context.set_source_rgba (1, 1, 1, 0.4); + buffer.context.fill_preserve (); + buffer.exponential_blur (1); + + buffer.context.move_to (0, 0); + buffer.context.set_source_rgba (0, 0, 0, 0.7); + } - buffer.context.set_source_rgb (0.8, 0.8, 0.8); buffer.context.fill (); cr.set_source_surface (buffer.surface, 0, 0); diff --git a/src/Widgets/MultitaskingView.vala b/src/Widgets/MultitaskingView.vala index e93a64ba4..6b548471d 100644 --- a/src/Widgets/MultitaskingView.vala +++ b/src/Widgets/MultitaskingView.vala @@ -41,8 +41,10 @@ namespace Gala { private Clutter.Actor workspaces; private Clutter.Actor dock_clones; private Clutter.Actor primary_monitor_container; + private Clutter.BrightnessContrastEffect brightness_effect; private GLib.Settings gala_behavior_settings; + private Granite.Settings granite_settings; private bool switching_workspace_with_gesture = false; private bool switching_workspace_in_progress { @@ -57,6 +59,7 @@ namespace Gala { construct { gala_behavior_settings = new GLib.Settings ("org.pantheon.desktop.gala.behavior"); + granite_settings = Granite.Settings.get_default (); visible = false; reactive = true; @@ -80,6 +83,15 @@ namespace Gala { dock_clones = new Clutter.Actor (); + brightness_effect = new Clutter.BrightnessContrastEffect (); + update_brightness_effect (); + + var blurred_bg = new BackgroundManager (wm, display.get_primary_monitor ()); + blurred_bg.add_effect (new BlurEffect (blurred_bg, 18)); + blurred_bg.add_effect (brightness_effect); + + add_child (blurred_bg); + // Create a child container that will be sized to fit the primary monitor, to contain the "main" // multitasking view UI. The Clutter.Actor of this class has to be allowed to grow to the size of the // stage as it contains MonitorClones for each monitor. @@ -137,6 +149,16 @@ namespace Gala { return Source.REMOVE; }); }); + + granite_settings.notify["prefers-color-scheme"].connect (update_brightness_effect); + } + + private void update_brightness_effect () { + if (granite_settings.prefers_color_scheme == DARK) { + brightness_effect.set_brightness (-0.4f); + } else { + brightness_effect.set_brightness (0.4f); + } } /** diff --git a/src/meson.build b/src/meson.build index b7a470fcb..c1d56ef18 100644 --- a/src/meson.build +++ b/src/meson.build @@ -24,6 +24,7 @@ gala_bin_sources = files( 'Background/BackgroundContainer.vala', 'Background/BackgroundManager.vala', 'Background/BackgroundSource.vala', + 'Background/BlurEffect.vala', 'Background/SystemBackground.vala', 'ColorFilters/ColorblindnessCorrectionEffect.vala', 'ColorFilters/FilterManager.vala',