From 81a5f29979d38c8deb48f02c0f1a1383ce0bdd60 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 16 Dec 2024 08:56:37 +0530 Subject: [PATCH] Option to control underline exclusion --- kitty/fonts.c | 14 ++++++++++++-- kitty/options/definition.py | 8 ++++++++ kitty/options/parse.py | 9 ++++++--- kitty/options/to-c-generated.h | 15 +++++++++++++++ kitty/options/to-c.h | 10 ++++++++++ kitty/options/types.py | 2 ++ kitty/options/utils.py | 14 ++++++++++++++ kitty/state.h | 1 + 8 files changed, 68 insertions(+), 5 deletions(-) diff --git a/kitty/fonts.c b/kitty/fonts.c index 0e204a657..9c2fe48fd 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -339,7 +339,16 @@ static void calculate_underline_exclusion_zones(pixel *buf, const FontGroup *fg, DecorationGeometry dg) { pixel *ans = buf + fg->fcm.cell_height * fg->fcm.cell_width; const unsigned bottom = MIN(dg.top + dg.height, fg->fcm.cell_height); - const unsigned thickness = MAX(1u, fg->fcm.underline_thickness / 1); + unsigned thickness; + switch(OPT(underline_exclusion.unit)) { + case 2: + thickness = ((long)round((OPT(underline_exclusion).thickness * (fg->logical_dpi_x / 72.0)))); break; + case 1: + thickness = (unsigned)OPT(underline_exclusion).thickness; break; + default: + thickness = (unsigned)(OPT(underline_exclusion).thickness * fg->fcm.underline_thickness); break; + } + thickness = MAX(1u, thickness); for (unsigned x = 0; x < fg->fcm.cell_width; x++) { for (unsigned y = dg.top + 2; y < bottom && !ans[x]; y++) { if ((buf[y * fg->fcm.cell_width + x] & 0x000000ff) > 0) { @@ -356,7 +365,8 @@ current_send_sprite_to_gpu(FontGroup *fg, pixel *buf, DecorationMetadata dec) { sprite_index ans = current_sprite_index(&fg->sprite_tracker); if (!do_increment(fg)) return 0; if (python_send_to_gpu_impl) { python_send_to_gpu(fg, ans, buf); return ans; } - if (dec.underline_region.height) calculate_underline_exclusion_zones(buf, fg, dec.underline_region); + if (dec.underline_region.height && OPT(underline_exclusion).thickness > 0) calculate_underline_exclusion_zones( + buf, fg, dec.underline_region); send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, ans, buf, dec.start_idx); if (0) { printf("Sprite: %u dec_idx: %u\n", ans, dec.start_idx); display_rgba_data(buf, fg->fcm.cell_width, fg->fcm.cell_height); printf("\n"); } return ans; diff --git a/kitty/options/definition.py b/kitty/options/definition.py index a113fce23..2fe09fd46 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -218,6 +218,14 @@ the curl will peak once per character, with dense twice. ''' ) +opt('underline_exclusion', '1', option_type='underline_exclusion', ctype='!underline_exclusion', long_text=''' +By default kitty renders gaps in underlines when they overlap with descenders +(the parts of letters below the baseline, such as for y, q, p etc.). This option +controls the thickness of the gaps. It can be either a unitless number in which +case it is a fraction of the underline thickness as specified in the font or +it can have a suffix of :code:`px` for pixels or :code:`pt` for points. +''') + opt('text_composition_strategy', 'platform', ctype='!text_composition_strategy', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index a6143f68f..b7ff1b9a0 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -19,9 +19,9 @@ from kitty.options.utils import ( shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, titlebar_color, to_cursor_shape, to_cursor_unfocused_shape, to_font_size, - to_layout_names, to_modifiers, transparent_background_colors, url_prefixes, url_style, - visual_bell_duration, visual_window_select_characters, window_border_width, window_logo_scale, - window_size + to_layout_names, to_modifiers, transparent_background_colors, underline_exclusion, url_prefixes, + url_style, visual_bell_duration, visual_window_select_characters, window_border_width, + window_logo_scale, window_size ) @@ -1344,6 +1344,9 @@ class Parser: choices_for_undercurl_style = frozenset(('thin-sparse', 'thin-dense', 'thick-sparse', 'thick-dense')) + def underline_exclusion(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['underline_exclusion'] = underline_exclusion(val) + def underline_hyperlinks(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: val = val.lower() if val not in self.choices_for_underline_hyperlinks: diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 72a6c3966..66dc88c2d 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -83,6 +83,19 @@ convert_from_opts_undercurl_style(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_underline_exclusion(PyObject *val, Options *opts) { + underline_exclusion(val, opts); +} + +static void +convert_from_opts_underline_exclusion(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "underline_exclusion"); + if (ret == NULL) return; + convert_from_python_underline_exclusion(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_text_composition_strategy(PyObject *val, Options *opts) { text_composition_strategy(val, opts); @@ -1163,6 +1176,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_undercurl_style(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_underline_exclusion(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_text_composition_strategy(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_cursor_shape(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index 8eeabb7a6..3d190a099 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -380,6 +380,16 @@ menu_map(PyObject *entry_dict, Options *opts) { } } +static inline void +underline_exclusion(PyObject *val, Options *opts) { + if (!PyTuple_Check(val)) { PyErr_SetString(PyExc_TypeError, "underline_exclusion must be a tuple"); return; } + opts->underline_exclusion.thickness = PyFloat_AsFloat(PyTuple_GET_ITEM(val, 0)); + if (PyUnicode_CompareWithASCIIString(PyTuple_GET_ITEM(val, 1), "%")) opts->underline_exclusion.unit = 0; + else if (PyUnicode_CompareWithASCIIString(PyTuple_GET_ITEM(val, 1), "px")) opts->underline_exclusion.unit = 1; + else if (PyUnicode_CompareWithASCIIString(PyTuple_GET_ITEM(val, 1), "pt")) opts->underline_exclusion.unit = 2; + else opts->underline_exclusion.unit = 0; +} + static inline void text_composition_strategy(PyObject *val, Options *opts) { if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_rendering_strategy must be a string"); return; } diff --git a/kitty/options/types.py b/kitty/options/types.py index 823d15d6d..cc0489625 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -447,6 +447,7 @@ option_names = ( 'touch_scroll_multiplier', 'transparent_background_colors', 'undercurl_style', + 'underline_exclusion', 'underline_hyperlinks', 'update_check_interval', 'url_color', @@ -615,6 +616,7 @@ class Options: touch_scroll_multiplier: float = 1.0 transparent_background_colors: typing.Tuple[typing.Tuple[kitty.fast_data_types.Color, float], ...] = () undercurl_style: choices_for_undercurl_style = 'thin-sparse' + underline_exclusion: tuple[float, typing.Literal['%', 'px', 'pt']] = (1.0, '%') underline_hyperlinks: choices_for_underline_hyperlinks = 'hover' update_check_interval: float = 24.0 url_color: Color = Color(0, 135, 189) diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 742ba9d32..f2cea0aca 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -1017,6 +1017,20 @@ def shell_integration(x: str) -> FrozenSet[str]: return q +def underline_exclusion(x: str) -> tuple[float, Literal['%', 'px', 'pt']]: + try: + return float(x), '%' + except Exception: + unit: Literal['%', 'pt', 'px'] = x[-2:] # type: ignore + if unit not in ('px', 'pt', '%'): + raise ValueError(f'Invalid underline_exclusion with unrecognized unit: {x}') + try: + val = float(x[:-2]) + except Exception: + raise ValueError(f'Invalid underline_exclusion with non numberic value: {x}') + return val, unit + + def paste_actions(x: str) -> FrozenSet[str]: s = frozenset({'quote-urls-at-prompt', 'confirm', 'filter', 'confirm-if-large', 'replace-dangerous-control-codes', 'replace-newline', 'no-op'}) q = frozenset(x.lower().split(',')) diff --git a/kitty/state.h b/kitty/state.h index 17d79bc77..25005c216 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -124,6 +124,7 @@ typedef struct { } font_features; struct { Animation *cursor, *visual_bell; } animation; unsigned undercurl_style; + struct { float thickness; int unit; } underline_exclusion; } Options; typedef struct WindowLogoRenderData {