diff --git a/data/gala.gresource.xml b/data/gala.gresource.xml
index cc9d395a..727942e9 100644
--- a/data/gala.gresource.xml
+++ b/data/gala.gresource.xml
@@ -22,5 +22,7 @@
gala.css
+ shaders/colorblindness-correction.vert
+ shaders/monochrome.vert
diff --git a/data/gala.gschema.xml b/data/gala.gschema.xml
index ee21b517..d502591e 100644
--- a/data/gala.gschema.xml
+++ b/data/gala.gschema.xml
@@ -394,4 +394,38 @@
The action that corresponds to performing a horizontal swipe gesture with four fingers
+
+
+
+
+
+
+
+
+
+
+
+
+ "none"
+ Colorblind correction filter.
+
+
+
+
+ 1.0
+ The strength of colorblindness correction filter.
+
+
+
+ false
+ Enable monochrome filter.
+
+
+
+
+ 1.0
+ The strength of monochrome filter.
+
+
+
diff --git a/data/shaders/colorblindness-correction.vert b/data/shaders/colorblindness-correction.vert
new file mode 100644
index 00000000..e4da573e
--- /dev/null
+++ b/data/shaders/colorblindness-correction.vert
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022-2023 GdH
+ * Copyright 2023 elementary, Inc.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+uniform sampler2D tex;
+uniform int COLORBLIND_MODE;
+uniform float STRENGTH;
+void main() {
+ vec4 c = texture2D(tex, cogl_tex_coord0_in.xy);
+
+ // RGB to LMS matrix
+ float L = (17.8824f * c.r) + (43.5161f * c.g) + (4.11935f * c.b);
+ float M = (3.45565f * c.r) + (27.1554f * c.g) + (3.86714f * c.b);
+ float S = (0.0299566f * c.r) + (0.184309f * c.g) + (1.46709f * c.b);
+
+ float l, m, s;
+
+ // Remove invisible colors
+ if ( COLORBLIND_MODE == 1 || COLORBLIND_MODE == 2) { // Protanopia - reds are greatly reduced
+ l = 0.0f * L + 2.02344f * M + -2.52581f * S;
+ m = 0.0f * L + 1.0f * M + 0.0f * S;
+ s = 0.0f * L + 0.0f * M + 1.0f * S;
+ } else if ( COLORBLIND_MODE == 3 || COLORBLIND_MODE == 4) { // Deuteranopia - greens are greatly reduced
+ l = 1.0f * L + 0.0f * M + 0.0f * S;
+ m = 0.494207f * L + 0.0f * M + 1.24827f * S;
+ s = 0.0f * L + 0.0f * M + 1.0f * S;
+ } else if ( COLORBLIND_MODE == 5 ) { // Tritanopia - blues are greatly reduced (1 of 10 000)
+ l = 1.0f * L + 0.0f * M + 0.0f * S;
+ m = 0.0f * L + 1.0f * M + 0.0f * S;
+ // GdH - trinatopia vector calculated by me, all public sources were off
+ s = -0.012491378299329402f * L + 0.07203451899279534f * M + 0.0f * S;
+ }
+
+ // LMS to RGB matrix conversion
+ vec4 error;
+ error.r = (0.0809444479f * l) + (-0.130504409f * m) + (0.116721066f * s);
+ error.g = (-0.0102485335f * l) + (0.0540193266f * m) + (-0.113614708f * s);
+ error.b = (-0.000365296938f * l) + (-0.00412161469f * m) + (0.693511405f * s);
+ // The error is what they see
+
+ // ratio between original and error colors allows adjusting filter for weaker forms of dichromacy
+ error = error * STRENGTH + c * (1.0 - STRENGTH);
+ error.a = 1.0;
+
+ // Isolate invisible colors to color vision deficiency (calculate error matrix)
+ error = (c - error);
+
+ // Shift colors
+ vec4 correction;
+ // protanopia / protanomaly corrections
+ if ( COLORBLIND_MODE == 1 ) {
+ //(kwin effect values)
+ correction.r = error.r * 0.56667 + error.g * 0.43333 + error.b * 0.00000;
+ correction.g = error.r * 0.55833 + error.g * 0.44267 + error.b * 0.00000;
+ correction.b = error.r * 0.00000 + error.g * 0.24167 + error.b * 0.75833;
+ // tries to mimic Android, GdH
+ //correction.r = error.r * -0.5 + error.g * -0.3 + error.b * 0.0;
+ //correction.g = error.r * 0.2 + error.g * 0.0 + error.b * 0.0;
+ //correction.b = error.r * 0.2 + error.g * 1.0 + error.b * 1.0;
+ // protanopia / protanomaly high contrast G-R corrections
+ } else if ( COLORBLIND_MODE == 2 ) {
+ correction.r = error.r * 2.56667 + error.g * 0.43333 + error.b * 0.00000;
+ correction.g = error.r * 1.55833 + error.g * 0.44267 + error.b * 0.00000;
+ correction.b = error.r * 0.00000 + error.g * 0.24167 + error.b * 0.75833;
+ // deuteranopia / deuteranomaly corrections (tries to mimic Android, GdH)
+ } else if ( COLORBLIND_MODE == 3 ) {
+ correction.r = error.r * -0.7 + error.g * 0.0 + error.b * 0.0;
+ correction.g = error.r * 0.5 + error.g * 1.0 + error.b * 0.0;
+ correction.b = error.r * -0.3 + error.g * 0.0 + error.b * 1.0;
+ // deuteranopia / deuteranomaly high contrast R-G corrections
+ } else if ( COLORBLIND_MODE == 4 ) {
+ correction.r = error.r * -1.5 + error.g * 1.5 + error.b * 0.0;
+ correction.g = error.r * -1.5 + error.g * 1.5 + error.b * 0.0;
+ correction.b = error.r * 1.5 + error.g * 0.0 + error.b * 0.0;
+ // tritanopia / tritanomaly corrections (GdH)
+ } else if ( COLORBLIND_MODE == 5 ) {
+ correction.r = error.r * 0.3 + error.g * 0.5 + error.b * 0.4;
+ correction.g = error.r * 0.5 + error.g * 0.7 + error.b * 0.3;
+ correction.b = error.r * 0.0 + error.g * 0.0 + error.b * 1.0;
+ }
+
+ // Add compensation to original values
+ correction = c + correction;
+ correction.a = c.a;
+
+ cogl_color_out = correction;
+}
diff --git a/data/shaders/monochrome.vert b/data/shaders/monochrome.vert
new file mode 100644
index 00000000..f1792937
--- /dev/null
+++ b/data/shaders/monochrome.vert
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 elementary, Inc.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+uniform sampler2D tex;
+uniform float STRENGTH;
+void main() {
+ vec2 uv = cogl_tex_coord0_in.xy;
+ vec4 sample = texture2D (tex, uv);
+ vec3 luminance = vec3 (0.2126, 0.7512, 0.0722);
+ float gray = luminance.r * sample.r + luminance.g * sample.g + luminance.b * sample.b;
+ cogl_color_out = vec4 (
+ sample.r * (1.0 - STRENGTH) + gray * STRENGTH,
+ sample.g * (1.0 - STRENGTH) + gray * STRENGTH,
+ sample.b * (1.0 - STRENGTH) + gray * STRENGTH,
+ sample.a
+ ) ;
+}
diff --git a/src/ColorFilters/ColorblindnessCorrectionEffect.vala b/src/ColorFilters/ColorblindnessCorrectionEffect.vala
new file mode 100644
index 00000000..8ed14618
--- /dev/null
+++ b/src/ColorFilters/ColorblindnessCorrectionEffect.vala
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 elementary, Inc.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+public class Gala.ColorblindnessCorrectionEffect : Clutter.ShaderEffect {
+ public const string EFFECT_NAME = "colorblindness-correction-filter";
+
+ private int _mode;
+ public int mode {
+ get { return _mode; }
+ construct set {
+ _mode = value;
+ set_uniform_value ("COLORBLIND_MODE", _mode);
+ }
+ }
+ private double _strength;
+ public double strength {
+ get { return _strength; }
+ construct set {
+ _strength = value;
+
+ set_uniform_value ("STRENGTH", value);
+ queue_repaint ();
+ }
+ }
+
+ /*
+ * Used for fading in and out the effect, since you can't add transitions to effects.
+ */
+ public Clutter.Actor? transition_actor { get; set; default = null; }
+
+ public ColorblindnessCorrectionEffect (int mode, double strength) {
+ Object (
+ shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
+ mode: mode,
+ strength: strength
+ );
+
+ try {
+ var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/colorblindness-correction.vert", GLib.ResourceLookupFlags.NONE);
+ set_shader_source ((string) bytes.get_data ());
+ } catch (Error e) {
+ critical ("Unable to load colorblindness-correction.vert: %s", e.message);
+ }
+ }
+}
diff --git a/src/ColorFilters/FilterManager.vala b/src/ColorFilters/FilterManager.vala
new file mode 100644
index 00000000..d3eda7ed
--- /dev/null
+++ b/src/ColorFilters/FilterManager.vala
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2023 elementary, Inc.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+public class Gala.FilterManager : Object {
+ private const string TRANSITION_NAME = "strength-transition";
+ private const int TRANSITION_DURATION = 500;
+
+ private static FilterManager instance;
+ private static GLib.Settings settings;
+ public WindowManager wm { get; construct; }
+
+ public static void init (WindowManager wm) {
+ if (instance != null) {
+ return;
+ }
+
+ instance = new FilterManager (wm);
+ }
+
+ private FilterManager (WindowManager wm) {
+ Object (wm: wm);
+ }
+
+ static construct {
+ settings = new GLib.Settings ("io.elementary.desktop.wm.accessibility");
+ }
+
+ construct {
+ settings.changed["colorblindness-correction-filter"].connect (update_colorblindness_filter);
+ settings.changed["colorblindness-correction-filter-strength"].connect (update_colorblindness_strength);
+ settings.changed["enable-monochrome-filter"].connect (update_monochrome_filter);
+ settings.changed["monochrome-filter-strength"].connect (update_monochrome_strength);
+
+ load_filters ();
+ }
+
+ private void load_filters () {
+ load_colorblindness_filter ();
+ load_monochrome_filter ();
+ }
+
+ private void load_colorblindness_filter () {
+ // When gala launches and there is an effect active, it shouldn't be faded in
+ var filter_variant = settings.get_enum ("colorblindness-correction-filter");
+ var strength = settings.get_double ("colorblindness-correction-filter-strength");
+ if (filter_variant != 0 && strength > 0.0) {
+ wm.stage.add_effect_with_name (
+ ColorblindnessCorrectionEffect.EFFECT_NAME,
+ new ColorblindnessCorrectionEffect (filter_variant, strength)
+ );
+ }
+ }
+
+ private void update_colorblindness_filter () {
+ var filter_variant = settings.get_enum ("colorblindness-correction-filter");
+ var strength = settings.get_double ("colorblindness-correction-filter-strength");
+
+ // Fade out applied effects
+ foreach (unowned var _effect in wm.stage.get_effects ()) {
+ if (_effect is ColorblindnessCorrectionEffect) {
+ unowned var effect = (ColorblindnessCorrectionEffect) _effect;
+
+ if (effect.mode == filter_variant) {
+ continue;
+ }
+
+ // Since you can't add a transition to an effect
+ // add it to a transition actor and bind one of its properties to the effect
+
+ // stop transition (if there is one in progress)
+ if (effect.transition_actor != null) {
+ effect.transition_actor.destroy ();
+ }
+
+ // create a new transition
+ var transition = new Clutter.PropertyTransition ("scale_x") {
+ duration = TRANSITION_DURATION,
+ progress_mode = Clutter.AnimationMode.LINEAR,
+ remove_on_complete = true
+ };
+ transition.set_from_value (effect.strength);
+ transition.set_to_value (0.0);
+
+ // create a transition actor and bind its `scale_x` to effect's `strength`
+ effect.transition_actor = new Clutter.Actor () {
+ visible = false
+ };
+ wm.ui_group.add_child (effect.transition_actor);
+ effect.transition_actor.bind_property ("scale_x", effect, "strength");
+
+ transition.completed.connect (() => {
+ effect.transition_actor.destroy ();
+ wm.stage.remove_effect (effect);
+ });
+
+ effect.transition_actor.add_transition (TRANSITION_NAME, transition);
+ }
+ }
+
+ // Apply a new filter
+ if (filter_variant == 0 || strength == 0.0) {
+ return;
+ }
+
+ var new_effect = new ColorblindnessCorrectionEffect (filter_variant, 0.0);
+ wm.stage.add_effect_with_name (ColorblindnessCorrectionEffect.EFFECT_NAME, new_effect);
+
+ // Transition new effect in the same way
+ var new_transition = new Clutter.PropertyTransition ("scale_x") {
+ duration = TRANSITION_DURATION,
+ progress_mode = Clutter.AnimationMode.LINEAR,
+ remove_on_complete = true
+ };
+ new_transition.set_from_value (0.0);
+ new_transition.set_to_value (strength);
+
+ new_effect.transition_actor = new Clutter.Actor () {
+ visible = false
+ };
+ wm.ui_group.add_child (new_effect.transition_actor);
+ new_effect.transition_actor.bind_property ("scale_x", new_effect, "strength");
+
+ new_transition.completed.connect (() => {
+ new_effect.transition_actor.destroy ();
+ });
+
+ new_effect.transition_actor.add_transition (TRANSITION_NAME, new_transition);
+ }
+
+ private void update_colorblindness_strength () {
+ var filter_variant = settings.get_enum ("colorblindness-correction-filter");
+ var strength = settings.get_double ("colorblindness-correction-filter-strength");
+
+ foreach (unowned var _effect in wm.stage.get_effects ()) {
+ if (_effect is ColorblindnessCorrectionEffect) {
+ unowned var effect = (ColorblindnessCorrectionEffect) _effect;
+
+ if (effect.mode != filter_variant) {
+ continue;
+ }
+
+ // stop transition (if there is one in progress)
+ if (effect.transition_actor != null) {
+ effect.transition_actor.destroy ();
+ }
+
+ // create a new transition
+ var transition = new Clutter.PropertyTransition ("scale_x") {
+ duration = TRANSITION_DURATION,
+ progress_mode = Clutter.AnimationMode.LINEAR,
+ remove_on_complete = true
+ };
+ transition.set_from_value (effect.strength);
+ transition.set_to_value (strength);
+
+ // create a transition actor and bind its `scale_x` to effect's `strength`
+ effect.transition_actor = new Clutter.Actor () {
+ visible = false
+ };
+ wm.ui_group.add_child (effect.transition_actor);
+ effect.transition_actor.bind_property ("scale_x", effect, "strength");
+
+ transition.completed.connect (() => {
+ effect.transition_actor.destroy ();
+ });
+
+ effect.transition_actor.add_transition (TRANSITION_NAME, transition);
+
+ return;
+ }
+ }
+ }
+
+ private void load_monochrome_filter () {
+ // When gala launches and there is an effect active, it shouldn't be faded in
+ var enable = settings.get_boolean ("enable-monochrome-filter");
+ var strength = settings.get_double ("monochrome-filter-strength");
+ if (enable && strength > 0.0) {
+ wm.stage.add_effect_with_name (
+ MonochromeEffect.EFFECT_NAME,
+ new MonochromeEffect (strength)
+ );
+ }
+ }
+
+ private void update_monochrome_filter () {
+ var enabled = settings.get_boolean ("enable-monochrome-filter");
+ var strength = settings.get_double ("monochrome-filter-strength");
+ unowned var effect = (MonochromeEffect) wm.stage.get_effect (MonochromeEffect.EFFECT_NAME);
+
+ if ((effect != null) == enabled) {
+ return;
+ }
+
+ // Fade out applied effects
+ if (effect != null) {
+ // Since you can't add a transition to an effect
+ // add it to a transition actor and bind one of its properties to the effect
+
+ // stop transition (if there is one in progress)
+ if (effect.transition_actor != null) {
+ effect.transition_actor.destroy ();
+ }
+
+ // create a new transition
+ var transition = new Clutter.PropertyTransition ("scale_x") {
+ duration = TRANSITION_DURATION,
+ progress_mode = Clutter.AnimationMode.LINEAR,
+ remove_on_complete = true
+ };
+ transition.set_from_value (effect.strength);
+ transition.set_to_value (0.0);
+
+ // create a transition actor and bind its `scale_x` to effect's `strength`
+ effect.transition_actor = new Clutter.Actor () {
+ visible = false
+ };
+ wm.ui_group.add_child (effect.transition_actor);
+ effect.transition_actor.bind_property ("scale_x", effect, "strength");
+
+ transition.completed.connect (() => {
+ effect.transition_actor.destroy ();
+ wm.stage.remove_effect (effect);
+ });
+
+ effect.transition_actor.add_transition (TRANSITION_NAME, transition);
+ }
+
+ // Apply a new filter
+ if (!enabled || strength == 0.0) {
+ return;
+ }
+
+ var new_effect = new MonochromeEffect (0.0);
+ wm.stage.add_effect_with_name (MonochromeEffect.EFFECT_NAME, new_effect);
+
+ // Transition new effect in the same way
+ var new_transition = new Clutter.PropertyTransition ("scale_x") {
+ duration = TRANSITION_DURATION,
+ progress_mode = Clutter.AnimationMode.LINEAR,
+ remove_on_complete = true
+ };
+ new_transition.set_from_value (0.0);
+ new_transition.set_to_value (strength);
+
+ new_effect.transition_actor = new Clutter.Actor () {
+ visible = false
+ };
+ wm.ui_group.add_child (new_effect.transition_actor);
+ new_effect.transition_actor.bind_property ("scale_x", new_effect, "strength");
+
+ new_transition.completed.connect (() => {
+ new_effect.transition_actor.destroy ();
+ });
+
+ new_effect.transition_actor.add_transition (TRANSITION_NAME, new_transition);
+ }
+
+ private void update_monochrome_strength () {
+ var strength = settings.get_double ("monochrome-filter-strength");
+
+ unowned var effect = (MonochromeEffect) wm.stage.get_effect (MonochromeEffect.EFFECT_NAME);
+
+ // stop transition (if there is one in progress)
+ if (effect.transition_actor != null) {
+ effect.transition_actor.destroy ();
+ }
+
+ // create a new transition
+ var transition = new Clutter.PropertyTransition ("scale_x") {
+ duration = TRANSITION_DURATION,
+ progress_mode = Clutter.AnimationMode.LINEAR,
+ remove_on_complete = true
+ };
+ transition.set_from_value (effect.strength);
+ transition.set_to_value (strength);
+
+ // create a transition actor and bind its `scale_x` to effect's `strength`
+ effect.transition_actor = new Clutter.Actor () {
+ visible = false
+ };
+ wm.ui_group.add_child (effect.transition_actor);
+ effect.transition_actor.bind_property ("scale_x", effect, "strength");
+
+ transition.completed.connect (() => {
+ effect.transition_actor.destroy ();
+ });
+
+ effect.transition_actor.add_transition (TRANSITION_NAME, transition);
+ }
+}
diff --git a/src/ColorFilters/MonochromeEffect.vala b/src/ColorFilters/MonochromeEffect.vala
new file mode 100644
index 00000000..6be200e5
--- /dev/null
+++ b/src/ColorFilters/MonochromeEffect.vala
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 elementary, Inc.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+public class Gala.MonochromeEffect : Clutter.ShaderEffect {
+ public const string EFFECT_NAME = "monochrome-filter";
+
+ private double _strength;
+ public double strength {
+ get { return _strength; }
+ construct set {
+ _strength = value;
+
+ set_uniform_value ("STRENGTH", value);
+ queue_repaint ();
+ }
+ }
+
+ /*
+ * Used for fading in and out the effect, since you can't add transitions to effects.
+ */
+ public Clutter.Actor? transition_actor { get; set; default = null; }
+
+ public MonochromeEffect (double strength) {
+ Object (
+ shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
+ strength: strength
+ );
+
+ try {
+ var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/monochrome.vert", GLib.ResourceLookupFlags.NONE);
+ set_shader_source ((string) bytes.get_data ());
+ } catch (Error e) {
+ critical ("Unable to load monochrome.vert: %s", e.message);
+ }
+ }
+}
diff --git a/src/WindowManager.vala b/src/WindowManager.vala
index 75eec501..c1c6e247 100644
--- a/src/WindowManager.vala
+++ b/src/WindowManager.vala
@@ -242,6 +242,8 @@ namespace Gala {
stage.remove_child (top_window_group);
ui_group.add_child (top_window_group);
+ FilterManager.init (this);
+
/*keybindings*/
var keybinding_settings = new GLib.Settings (Config.SCHEMA + ".keybindings");
diff --git a/src/meson.build b/src/meson.build
index 11abcd11..6177a429 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -29,6 +29,9 @@ gala_bin_sources = files(
'Background/BackgroundManager.vala',
'Background/BackgroundSource.vala',
'Background/SystemBackground.vala',
+ 'ColorFilters/ColorblindnessCorrectionEffect.vala',
+ 'ColorFilters/FilterManager.vala',
+ 'ColorFilters/MonochromeEffect.vala',
'Gestures/Gesture.vala',
'Gestures/GestureSettings.vala',
'Gestures/GestureTracker.vala',