Add MPRIS player background blur (#355)

* Add MPRIS player background blur

* Added edge fade

* Only blur cover if the size or source changes
This commit is contained in:
Erik Reider 2023-12-19 18:40:23 +01:00 committed by GitHub
parent 1fcef5c966
commit ff04ca3924
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 269 additions and 38 deletions

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive
PACKAGES: meson libwayland-dev libgtk-3-dev gobject-introspection libgirepository1.0-dev valac libjson-glib-dev libhandy-1-dev libgtk-layer-shell-dev scdoc libgee-0.8-dev libpulse-dev sassc PACKAGES: meson libwayland-dev libgtk-3-dev gobject-introspection libgirepository1.0-dev valac libjson-glib-dev libhandy-1-dev libgtk-layer-shell-dev scdoc libgee-0.8-dev libpulse-dev sassc libgranite-dev
steps: steps:
- name: Install packages - name: Install packages
run: | run: |

View File

@ -12,7 +12,7 @@ arch=(
'armv7h' # ARM v7 hardfloat 'armv7h' # ARM v7 hardfloat
) )
license=('GPL3') license=('GPL3')
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" "gvfs" "libnotify") depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" "gvfs" "libnotify" "granite")
conflicts=("swaync" "swaync-client") conflicts=("swaync" "swaync-client")
provides=("swaync" "swaync-client" "notification-daemon") provides=("swaync" "swaync-client" "notification-daemon")
makedepends=("vala>=0.56" meson git scdoc sassc) makedepends=("vala>=0.56" meson git scdoc sassc)

View File

@ -29,6 +29,7 @@ BuildRequires: pkgconfig(libpulse)
BuildRequires: systemd-devel BuildRequires: systemd-devel
BuildRequires: systemd BuildRequires: systemd
BuildRequires: sassc BuildRequires: sassc
BuildRequires: granite-devel
Requires: gvfs Requires: gvfs
Requires: libnotify Requires: libnotify

View File

@ -19,6 +19,7 @@ assert(sassc.found())
style_css = custom_target( style_css = custom_target(
'SCSS Compilation', 'SCSS Compilation',
build_by_default: true, build_by_default: true,
build_always_stale: true,
input : 'style/style.scss', input : 'style/style.scss',
output : 'style.css', output : 'style.css',
install: true, install: true,

View File

@ -1,14 +1,47 @@
@define-color mpris-album-art-overlay rgba(0, 0, 0, 0.55);
@define-color mpris-button-hover rgba(0, 0, 0, 0.50);
$mpris-shadow: 0px 0px 10px rgba(0, 0, 0, 0.75);
.widget-mpris { .widget-mpris {
/* The parent to all players */ /* The parent to all players */
} .widget-mpris-player {
.widget-mpris-player { padding: 8px;
padding: 8px; padding: 16px;
margin: 8px; margin: 16px 20px;
} background-color: #{"@mpris-album-art-overlay"};
.widget-mpris-title { border-radius: $border-radius;
font-weight: bold; box-shadow: $mpris-shadow;
font-size: 1.25rem;
} button:hover {
.widget-mpris-subtitle { /* The media player buttons (play, pause, next, etc...) */
font-size: 1.1rem; background: #{"@noti-bg-hover"};
}
.widget-mpris-album-art {
border-radius: $border-radius;
box-shadow: $mpris-shadow;
}
.widget-mpris-title {
font-weight: bold;
font-size: 1.25rem;
}
.widget-mpris-subtitle {
font-size: 1.1rem;
}
& > box > button {
/* Change player control buttons */
&:hover {
background-color: #{"@mpris-button-hover"};
}
}
}
& > box > button {
/* Change player side buttons */
}
& > box > button:disabled {
/* Change player side buttons insensitive */
}
} }

View File

@ -402,8 +402,14 @@
}, },
"image-radius": { "image-radius": {
"type": "integer", "type": "integer",
"description": "The border radius of the album art", "description": "The border radius of the album art. Will be overriden by setting the border-radius in the style.css for the \".widget-mpris-album-art\" class",
"default": 12 "default": 12
},
"blur": {
"type": "bool",
"description": "Appy the artwork as the MPRIS background and blur it",
"default": true
} }
} }
}, },

View File

