diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index 113b3c8c2..f9a4ad911 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 @@ -23,6 +24,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 +132,19 @@ 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 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); + float overrideLvl = (1.f - colored_sprite) * 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. 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 595f4ef3d..37d7e82eb 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -249,7 +249,7 @@ 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 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. @@ -269,6 +269,17 @@ 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 @@ def term(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: 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 fce4c8820..fa4661c6d 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -192,18 +192,37 @@ 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 || 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)); + 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); + } } } +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 @@ '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/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..7fd748d63 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -49,6 +49,7 @@ typedef struct { char *bell_path, *bell_theme; float background_opacity, dim_opacity; 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 @@ def __call__(self, semi_transparent: bool = False) -> None: 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')