From 05d94e8256df64e5b31e8d9a1bd0ce94a5ff924f Mon Sep 17 00:00:00 2001 From: Luna McNulty Date: Thu, 18 May 2023 11:06:00 -0400 Subject: [PATCH 1/3] Add foreground override contrast threshold This adds a parameter to text_composition_strategy that specifies a percentage difference luminance below which the foreground color will be overridden. The foreground color is set to white if the background is dark or black if the background is light. Many programs output colors that look good with the author's terminal's color scheme but which are completely illegible with other color schemes. This allows the user ensure that there is always sufficient contrast to read the text on the screen. Since we want existing configs to continue working, this also makes it so that rather than taking exactly two parameters, text_composition_strategy takes one--three parameters, using the default values for those not specified. --- kitty/cell_fragment.glsl | 11 +++++++++++ kitty/options/definition.py | 7 ++++++- kitty/options/to-c.h | 34 ++++++++++++++++++++++++---------- kitty/shaders.c | 3 +++ kitty/state.h | 2 +- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index 113b3c8c2..88080f359 100644 --- a/kitty/cell_fragment.glsl +++ b/kitty/cell_fragment.glsl @@ -23,6 +23,7 @@ uniform sampler2DArray sprites; uniform int text_old_gamma; uniform float text_contrast; uniform float text_gamma_adjustment; +uniform float text_fg_override_threshold; in float effective_text_alpha; in vec3 sprite_pos; in vec3 underline_pos; @@ -130,6 +131,16 @@ float clamp_to_unit_float(float x) { vec4 foreground_contrast(vec4 over, vec3 under) { float underL = dot(under, Y); float overL = dot(over.rgb, Y); + + // If the difference in luminance is too small, + // force the foreground color to be black or white. + float diffL = abs(underL - overL); + if (0.5 < underL && diffL < text_fg_override_threshold) { + over.rgb = vec3(0, 0, 0); + } else if (underL < 0.5 && diffL < text_fg_override_threshold) { + over.rgb = vec3(1, 1, 1); + } + // Apply additional gamma-adjustment scaled by the luminance difference, the darker the foreground the more adjustment we apply. // A multiplicative contrast is also available to increase saturation. over.a = clamp_to_unit_float(mix(over.a, pow(over.a, text_gamma_adjustment), (1 - overL + underL) * text_gamma_scaling) * text_contrast); diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 435e1c7cc..5288309f9 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -249,7 +249,7 @@ light text on dark backgrounds thinner. It might also make some text appear like the strokes are uneven. You can fine tune the actual contrast curve used for glyph composition by -specifying two space separated numbers for this setting. +specifying up to three space-separated numbers for this setting. The first number is the gamma adjustment, which controls the thickness of dark text on light backgrounds. Increasing the value will make text appear thicker. @@ -263,6 +263,11 @@ The second number is an additional multiplicative contrast. It is percentage ranging from :code:`0` to :code:`100`. The default value is :code:`0` on Linux and :code:`30` on macOS. +The third number is an override threshold. It is percentage ranging from :code:`0` +to :code:`100`. If the difference in luminance of the foreground and background +is below this threshold, the foreground color will be set to white if the background +is dark or black if the background is light. The default value is :code:`0`. + If you wish to achieve similar looking thickness in light and dark themes, a good way to experiment is start by setting the value to :code:`1.0 0` and use a dark theme. Then adjust the second parameter until it looks good. Then switch to a light theme diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index fce4c8820..bbdd31285 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -183,7 +183,7 @@ static void text_composition_strategy(PyObject *val, Options *opts) { if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_rendering_strategy must be a string"); return; } opts->text_old_gamma = false; - opts->text_gamma_adjustment = 1.0f; opts->text_contrast = 0.f; + opts->text_gamma_adjustment = 1.0f; opts->text_contrast = 0.f; opts->text_fg_override_threshold = 0.f; if (PyUnicode_CompareWithASCIIString(val, "platform") == 0) { #ifdef __APPLE__ opts->text_gamma_adjustment = 1.7f; opts->text_contrast = 30.f; @@ -192,15 +192,29 @@ text_composition_strategy(PyObject *val, Options *opts) { else if (PyUnicode_CompareWithASCIIString(val, "legacy") == 0) { opts->text_old_gamma = true; } else { - DECREF_AFTER_FUNCTION PyObject *parts = PyUnicode_Split(val, NULL, 1); - if (PyList_GET_SIZE(parts) != 2) { PyErr_SetString(PyExc_ValueError, "text_rendering_strategy must be of the form number:number"); return; } - DECREF_AFTER_FUNCTION PyObject *ga = PyFloat_FromString(PyList_GET_ITEM(parts, 0)); - if (PyErr_Occurred()) return; - opts->text_gamma_adjustment = MAX(0.01f, PyFloat_AsFloat(ga)); - DECREF_AFTER_FUNCTION PyObject *contrast = PyFloat_FromString(PyList_GET_ITEM(parts, 1)); - if (PyErr_Occurred()) return; - opts->text_contrast = MAX(0.0f, PyFloat_AsFloat(contrast)); - opts->text_contrast = MIN(100.0f, opts->text_contrast); + DECREF_AFTER_FUNCTION PyObject *parts = PyUnicode_Split(val, NULL, 2); + int size = PyList_GET_SIZE(parts); + if (size < 1 || 3 < size) { PyErr_SetString(PyExc_ValueError, "text_rendering_strategy must be of the form number:[number]:[number]"); return; } + + if (size > 0) { + DECREF_AFTER_FUNCTION PyObject *ga = PyFloat_FromString(PyList_GET_ITEM(parts, 0)); + if (PyErr_Occurred()) return; + opts->text_gamma_adjustment = MAX(0.01f, PyFloat_AsFloat(ga)); + } + + if (size > 1) { + DECREF_AFTER_FUNCTION PyObject *contrast = PyFloat_FromString(PyList_GET_ITEM(parts, 1)); + if (PyErr_Occurred()) return; + opts->text_contrast = MAX(0.0f, PyFloat_AsFloat(contrast)); + opts->text_contrast = MIN(100.0f, opts->text_contrast); + } + + if (size > 2) { + DECREF_AFTER_FUNCTION PyObject *text_fg_override_threshold = PyFloat_FromString(PyList_GET_ITEM(parts, 2)); + if (PyErr_Occurred()) return; + opts->text_fg_override_threshold = MAX(0.f, PyFloat_AsFloat(text_fg_override_threshold)); + opts->text_fg_override_threshold = MIN(100.f, PyFloat_AsFloat(text_fg_override_threshold)); + } } } diff --git a/kitty/shaders.c b/kitty/shaders.c index d9b8005f2..5d77d6bb4 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -576,6 +576,9 @@ set_cell_uniforms(float current_inactive_text_alpha, bool force) { S(CELL_PROGRAM, text_contrast, text_contrast, 1f); S(CELL_FG_PROGRAM, text_contrast, text_contrast, 1f); float text_gamma_adjustment = OPT(text_gamma_adjustment) < 0.01f ? 1.0f : 1.0f / OPT(text_gamma_adjustment); S(CELL_PROGRAM, text_gamma_adjustment, text_gamma_adjustment, 1f); S(CELL_FG_PROGRAM, text_gamma_adjustment, text_gamma_adjustment, 1f); + + float text_fg_override_threshold = OPT(text_fg_override_threshold) * 0.01f; + S(CELL_PROGRAM, text_fg_override_threshold, text_fg_override_threshold, 1f); S(CELL_FG_PROGRAM, text_fg_override_threshold, text_fg_override_threshold, 1f); #undef S #define SV(prog, name, num, val, type) { bind_program(prog); glUniform##type(glGetUniformLocation(program_id(prog), #name), num, val); } SV(CELL_PROGRAM, gamma_lut, 256, srgb_lut, 1fv); SV(CELL_FG_PROGRAM, gamma_lut, 256, srgb_lut, 1fv); diff --git a/kitty/state.h b/kitty/state.h index 454aa90d0..08a4557c7 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -48,7 +48,7 @@ typedef struct { WindowTitleIn macos_show_window_title_in; char *bell_path, *bell_theme; float background_opacity, dim_opacity; - float text_contrast, text_gamma_adjustment; + float text_contrast, text_gamma_adjustment, text_fg_override_threshold; bool text_old_gamma; char *background_image, *default_window_logo; From 9bd97b090dd659771f11560f388d3ef81f144b72 Mon Sep 17 00:00:00 2001 From: Luna McNulty Date: Sat, 20 May 2023 09:54:43 -0400 Subject: [PATCH 2/3] Remove glsl if-else, add macro, separate new opt This adds a config option called text_fg_override_threshold that specifies a percentage difference luminance below which the foreground color will be overridden. The foreground color is set to white if the background is dark or black if the background is light. The default is 0, and the computations will only be performed if the option is set. Many programs output colors that look good with the author's terminal's color scheme but which are completely illegible with other color schemes. This allows the user ensure that there is always sufficient contrast to read the text on the screen. I originally implemented this is as a parameter on text_composition_strategy. For that to work, the option needed to take _up to_ rather than _exactly_ the number of available parameters. While it now has nothing to do with the new feature, it seems like that change should be made anyway, so I'm leaving it in for now. --- kitty/cell_fragment.glsl | 14 +++++++++----- kitty/options/definition.py | 18 ++++++++++++------ kitty/options/parse.py | 3 +++ kitty/options/to-c-generated.h | 15 +++++++++++++++ kitty/options/to-c.h | 23 ++++++++++++++--------- kitty/options/types.py | 2 ++ kitty/state.h | 3 ++- kitty/window.py | 2 ++ 8 files changed, 59 insertions(+), 21 deletions(-) diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index 88080f359..df6cb767c 100644 --- a/kitty/cell_fragment.glsl +++ b/kitty/cell_fragment.glsl @@ -1,6 +1,7 @@ #version GLSL_VERSION #define {WHICH_PROGRAM} #define NOT_TRANSPARENT +#define NO_FG_OVERRIDE #if defined(SIMPLE) || defined(BACKGROUND) || defined(SPECIAL) #define NEEDS_BACKROUND @@ -132,14 +133,17 @@ vec4 foreground_contrast(vec4 over, vec3 under) { float underL = dot(under, Y); float overL = dot(over.rgb, Y); +#if defined(FG_OVERRIDE) // If the difference in luminance is too small, // force the foreground color to be black or white. float diffL = abs(underL - overL); - if (0.5 < underL && diffL < text_fg_override_threshold) { - over.rgb = vec3(0, 0, 0); - } else if (underL < 0.5 && diffL < text_fg_override_threshold) { - over.rgb = vec3(1, 1, 1); - } + float overrideLvl = step(diffL, text_fg_override_threshold); + float originalLvl = 1.f - overrideLvl; + over.rgb = ( + originalLvl * over.rgb + + overrideLvl * vec3(step(underL, 0.5f)) + ); +#endif // Apply additional gamma-adjustment scaled by the luminance difference, the darker the foreground the more adjustment we apply. // A multiplicative contrast is also available to increase saturation. diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 5288309f9..b7c84c169 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -249,7 +249,7 @@ light text on dark backgrounds thinner. It might also make some text appear like the strokes are uneven. You can fine tune the actual contrast curve used for glyph composition by -specifying up to three space-separated numbers for this setting. +specifying up to two space-separated numbers for this setting. The first number is the gamma adjustment, which controls the thickness of dark text on light backgrounds. Increasing the value will make text appear thicker. @@ -263,17 +263,23 @@ The second number is an additional multiplicative contrast. It is percentage ranging from :code:`0` to :code:`100`. The default value is :code:`0` on Linux and :code:`30` on macOS. -The third number is an override threshold. It is percentage ranging from :code:`0` -to :code:`100`. If the difference in luminance of the foreground and background -is below this threshold, the foreground color will be set to white if the background -is dark or black if the background is light. The default value is :code:`0`. - If you wish to achieve similar looking thickness in light and dark themes, a good way to experiment is start by setting the value to :code:`1.0 0` and use a dark theme. Then adjust the second parameter until it looks good. Then switch to a light theme and adjust the first parameter until the perceived thickness matches the dark theme. ''') +opt('text_fg_override_threshold', 0, + ctype='!text_fg_override_threshold', + long_text=''' +The minimum accepted difference in luminance between the foreground and background +color, below which kitty will override the foreground color. It is percentage +ranging from :code:`0` to :code:`100`. If the difference in luminance of the +foreground and background is below this threshold, the foreground color will be set +to white if the background is dark or black if the background is light. The default +value is :code:`0`. +''') + egr() # }}} diff --git a/kitty/options/parse.py b/kitty/options/parse.py index d4a607f15..7bd395c28 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1284,6 +1284,9 @@ class Parser: def text_composition_strategy(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['text_composition_strategy'] = str(val) + def text_fg_override_threshold(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['text_fg_override_threshold'] = str(val) + def touch_scroll_multiplier(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['touch_scroll_multiplier'] = float(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index c1ea350a2..8fb15102e 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -70,6 +70,19 @@ convert_from_opts_text_composition_strategy(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_text_fg_override_threshold(PyObject *val, Options *opts) { + text_fg_override_threshold(val, opts); +} + +static void +convert_from_opts_text_fg_override_threshold(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "text_fg_override_threshold"); + if (ret == NULL) return; + convert_from_python_text_fg_override_threshold(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_cursor_shape(PyObject *val, Options *opts) { opts->cursor_shape = PyLong_AsLong(val); @@ -1070,6 +1083,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_text_composition_strategy(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_text_fg_override_threshold(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_cursor_shape(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_cursor_beam_thickness(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index bbdd31285..fa4661c6d 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -183,7 +183,7 @@ static void text_composition_strategy(PyObject *val, Options *opts) { if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_rendering_strategy must be a string"); return; } opts->text_old_gamma = false; - opts->text_gamma_adjustment = 1.0f; opts->text_contrast = 0.f; opts->text_fg_override_threshold = 0.f; + opts->text_gamma_adjustment = 1.0f; opts->text_contrast = 0.f; if (PyUnicode_CompareWithASCIIString(val, "platform") == 0) { #ifdef __APPLE__ opts->text_gamma_adjustment = 1.7f; opts->text_contrast = 30.f; @@ -194,7 +194,7 @@ text_composition_strategy(PyObject *val, Options *opts) { } else { DECREF_AFTER_FUNCTION PyObject *parts = PyUnicode_Split(val, NULL, 2); int size = PyList_GET_SIZE(parts); - if (size < 1 || 3 < size) { PyErr_SetString(PyExc_ValueError, "text_rendering_strategy must be of the form number:[number]:[number]"); return; } + if (size < 1 || 2 < size) { PyErr_SetString(PyExc_ValueError, "text_rendering_strategy must be of the form number:[number]"); return; } if (size > 0) { DECREF_AFTER_FUNCTION PyObject *ga = PyFloat_FromString(PyList_GET_ITEM(parts, 0)); @@ -208,16 +208,21 @@ text_composition_strategy(PyObject *val, Options *opts) { opts->text_contrast = MAX(0.0f, PyFloat_AsFloat(contrast)); opts->text_contrast = MIN(100.0f, opts->text_contrast); } - - if (size > 2) { - DECREF_AFTER_FUNCTION PyObject *text_fg_override_threshold = PyFloat_FromString(PyList_GET_ITEM(parts, 2)); - if (PyErr_Occurred()) return; - opts->text_fg_override_threshold = MAX(0.f, PyFloat_AsFloat(text_fg_override_threshold)); - opts->text_fg_override_threshold = MIN(100.f, PyFloat_AsFloat(text_fg_override_threshold)); - } } } +static void +text_fg_override_threshold(PyObject *val, Options *opts) { + if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_fg_override_threshold must be a string"); return; } + opts->text_fg_override_threshold = 0.f; + + DECREF_AFTER_FUNCTION PyObject *text_fg_override_threshold = PyFloat_FromString(val); + if (PyErr_Occurred()) return; + opts->text_fg_override_threshold = MAX(0.f, PyFloat_AsFloat(text_fg_override_threshold)); + opts->text_fg_override_threshold = MIN(100.f, PyFloat_AsFloat(text_fg_override_threshold)); + +} + static char_type* list_of_chars(PyObject *chars) { if (!PyUnicode_Check(chars)) { PyErr_SetString(PyExc_TypeError, "list_of_chars must be a string"); return NULL; } diff --git a/kitty/options/types.py b/kitty/options/types.py index dde02d82f..f4a738db3 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -445,6 +445,7 @@ option_names = ( # {{{ 'tab_title_template', 'term', 'text_composition_strategy', + 'text_fg_override_threshold', 'touch_scroll_multiplier', 'undercurl_style', 'update_check_interval', @@ -598,6 +599,7 @@ class Options: tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}' term: str = 'xterm-kitty' text_composition_strategy: str = 'platform' + text_fg_override_threshold: str = '0' touch_scroll_multiplier: float = 1.0 undercurl_style: choices_for_undercurl_style = 'thin-sparse' update_check_interval: float = 24.0 diff --git a/kitty/state.h b/kitty/state.h index 08a4557c7..7fd748d63 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -48,7 +48,8 @@ typedef struct { WindowTitleIn macos_show_window_title_in; char *bell_path, *bell_theme; float background_opacity, dim_opacity; - float text_contrast, text_gamma_adjustment, text_fg_override_threshold; + float text_contrast, text_gamma_adjustment; + float text_fg_override_threshold; bool text_old_gamma; char *background_image, *default_window_logo; diff --git a/kitty/window.py b/kitty/window.py index 732c8d051..f5e207932 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -397,6 +397,8 @@ class LoadShaderPrograms: DECORATION_MASK=DECORATION_MASK, STRIKE_SPRITE_INDEX=NUM_UNDERLINE_STYLES + 1, ) + if get_options().text_fg_override_threshold != '0': + ff = ff.replace('#define NO_FG_OVERRIDE', '#define FG_OVERRIDE') if semi_transparent: vv = vv.replace('#define NOT_TRANSPARENT', '#define TRANSPARENT') ff = ff.replace('#define NOT_TRANSPARENT', '#define TRANSPARENT') From a15289c0388a6e60d45951b0b2fe82324ab3ce16 Mon Sep 17 00:00:00 2001 From: Luna McNulty Date: Sun, 21 May 2023 10:43:04 -0400 Subject: [PATCH 3/3] Don't override when colored_sprite --- kitty/cell_fragment.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index df6cb767c..f9a4ad911 100644 --- a/kitty/cell_fragment.glsl +++ b/kitty/cell_fragment.glsl @@ -137,7 +137,7 @@ vec4 foreground_contrast(vec4 over, vec3 under) { // If the difference in luminance is too small, // force the foreground color to be black or white. float diffL = abs(underL - overL); - float overrideLvl = step(diffL, text_fg_override_threshold); + float overrideLvl = (1.f - colored_sprite) * step(diffL, text_fg_override_threshold); float originalLvl = 1.f - overrideLvl; over.rgb = ( originalLvl * over.rgb +