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
env:
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:
- name: Install packages
run: |

View File

@ -12,7 +12,7 @@ arch=(
'armv7h' # ARM v7 hardfloat
)
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")
provides=("swaync" "swaync-client" "notification-daemon")
makedepends=("vala>=0.56" meson git scdoc sassc)

View File

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

View File

@ -19,6 +19,7 @@ assert(sassc.found())
style_css = custom_target(
'SCSS Compilation',
build_by_default: true,
build_always_stale: true,
input : 'style/style.scss',
output : 'style.css',
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 {
/* The parent to all players */
}
.widget-mpris-player {
padding: 8px;
margin: 8px;
}
.widget-mpris-title {
font-weight: bold;
font-size: 1.25rem;
}
.widget-mpris-subtitle {
font-size: 1.1rem;
.widget-mpris-player {
padding: 8px;
padding: 16px;
margin: 16px 20px;
background-color: #{"@mpris-album-art-overlay"};
border-radius: $border-radius;
box-shadow: $mpris-shadow;
button:hover {
/* The media player buttons (play, pause, next, etc...) */
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": {
"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
},
"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 {
int image_size;
int image_radius;
bool blur;
}
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.";
HashTable<string, MprisPlayer> players = new HashTable<string, MprisPlayer> (str_hash, str_equal);
@ -26,6 +29,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
Config mpris_config = Config () {
image_size = 96,
image_radius = 12,
blur = true,
};
public Mpris (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
@ -56,6 +60,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
#if HAVE_LATEST_LIBHANDY
carousel.allow_scroll_wheel = true;
#endif
carousel.draw.connect (carousel_draw_cb);
carousel.page_changed.connect ((index) => {
GLib.List<weak Gtk.Widget> children = carousel.get_children ();
int children_length = (int) children.length ();
@ -91,6 +96,11 @@ namespace SwayNotificationCenter.Widgets.Mpris {
// Clamp the radius
mpris_config.image_radius = mpris_config.image_radius.clamp (
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 ();
@ -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.
* 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="use-fallback">True</property>
<property name="icon_size">0</property>
<style>
<class name="widget-mpris-album-art"/>
</style>
</object>
<packing>
<property name="expand">False</property>

View File

@ -38,6 +38,9 @@ namespace SwayNotificationCenter.Widgets.Mpris {
private string prev_art_url;
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;
public MprisPlayer (MprisSource source, Config mpris_config) {
@ -99,6 +102,77 @@ namespace SwayNotificationCenter.Widgets.Mpris {
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,
HashTable<string, Variant> changed,
string[] invalid) {
@ -246,7 +320,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
int scale = get_style_context ().get_scale ();
Gdk.Pixbuf ? pixbuf = null;
// Cancel previous download, reset the state and download again
album_art_cancellable.cancel ();
album_art_cancellable.reset ();
@ -255,24 +328,45 @@ namespace SwayNotificationCenter.Widgets.Mpris {
InputStream stream = yield file.read_async (Priority.DEFAULT,
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);
} catch (Error e) {
debug ("Could not download album art for %s. Using fallback...",
source.media_player.identity);
}
if (pixbuf != null) {
var surface = Functions.scale_round_pixbuf (pixbuf,
mpris_config.image_size,
mpris_config.image_size,
scale,
mpris_config.image_radius);
pixbuf = Gdk.pixbuf_get_from_surface (surface,
0, 0,
if (this.album_art_pixbuf != null) {
unowned Gtk.StyleContext style_ctx = album_art.get_style_context ();
Value br_value =
style_ctx.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS,
style_ctx.get_state ());
int radius = 0;
if (!br_value.holds (Type.INT) ||
(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);
album_art.set_from_pixbuf (pixbuf);
mpris_config.image_size,
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);
this.queue_draw ();
return;
}
}

View File

@ -209,12 +209,12 @@ namespace SwayNotificationCenter {
return type;
}
/** 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) {
/** Roundes the Cairo Surface to the given radii */
public static Cairo.Surface round_surface (Cairo.Surface base_surf,
int buffer_width,
int buffer_height,
int img_scale,
int radius) {
// Limit radii size
radius = int.min (radius, int.min (buffer_width / 2, buffer_height / 2));
@ -236,7 +236,26 @@ namespace SwayNotificationCenter {
cr.paint ();
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,
null);
int width = pixbuf.width / img_scale;
@ -246,22 +265,40 @@ namespace SwayNotificationCenter {
if (window_ratio > bg_ratio) { // Taller wallpaper than monitor
double scale = (double) buffer_width / width;
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 {
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
double scale = (double) buffer_height / height;
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 {
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.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;
}

View File

@ -75,6 +75,7 @@ app_deps = [
dependency('gtk+-3.0', version: '>= 3.22'),
dependency('json-glib-1.0', version: '>= 1.0'),
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('m', required : true),
meson.get_compiler('vala').find_library('posix'),