Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ singularity_core_sources = files(
'src/core/dynamic_wallpaper.vala',
'src/core/dynamic_wallpaper_heic.vala',
'src/core/dynamic_wallpaper_controller.vala',
'src/core/wallpaper_effects.vala',
'src/core/wallpaper_collections.vala',
'src/core/wallpaper_gallery.vala',
'src/core/wallpaper_rotation_state.vala',
Expand Down Expand Up @@ -980,6 +981,13 @@ dynamic_wallpaper_test = executable('dynamic-wallpaper-test',
dependencies: [dependency('gobject-2.0'), dependency('gio-2.0'), dependency('gdk-pixbuf-2.0'), json_dep, libsingularity_dep],
)
test('dynamic-wallpaper', dynamic_wallpaper_test)
wallpaper_effects_test = executable('wallpaper-effects-test',
sources: ['src/core/wallpaper_effects.vala', 'tests/wallpaper_effects_test.vala'],
dependencies: core_deps + [gee_dep, cc.find_library('m')],
)
test('wallpaper-effects', wallpaper_effects_test,
depends: wallpaper_test_schema,
env: wallpaper_test_env)

settings_safety_test = executable('settings-safety-test',
sources: ['src/core/settings_safety.vala', 'tests/settings_safety_test.vala'],
Expand Down
28 changes: 28 additions & 0 deletions src/components/sidebar/pages/desktop_page.vala
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,34 @@ namespace Singularity {
});
grid_group.add_row(attribution_position_row);

var wallpaper_effect_options = new Gee.ArrayList<Singularity.Core.AppSettingOption>();
wallpaper_effect_options.add(new Singularity.Core.AppSettingOption() { id = "none", label = _("None") });
wallpaper_effect_options.add(new Singularity.Core.AppSettingOption() { id = "grayscale", label = _("Grayscale") });
wallpaper_effect_options.add(new Singularity.Core.AppSettingOption() { id = "blur", label = _("Blur") });
string current_effect = settings.get_string("wallpaper-effect");
var wallpaper_effect_row = new SelectionRow.with_options(
_("Wallpaper Effect"), wallpaper_effect_options, current_effect);
grid_group.add_row(wallpaper_effect_row);

var blur_options = new Gee.ArrayList<Singularity.Core.AppSettingOption>();
blur_options.add(new Singularity.Core.AppSettingOption() { id = "4", label = _("Light") });
blur_options.add(new Singularity.Core.AppSettingOption() { id = "8", label = _("Medium") });
blur_options.add(new Singularity.Core.AppSettingOption() { id = "16", label = _("Strong") });
string current_radius = settings.get_int("wallpaper-effect-blur-radius").to_string();
var blur_row = new SelectionRow.with_options(_("Blur Strength"), blur_options, current_radius);
blur_row.visible = current_effect == "blur";
blur_row.selected.connect((id) => {
int radius;
if (int.try_parse(id, out radius))
settings.set_int("wallpaper-effect-blur-radius", radius);
});
grid_group.add_row(blur_row);

wallpaper_effect_row.selected.connect((id) => {
settings.set_string("wallpaper-effect", id);
blur_row.visible = id == "blur";
});

var interval_options = new Gee.ArrayList<Singularity.Core.AppSettingOption>();
interval_options.add(new Singularity.Core.AppSettingOption() { id = "600", label = _("Every 10 minutes") });
interval_options.add(new Singularity.Core.AppSettingOption() { id = "1800", label = _("Every 30 minutes") });
Expand Down
103 changes: 103 additions & 0 deletions src/core/wallpaper_effects.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using GLib;

namespace Singularity {

public class WallpaperEffects : Object {
public const int MAX_BLUR_RADIUS = 64;

public static Gdk.Pixbuf apply(Gdk.Pixbuf source, string effect, int blur_radius) {
switch (effect) {
case "grayscale":
return grayscale(source);
case "blur":
return blur(source, blur_radius);
default:
return source;
}
}

public static Gdk.Pixbuf grayscale(Gdk.Pixbuf source) {
var output = source.copy();
unowned uint8[] pixels = output.get_pixels_with_length();
int channels = output.get_n_channels();
int stride = output.get_rowstride();
for (int y = 0; y < output.get_height(); y++) {
for (int x = 0; x < output.get_width(); x++) {
int i = y * stride + x * channels;
double luma = 0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2];
uint8 value = (uint8) int.min(255, (int) Math.round(luma));
pixels[i] = value;
pixels[i + 1] = value;
pixels[i + 2] = value;
}
}
return output;
}

public static Gdk.Pixbuf blur(Gdk.Pixbuf source, int radius) {
radius = radius.clamp(1, MAX_BLUR_RADIUS);
var output = source;
for (int pass = 0; pass < 3; pass++) {
output = box_blur(output, radius);
}
return output;
}

private static Gdk.Pixbuf box_blur(Gdk.Pixbuf source, int radius) {
int width = source.get_width();
int height = source.get_height();
int channels = source.get_n_channels();
var horizontal = source.copy();
var output = source.copy();
unowned uint8[] src = source.get_pixels_with_length();
unowned uint8[] mid = horizontal.get_pixels_with_length();
unowned uint8[] dst = output.get_pixels_with_length();
int src_stride = source.get_rowstride();
int mid_stride = horizontal.get_rowstride();
int dst_stride = output.get_rowstride();

for (int y = 0; y < height; y++) {
for (int c = 0; c < channels; c++) {
int count = int.min(width - 1, radius) + 1;
int sum = 0;
for (int x = 0; x < count; x++) sum += src[y * src_stride + x * channels + c];
for (int x = 0; x < width; x++) {
mid[y * mid_stride + x * channels + c] = (uint8) (sum / count);
int leaving = x - radius;
int entering = x + radius + 1;
if (leaving >= 0) {
sum -= src[y * src_stride + leaving * channels + c];
count--;
}
if (entering < width) {
sum += src[y * src_stride + entering * channels + c];
count++;
}
}
}
}

for (int x = 0; x < width; x++) {
for (int c = 0; c < channels; c++) {
int count = int.min(height - 1, radius) + 1;
int sum = 0;
for (int y = 0; y < count; y++) sum += mid[y * mid_stride + x * channels + c];
for (int y = 0; y < height; y++) {
dst[y * dst_stride + x * channels + c] = (uint8) (sum / count);
int leaving = y - radius;
int entering = y + radius + 1;
if (leaving >= 0) {
sum -= mid[leaving * mid_stride + x * channels + c];
count--;
}
if (entering < height) {
sum += mid[entering * mid_stride + x * channels + c];
count++;
}
}
}
}
return output;
}
}
}
16 changes: 15 additions & 1 deletion src/core/wallpaper_manager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Singularity {
private int _load_serial = 0;
private Mutex _mutex = Mutex ();
private WallpaperRotator? rotator = null;
private string _cached_effect_signature = "";

// Attribution metadata for the current wallpaper. Mirrors the
// dev.sinty.desktop gschema keys background-attribution-title
Expand Down Expand Up @@ -93,6 +94,10 @@ namespace Singularity {
settings.changed["background-attribution-title"].connect(() => schedule_reload());
if (schema.has_key("background-attribution-author"))
settings.changed["background-attribution-author"].connect(() => schedule_reload());
if (schema.has_key("wallpaper-effect"))
settings.changed["wallpaper-effect"].connect(() => schedule_reload());
if (schema.has_key("wallpaper-effect-blur-radius"))
settings.changed["wallpaper-effect-blur-radius"].connect(() => schedule_reload());
}
DynamicWallpaperController.get_default();
reload();
Expand Down Expand Up @@ -148,6 +153,11 @@ namespace Singularity {
string custom_uri = settings.get_string("background-picture-uri");
string? path = resolve_path(custom_uri);
if (path != null && DynamicWallpaper.is_dynamic_path(path)) return;
string effect = schema != null && schema.has_key("wallpaper-effect")
? settings.get_string("wallpaper-effect") : "none";
int blur_radius = schema != null && schema.has_key("wallpaper-effect-blur-radius")
? settings.get_int("wallpaper-effect-blur-radius") : 8;
string effect_signature = "%s|%d".printf(effect, blur_radius);
if (path == null) {
string[] fallbacks = {};
foreach (unowned string d in GLib.Environment.get_system_data_dirs()) {
Expand Down Expand Up @@ -178,7 +188,7 @@ namespace Singularity {
}
}
if (path != null) {
if (path == _cached_path) {
if (path == _cached_path && effect_signature == _cached_effect_signature) {
// Wallpaper file unchanged; if only the attribution
// metadata moved (URI stays the same but the keys
// were updated), the overlay widget still needs to
Expand All @@ -189,6 +199,7 @@ namespace Singularity {
return;
}
_cached_path = path;
_cached_effect_signature = effect_signature;
wallpaper_path = path;

int serial;
Expand Down Expand Up @@ -230,6 +241,9 @@ namespace Singularity {
pb_medium = ensure_alpha(pb_medium);
pb_small = ensure_alpha(pb_small);

if (pb_display != null) pb_display = WallpaperEffects.apply(pb_display, effect, blur_radius);
if (pb_medium != null) pb_medium = WallpaperEffects.apply(pb_medium, effect, int.max(1, blur_radius / 4));

_mutex.lock();
bool stale = (serial != _load_serial);
_mutex.unlock();
Expand Down
12 changes: 12 additions & 0 deletions tests/dev.sinty.desktop.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,17 @@
<schemalist>
<schema id="dev.sinty.desktop" path="/dev/sinty/desktop/">
<key name="wallpaper-favorites" type="as"><default>[]</default></key>
<key name="wallpaper-effect" type="s">
<choices>
<choice value="none"/>
<choice value="grayscale"/>
<choice value="blur"/>
</choices>
<default>'none'</default>
</key>
<key name="wallpaper-effect-blur-radius" type="i">
<range min="1" max="64"/>
<default>8</default>
</key>
</schema>
</schemalist>
68 changes: 68 additions & 0 deletions tests/wallpaper_effects_test.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using GLib;
using Gdk;
using Singularity;

private Pixbuf solid(int width, int height, uint8 r, uint8 g, uint8 b) {
var pixbuf = new Pixbuf(Colorspace.RGB, true, 8, width, height);
unowned uint8[] pixels = pixbuf.get_pixels_with_length();
int stride = pixbuf.get_rowstride();
int channels = pixbuf.get_n_channels();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int i = y * stride + x * channels;
pixels[i] = r;
pixels[i + 1] = g;
pixels[i + 2] = b;
pixels[i + 3] = 255;
}
}
return pixbuf;
}

private void test_settings_roundtrip() {
var schema = SettingsSchemaSource.get_default().lookup("dev.sinty.desktop", true);
assert(schema != null);
var settings = new GLib.Settings.full(schema, SettingsBackend.memory_settings_backend_new(), null);
assert(settings.set_string("wallpaper-effect", "blur"));
assert(settings.set_int("wallpaper-effect-blur-radius", 16));
assert(settings.get_string("wallpaper-effect") == "blur");
assert(settings.get_int("wallpaper-effect-blur-radius") == 16);
}

private void test_grayscale() {
var result = WallpaperEffects.grayscale(solid(2, 2, 230, 40, 90));
uint8[] pixels = result.get_pixels_with_length();
assert(pixels[0] == pixels[1] && pixels[1] == pixels[2]);
assert(pixels[0] != 230);
}

private void test_blur() {
var pixbuf = solid(9, 3, 0, 0, 0);
unowned uint8[] pixels = pixbuf.get_pixels_with_length();
int stride = pixbuf.get_rowstride();
int channels = pixbuf.get_n_channels();
for (int y = 0; y < 3; y++) {
for (int x = 5; x < 9; x++) {
int i = y * stride + x * channels;
pixels[i] = pixels[i + 1] = pixels[i + 2] = 255;
}
}
var result = WallpaperEffects.blur(pixbuf, 1);
uint8[] blurred = result.get_pixels_with_length();
int edge = stride + 5 * channels;
assert(blurred[edge] > 0 && blurred[edge] < 255);
}

private void test_none_keeps_source() {
var pixbuf = solid(2, 2, 10, 20, 30);
assert(WallpaperEffects.apply(pixbuf, "none", 8) == pixbuf);
}

public int main(string[] args) {
Test.init(ref args);
Test.add_func("/wallpaper-effects/grayscale", test_grayscale);
Test.add_func("/wallpaper-effects/blur", test_blur);
Test.add_func("/wallpaper-effects/none", test_none_keeps_source);
Test.add_func("/wallpaper-effects/settings-roundtrip", test_settings_roundtrip);
return Test.run();
}
Loading