@ -2,6 +2,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
public struct Config { public struct Config {
int image_size; int image_size;
int image_radius; int image_radius;
bool blur;
} }
public class Mpris : BaseWidget { public class Mpris : BaseWidget {
@ -11,6 +12,8 @@ namespace SwayNotificationCenter.Widgets.Mpris {
} }
} }
private const int FADE_WIDTH = 20;
const string MPRIS_PREFIX = "org.mpris.MediaPlayer2."; const string MPRIS_PREFIX = "org.mpris.MediaPlayer2.";
HashTable<string, MprisPlayer> players = new HashTable<string, MprisPlayer> (str_hash, str_equal); HashTable<string, MprisPlayer> players = new HashTable<string, MprisPlayer> (str_hash, str_equal);
@ -26,6 +29,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
Config mpris_config = Config () { Config mpris_config = Config () {
image_size = 96, image_size = 96,
image_radius = 12, image_radius = 12,
blur = true,
}; };
public Mpris (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) { public Mpris (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
@ -56,6 +60,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
#if HAVE_LATEST_LIBHANDY #if HAVE_LATEST_LIBHANDY
carousel.allow_scroll_wheel = true; carousel.allow_scroll_wheel = true;
#endif #endif
carousel.draw.connect (carousel_draw_cb);
carousel.page_changed.connect ((index) => { carousel.page_changed.connect ((index) => {
GLib.List<weak Gtk.Widget> children = carousel.get_children (); GLib.List<weak Gtk.Widget> children = carousel.get_children ();
int children_length = (int) children.length (); int children_length = (int) children.length ();
@ -91,6 +96,11 @@ namespace SwayNotificationCenter.Widgets.Mpris {
// Clamp the radius // Clamp the radius
mpris_config.image_radius = mpris_config.image_radius.clamp ( mpris_config.image_radius = mpris_config.image_radius.clamp (
0, (int) (mpris_config.image_size * 0.5)); 0, (int) (mpris_config.image_size * 0.5));
// Get blur
bool blur_found;
bool? blur = get_prop<bool> (config, "blur", out blur_found);
if (blur_found) mpris_config.blur = blur;
} }
hide (); hide ();
@ -101,6 +111,51 @@ namespace SwayNotificationCenter.Widgets.Mpris {
} }
} }
private bool carousel_draw_cb (Cairo.Context cr) {
Gtk.Allocation alloc;
carousel.get_allocated_size (out alloc, null);
Cairo.Pattern left_fade_gradient = new Cairo.Pattern.linear (0, 0, 1, 0);
left_fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 1);
left_fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 0);
Cairo.Pattern right_fade_gradient = new Cairo.Pattern.linear (0, 0, 1, 0);
right_fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 0);
right_fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 1);
cr.save ();
cr.push_group ();
// Draw widgets
carousel.draw.disconnect (carousel_draw_cb);
carousel.draw (cr);
carousel.draw.connect (carousel_draw_cb);
/// Draw vertical fade
// Top fade
cr.save ();
cr.scale (FADE_WIDTH, alloc.height);
cr.rectangle (0, 0, FADE_WIDTH, alloc.height);
cr.set_source (left_fade_gradient);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
// Bottom fade
cr.save ();
cr.translate (alloc.width - FADE_WIDTH, 0);
cr.scale (FADE_WIDTH, alloc.height);
cr.rectangle (0, 0, FADE_WIDTH, alloc.height);
cr.set_source (right_fade_gradient);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
cr.pop_group_to_source ();
cr.paint ();
cr.restore ();
return true;
}
/** /**
* Forces the carousel to reload its style_context. * Forces the carousel to reload its style_context.
* Fixes carousel items not redrawing when window isn't visible. * Fixes carousel items not redrawing when window isn't visible.

View File

@ -22,6 +22,9 @@
<property name="icon-name">audio-x-generic-symbolic</property> <property name="icon-name">audio-x-generic-symbolic</property>
<property name="use-fallback">True</property> <property name="use-fallback">True</property>
<property name="icon_size">0</property> <property name="icon_size">0</property>
<style>
<class name="widget-mpris-album-art"/>
</style>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>

View File

@ -38,6 +38,9 @@ namespace SwayNotificationCenter.Widgets.Mpris {
private string prev_art_url; private string prev_art_url;
private DesktopAppInfo ? desktop_entry = null; private DesktopAppInfo ? desktop_entry = null;
private Gdk.Pixbuf ? album_art_pixbuf = null;
private Granite.Drawing.BufferSurface ? blurred_cover_surface = null;
private unowned Config mpris_config; private unowned Config mpris_config;
public MprisPlayer (MprisSource source, Config mpris_config) { public MprisPlayer (MprisSource source, Config mpris_config) {
@ -99,6 +102,77 @@ namespace SwayNotificationCenter.Widgets.Mpris {
source.properties_changed.disconnect (properties_changed); source.properties_changed.disconnect (properties_changed);
} }
public override bool draw (Cairo.Context cr) {
unowned Gdk.Window ? window = get_window ();
if (!mpris_config.blur || window == null ||
!(album_art_pixbuf is Gdk.Pixbuf)) {
return base.draw (cr);
}
const double DEGREES = Math.PI / 180.0;
unowned Gtk.StyleContext style_ctx = this.get_style_context ();
unowned Gtk.StateFlags state = style_ctx.get_state ();
Gtk.Border border = style_ctx.get_border (state);
Gtk.Border margin = style_ctx.get_margin (state);
Value radius_value = style_ctx.get_property (
Gtk.STYLE_PROPERTY_BORDER_RADIUS, state);
int radius = 0;
if (!radius_value.holds (Type.INT) ||
(radius = radius_value.get_int ()) == 0) {
radius = mpris_config.image_radius;
}
int scale = style_ctx.get_scale ();
int width = get_allocated_width ()
- margin.left - margin.right
- border.left - border.right;
int height = get_allocated_height ()
- margin.top - margin.bottom
- border.top - border.bottom;
cr.save ();
cr.new_sub_path ();
// Top Right
cr.arc (width - radius + margin.right,
radius + margin.top,
radius, -90 * DEGREES, 0 * DEGREES);
// Bottom Right
cr.arc (width - radius + margin.right,
height - radius + margin.bottom,
radius, 0 * DEGREES, 90 * DEGREES);
// Bottom Left
cr.arc (radius + margin.left,
height - radius + margin.bottom,
radius, 90 * DEGREES, 180 * DEGREES);
// Top Left
cr.arc (radius + margin.left,
radius + margin.top,
radius, 180 * DEGREES, 270 * DEGREES);
cr.close_path ();
cr.set_source_rgba (0, 0, 0, 0);
cr.clip ();
// Draw blurred player background
if (blurred_cover_surface == null
|| blurred_cover_surface.width != width
|| blurred_cover_surface.height != height) {
var buffer = new Granite.Drawing.BufferSurface (width, height);
Cairo.Surface surface = Functions.scale_pixbuf (
album_art_pixbuf, width, height, scale);
buffer.context.set_source_surface (surface, 0, 0);
buffer.context.paint ();
buffer.fast_blur (8, 2);
blurred_cover_surface = buffer;
}
cr.set_source_surface (blurred_cover_surface.surface, margin.left, margin.top);
cr.paint ();
cr.restore ();
return base.draw (cr);
}
private void properties_changed (string iface, private void properties_changed (string iface,
HashTable<string, Variant> changed, HashTable<string, Variant> changed,
string[] invalid) { string[] invalid) {
@ -246,7 +320,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
int scale = get_style_context ().get_scale (); int scale = get_style_context ().get_scale ();
Gdk.Pixbuf ? pixbuf = null;
// Cancel previous download, reset the state and download again // Cancel previous download, reset the state and download again
album_art_cancellable.cancel (); album_art_cancellable.cancel ();
album_art_cancellable.reset (); album_art_cancellable.reset ();
@ -255,24 +328,45 @@ namespace SwayNotificationCenter.Widgets.Mpris {
InputStream stream = yield file.read_async (Priority.DEFAULT, InputStream stream = yield file.read_async (Priority.DEFAULT,
album_art_cancellable); album_art_cancellable);
pixbuf = yield new Gdk.Pixbuf.from_stream_async ( this.album_art_pixbuf = yield new Gdk.Pixbuf.from_stream_async (
stream, album_art_cancellable); stream, album_art_cancellable);
} catch (Error e) { } catch (Error e) {
debug ("Could not download album art for %s. Using fallback...", debug ("Could not download album art for %s. Using fallback...",
source.media_player.identity); source.media_player.identity);
} }
if (pixbuf != null) { if (this.album_art_pixbuf != null) {
var surface = Functions.scale_round_pixbuf (pixbuf, unowned Gtk.StyleContext style_ctx = album_art.get_style_context ();
mpris_config.image_size, Value br_value =
mpris_config.image_size, style_ctx.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS,
scale, style_ctx.get_state ());
mpris_config.image_radius); int radius = 0;
pixbuf = Gdk.pixbuf_get_from_surface (surface, if (!br_value.holds (Type.INT) ||
0, 0, (radius = br_value.get_int ()) == 0) {
radius = mpris_config.image_radius;
}
var surface = Functions.scale_pixbuf (this.album_art_pixbuf,
mpris_config.image_size, mpris_config.image_size,
mpris_config.image_size); mpris_config.image_size,
album_art.set_from_pixbuf (pixbuf); scale);
this.album_art_pixbuf = Gdk.pixbuf_get_from_surface (surface,
0, 0,
mpris_config.image_size,
mpris_config.image_size);
this.blurred_cover_surface = null;
surface = Functions.round_surface (surface,
mpris_config.image_size,
mpris_config.image_size,
scale,
radius);
var pix_buf = Gdk.pixbuf_get_from_surface (surface,
0, 0,
mpris_config.image_size,
mpris_config.image_size);
surface.finish ();
album_art.set_from_pixbuf (pix_buf);
album_art.get_style_context ().set_scale (1); album_art.get_style_context ().set_scale (1);
this.queue_draw ();
return; return;
} }
} }

View File

@ -209,12 +209,12 @@ namespace SwayNotificationCenter {
return type; return type;
} }
/** Scales the pixbuf to fit the given dimensions */ /** Roundes the Cairo Surface to the given radii */
public static Cairo.Surface scale_round_pixbuf (Gdk.Pixbuf pixbuf, public static Cairo.Surface round_surface (Cairo.Surface base_surf,
int buffer_width, int buffer_width,
int buffer_height, int buffer_height,
int img_scale, int img_scale,
int radius) { int radius) {
// Limit radii size // Limit radii size
radius = int.min (radius, int.min (buffer_width / 2, buffer_height / 2)); radius = int.min (radius, int.min (buffer_width / 2, buffer_height / 2));
@ -236,7 +236,26 @@ namespace SwayNotificationCenter {
cr.paint (); cr.paint ();
cr.save (); cr.save ();
Cairo.Surface scale_surf = Gdk.cairo_surface_create_from_pixbuf (pixbuf, cr.set_source_surface (base_surf, 0, 0);
cr.paint ();
cr.restore ();
return surface;
}
/** Scales the pixbuf to fit the given dimensions */
public static Cairo.Surface scale_pixbuf (Gdk.Pixbuf pixbuf,
int buffer_width,
int buffer_height,
int img_scale) {
Cairo.Surface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32,
buffer_width,
buffer_height);
var cr = new Cairo.Context (surface);
cr.save ();
Cairo.Surface base_surf = Gdk.cairo_surface_create_from_pixbuf (pixbuf,
img_scale, img_scale,
null); null);
int width = pixbuf.width / img_scale; int width = pixbuf.width / img_scale;
@ -246,22 +265,40 @@ namespace SwayNotificationCenter {
if (window_ratio > bg_ratio) { // Taller wallpaper than monitor if (window_ratio > bg_ratio) { // Taller wallpaper than monitor
double scale = (double) buffer_width / width; double scale = (double) buffer_width / width;
if (scale * height < buffer_height) { if (scale * height < buffer_height) {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf); draw_scale_wide (buffer_width, width, buffer_height, height, cr, base_surf);
} else { } else {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf); draw_scale_tall (buffer_width, width, buffer_height, height, cr, base_surf);
} }
} else { // Wider wallpaper than monitor } else { // Wider wallpaper than monitor
double scale = (double) buffer_height / height; double scale = (double) buffer_height / height;
if (scale * width < buffer_width) { if (scale * width < buffer_width) {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf); draw_scale_tall (buffer_width, width, buffer_height, height, cr, base_surf);
} else { } else {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf); draw_scale_wide (buffer_width, width, buffer_height, height, cr, base_surf);
} }
} }
cr.paint (); cr.paint ();
cr.restore (); cr.restore ();
scale_surf.finish (); base_surf.finish ();
return surface;
}
/** Scales the pixbuf to fit the given dimensions */
public static Cairo.Surface scale_round_pixbuf (Gdk.Pixbuf pixbuf,
int buffer_width,
int buffer_height,
int img_scale,
int radius) {
var surface = Functions.scale_pixbuf (pixbuf,
buffer_width,
buffer_height,
img_scale);
surface = Functions.round_surface (surface,
buffer_width,
buffer_height,
img_scale,
radius);
return surface; return surface;
} }

View File

@ -75,6 +75,7 @@ app_deps = [
dependency('gtk+-3.0', version: '>= 3.22'), dependency('gtk+-3.0', version: '>= 3.22'),
dependency('json-glib-1.0', version: '>= 1.0'), dependency('json-glib-1.0', version: '>= 1.0'),
dependency('libhandy-1', version: '>= 1.2.3'), dependency('libhandy-1', version: '>= 1.2.3'),
dependency('granite', version: '>= 6.2.0'),
meson.get_compiler('c').find_library('gtk-layer-shell'), meson.get_compiler('c').find_library('gtk-layer-shell'),
meson.get_compiler('c').find_library('m', required : true), meson.get_compiler('c').find_library('m', required : true),
meson.get_compiler('vala').find_library('posix'), meson.get_compiler('vala').find_library('posix'),