diff --git a/gen/config.py b/gen/config.py index cbc7dd938..b7defd613 100755 --- a/gen/config.py +++ b/gen/config.py @@ -48,7 +48,7 @@ def main(args: list[str]=sys.argv) -> None: if opt.parser_func.__name__ in ('to_color_or_none', 'cursor_text_color'): nullable_colors.append(opt.name) all_colors.append(opt.name) - elif opt.parser_func.__name__ in ('to_color', 'titlebar_color', 'macos_titlebar_color'): + elif opt.parser_func.__name__ in ('to_color', 'titlebar_color', 'macos_titlebar_color', 'scrollbar_color'): all_colors.append(opt.name) patch_color_list('tools/cmd/at/set_colors.go', nullable_colors, 'NULLABLE') patch_color_list('tools/themes/collection.go', all_colors, 'ALL') diff --git a/kitty/data-types.c b/kitty/data-types.c index ac88f2fe3..687dccd87 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -864,11 +864,6 @@ PyInit_fast_data_types(void) { PyModule_AddIntMacro(m, COLOR_IS_SPECIAL); PyModule_AddIntMacro(m, COLOR_IS_INDEX); PyModule_AddIntMacro(m, COLOR_IS_RGB); - PyModule_AddIntMacro(m, SCROLLBAR_ALWAYS); - PyModule_AddIntMacro(m, SCROLLBAR_NEVER); - PyModule_AddIntMacro(m, SCROLLBAR_ON_HOVERED); - PyModule_AddIntMacro(m, SCROLLBAR_ON_SCROLLED); - PyModule_AddIntMacro(m, SCROLLBAR_ON_SCROLL_AND_HOVER); #ifdef __APPLE__ // Apple says its SHM_NAME_MAX but SHM_NAME_MAX is not actually declared in typical CrApple style. // This value is based on experimentation and from qsharedmemory.cpp in Qt diff --git a/kitty/data-types.h b/kitty/data-types.h index 3f242ac14..1264fe0ee 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -113,7 +113,6 @@ typedef enum MouseShapes { /* end mouse shapes */ } MouseShape; typedef enum { NONE, MENUBAR, WINDOW, ALL } WindowTitleIn; -typedef enum { SCROLLBAR_TRACK_JUMP, SCROLLBAR_TRACK_PAGE } ScrollbarTrackBehavior; typedef enum { SCROLLBAR_NEVER, SCROLLBAR_ON_SCROLLED, SCROLLBAR_ON_HOVERED, SCROLLBAR_ON_SCROLL_AND_HOVER, SCROLLBAR_ALWAYS } ScrollbarVisibilityPolicy; typedef enum { TILING, SCALED, MIRRORED, CLAMPED, CENTER_CLAMPED, CENTER_SCALED } BackgroundImageLayout; typedef struct ImageAnchorPosition { diff --git a/kitty/debug_config.py b/kitty/debug_config.py index f546abd03..75921b685 100644 --- a/kitty/debug_config.py +++ b/kitty/debug_config.py @@ -22,7 +22,7 @@ from .fast_data_types import Color, SingleKey, current_fonts, glfw_get_system_color_theme, gpu_driver_version_string, num_users, wayland_compositor_data from .options.types import Options as KittyOpts from .options.types import defaults, secret_options -from .options.utils import KeyboardMode, KeyDefinition, ScrollbarSettings +from .options.utils import KeyboardMode, KeyDefinition from .rgb import color_as_sharp, color_from_int from .types import MouseEvent, Shortcut, mod_to_names from .utils import shlex_split @@ -100,10 +100,6 @@ def compare_opts(opts: KittyOpts, global_shortcuts: dict[str, SingleKey] | None, print(' ', val[k]) else: print(pformat(val)) - elif isinstance(val, ScrollbarSettings): - print(f'{f}:') - for ssd in ScrollbarSettings().differences(val): - print(f'\t{ssd}') else: val = getattr(opts, f) if isinstance(val, Color): diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 1e509d2d6..d0d65bada 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -20,11 +20,6 @@ COLOR_IS_SPECIAL: int COLOR_NOT_SET: int COLOR_IS_RGB: int COLOR_IS_INDEX: int -SCROLLBAR_ALWAYS: int -SCROLLBAR_NEVER: int -SCROLLBAR_ON_HOVERED: int -SCROLLBAR_ON_SCROLLED: int -SCROLLBAR_ON_SCROLL_AND_HOVER: int GLFW_LAYER_SHELL_NONE: int GLFW_LAYER_SHELL_PANEL: int GLFW_LAYER_SHELL_TOP: int diff --git a/kitty/mouse.c b/kitty/mouse.c index 8d6fbf32c..0b44d81c9 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -446,9 +446,9 @@ calculate_scrollbar_geometry(Window *w) { WindowGeometry *g = &w->render_data.geometry; unsigned cell_width = w->render_data.screen->cell_size.width; - geom.width = OPT(scrollbar.width) * cell_width; - geom.gap = OPT(scrollbar.gap) * cell_width; - geom.hitbox_expansion = OPT(scrollbar.hitbox_expansion) * cell_width; + geom.width = OPT(scrollbar_width) * cell_width; + geom.gap = OPT(scrollbar_gap) * cell_width; + geom.hitbox_expansion = OPT(scrollbar_hitbox_expansion) * cell_width; double right_edge = g->right + g->spaces.right; geom.left = right_edge - geom.gap - geom.width - geom.hitbox_expansion; @@ -474,7 +474,7 @@ get_scrollbar_hit_type(Window *w, double mouse_x, double mouse_y) { if (!os_window) return SCROLLBAR_HIT_TRACK; double mouse_window_fraction = mouse_y / os_window->viewport_height; unsigned cell_width = w->render_data.screen->cell_size.width; - double hitbox_expansion_fraction = (double)(OPT(scrollbar.hitbox_expansion) * cell_width) / os_window->viewport_height; + double hitbox_expansion_fraction = (double)(OPT(scrollbar_hitbox_expansion) * cell_width) / os_window->viewport_height; if (mouse_window_fraction >= (w->scrollbar.thumb_top - hitbox_expansion_fraction) && mouse_window_fraction <= (w->scrollbar.thumb_bottom + hitbox_expansion_fraction)) { @@ -490,7 +490,7 @@ handle_scrollbar_track_click(Window *w, double mouse_y) { Screen *screen = w->render_data.screen; if (!validate_scrollbar_state(w)) return; - if (OPT(scrollbar.track_behavior) == SCROLLBAR_TRACK_JUMP) { + if (OPT(scrollbar_jump_on_click)) { ScrollbarGeometry geom = calculate_scrollbar_geometry(w); double scrollbar_height = geom.bottom - geom.top; double mouse_pane_fraction = (mouse_y - geom.top) / scrollbar_height; @@ -552,7 +552,7 @@ handle_scrollbar_drag(Window *w, double mouse_y) { double delta_y = mouse_pane_fraction - w->scrollbar.drag_start_y; double visible_fraction = (double)screen->lines / (screen->lines + screen->historybuf->count); unsigned cell_height = screen->cell_size.height; - double min_thumb_height_fraction = ((double)OPT(scrollbar.min_handle_height) * cell_height) / scrollbar_height; + double min_thumb_height_fraction = ((double)OPT(scrollbar_min_handle_height) * cell_height) / scrollbar_height; double thumb_height = MAX(min_thumb_height_fraction, visible_fraction); double available_space = 1.0 - thumb_height; @@ -573,7 +573,7 @@ handle_scrollbar_drag(Window *w, double mouse_y) { static bool handle_scrollbar_mouse(Window *w, int button, MouseAction action, int modifiers UNUSED) { - if (!w || !OPT(scrollbar.interactive) || !global_state.callback_os_window || global_state.active_drag_in_window == w->id || global_state.tracked_drag_in_window == w->id) return false; + if (!w || !OPT(scrollbar_interactive) || !global_state.callback_os_window || global_state.active_drag_in_window == w->id || global_state.tracked_drag_in_window == w->id) return false; double mouse_x = global_state.callback_os_window->mouse_x; double mouse_y = global_state.callback_os_window->mouse_y; diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 3f384ed3c..ee090c2d5 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -7,7 +7,7 @@ from kitty.conf.types import Action, Definition from kitty.constants import website_url -from kitty.options.utils import default_scrollbar, pointer_shape_names +from kitty.options.utils import pointer_shape_names definition = Definition( 'kitty', @@ -442,61 +442,76 @@ ''' ) -opt('scrollbar', '', option_type='scrollbar', ctype='!scrollbar', - long_text=f''' -Control the appearance and behavior of the scrollbar that is displayed when -scrolling through the scrollback. Accepts a number of space separated settings in key=value -format, documented below. To have the scrollbar always visible, use: -:code:`visible_when=always`. To disable it completely use: :code:`visible_when=never`. -To have it be visible but not react to the mouse, use :code:`interactive=no`. For example: -:code:`visible_when=always interactive=no`. +opt('scrollbar', 'scrolled', ctype='scrollbar', choices=( + 'scrolled', 'always', 'never', 'hovered', 'scrolled-and-hovered'), long_text='''\ +Control when the scrollbar is displayed. -The various settings, with their default values: +:code:`scrolled` + means when the scrolling backwards has started. +:code:`hovered` + means when the mouse is hovering on the right edge of the window. +:code:`scrolled-and-hovered` + means when the mouse is over the scrollbar region *and* scrolling backwards has started. +:code:`always` + means whenever any scrollback is present +:code:`never` + means disable the scrollbar. +''') -**color** :code:`={default_scrollbar.color_as_string(default_scrollbar.color)}` - the color of the scrollbar handle. Use the special keywords: :code:`foreground` or :code:`selection_background` to use those colors. +opt('scrollbar_interactive', 'yes', option_type='to_bool', ctype='bool', long_text=''' +If disabled, the scrollbar will not be controllable via th emouse and all mouse events +will pass through the scrollbar.''') -**track_color** :code:`={default_scrollbar.color_as_string(default_scrollbar.color)}` - the color of the scrollbar track. Use the special keywords: :code:`foreground` or :code:`selection_background` to use those colors. +opt('scrollbar_jump_on_click', 'yes', option_type='to_bool', ctype='bool', long_text=''' +When enabled clicking in the scrollbar track will cause the scroll position to +jump to the clicked location, otherwise the scroll position will only move +towards the position by a single screenful, which is how traditional scrollbars behave.''') -**opacity** :code:`={default_scrollbar.opacity}` - the opacity of the scrollbar handle (0 invisible to 1 fully opaque) +opt('scrollbar_width', '0.5', option_type='positive_float', ctype='float', long_text=''' +The width of the scroll bar in units of cell width. +''') -**track_opacity** :code:`={default_scrollbar.track_opacity}` - the opacity of the scrollbar track +opt('scrollbar_handle_opacity', '0.5', option_type='positive_float', ctype='float', long_text=''' +The opacity of the scrollbar handle, 0 being fully transparent and 1 being full opaque. +''') -**track_hover_opacity** :code:`={default_scrollbar.track_opacity}` - the opacity of the scrollbar track when the mouse is over it. +opt('scrollbar_radius', '0.3', option_type='positive_float', ctype='float', long_text=''' +The radius (curvature) of the scrollbar handle in units of cell width. Should be less than +:opt:`scrollbar_width`. +''') -**visible_when** :code:`=scrolled` - when the scrollbar is displayed. - :code:`scrolled` means when the scrolling backwards has started. - :code:`hovered` means when the mouse is hovering on the right edge of the window. - :code:`scrolled-and-hovered` means when the mouse is over the scrollbar region *and* scrolling backwards has started. - :code:`always` means whenever any scrollback is present - :code:`never` means disable the scrollbar. +opt('scrollbar_gap', '0.1', option_type='positive_float', ctype='float', long_text=''' +The gap between the scrollbar and the window edge in units of cell width. +''') -**interactive** :code:`={'yes' if default_scrollbar.interactive else 'no'}` - whether the scrollbar can be controlled by the mouse. +opt('scrollbar_min_handle_height', '1', option_type='positive_float', ctype='float', long_text=''' +The minimum height of the scrollbar handle in units of cell height. Prevents the handle +from becoming too small when there is a lot of scrollback.''') -**track_click** :code:`={'jump' if default_scrollbar.jump_on_track_click else 'page'}` - the behavior of clicking on the scrollbar track. :code:`jump` means scroll to the position of - the click and :code:`page` means scroll by a single screen towards the position of the click. +opt('scrollbar_hitbox_expansion', '0.25', option_type='positive_float', ctype='float', long_text=''' +The extra area around the handle to allow easier grabbing of the scollbar in units of cell width.''') -**width** :code:`={default_scrollbar.width}` - the width of the scrollbar in units of cells +opt('scrollbar_track_opacity', '0', option_type='positive_float', ctype='float', long_text=''' +The opacity of the scrollbar track, 0 being fully transparent and 1 being full opaque. +''') -**radius** :code:`={default_scrollbar.radius}` - the radius (curvature) of the scrollbar handle in units of cells +opt('scrollbar_track_hover_opacity', '0.1', option_type='positive_float', ctype='float', long_text=''' +The opacity of the scrollbar track when the mouse is over the scrollbar, +0 being fully transparent and 1 being full opaque. +''') -**gap** :code:`={default_scrollbar.gap}` - the gap between the scrollbar and the window edge in units of cells +opt('scrollbar_handle_color', 'foreground', option_type='scrollbar_color', ctype='uint', long_text=''' +The color of the scrollbar handle. A value of :code:`foreground` means to use +the current foreground text color, a value of :code:`selection_background` means to +use the current selection background color. Also, you can use an +arbitrary color, such as :code:`#12af59` or :code:`red`. +''') -**min_handle_height** :code:`={default_scrollbar.min_handle_height}` - the minimum height of the scrollbar handle in units of cells - -**hitbox_expansion** :code:`={default_scrollbar.hitbox_expansion}` - the extra area around the handle to allow easier grabbing of the scollbar in units of cells +opt('scrollbar_track_color', 'foreground', option_type='scrollbar_color', ctype='uint', long_text=''' +The color of the scrollbar track. A value of :code:`foreground` means to use +the current foreground text color, a value of :code:`selection_background` means to +use the current selection background color. Also, you can use an +arbitrary color, such as :code:`#12af59` or :code:`red`. ''') opt('scrollback_pager', 'less --chop-long-lines --RAW-CONTROL-CHARS +INPUT_LINE_NUMBER', diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 7d7c5ab40..892c6288a 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -17,13 +17,13 @@ hide_window_decorations, macos_option_as_alt, macos_titlebar_color, menu_map, modify_font, mouse_hide_wait, narrow_symbols, notify_on_cmd_finish, optional_edge_width, parse_font_spec, parse_map, parse_mouse_map, paste_actions, pointer_shape_when_dragging, remote_control_password, - resize_debounce_time, scrollback_lines, scrollback_pager_history_size, scrollbar, 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, - text_fg_override_threshold, titlebar_color, to_cursor_shape, to_cursor_unfocused_shape, - to_font_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 + resize_debounce_time, scrollback_lines, scrollback_pager_history_size, scrollbar_color, + 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, text_fg_override_threshold, titlebar_color, to_cursor_shape, + to_cursor_unfocused_shape, to_font_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 ) @@ -1207,7 +1207,48 @@ def scrollback_pager_history_size(self, val: str, ans: dict[str, typing.Any]) -> ans['scrollback_pager_history_size'] = scrollback_pager_history_size(val) def scrollbar(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['scrollbar'] = scrollbar(val) + val = val.lower() + if val not in self.choices_for_scrollbar: + raise ValueError(f"The value {val} is not a valid choice for scrollbar") + ans["scrollbar"] = val + + choices_for_scrollbar = frozenset(('scrolled', 'always', 'never', 'hovered', 'scrolled-and-hovered')) + + def scrollbar_gap(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_gap'] = positive_float(val) + + def scrollbar_handle_color(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_handle_color'] = scrollbar_color(val) + + def scrollbar_handle_opacity(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_handle_opacity'] = positive_float(val) + + def scrollbar_hitbox_expansion(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_hitbox_expansion'] = positive_float(val) + + def scrollbar_interactive(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_interactive'] = to_bool(val) + + def scrollbar_jump_on_click(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_jump_on_click'] = to_bool(val) + + def scrollbar_min_handle_height(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_min_handle_height'] = positive_float(val) + + def scrollbar_radius(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_radius'] = positive_float(val) + + def scrollbar_track_color(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_track_color'] = scrollbar_color(val) + + def scrollbar_track_hover_opacity(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_track_hover_opacity'] = positive_float(val) + + def scrollbar_track_opacity(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_track_opacity'] = positive_float(val) + + def scrollbar_width(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_width'] = positive_float(val) def select_by_word_characters(self, val: str, ans: dict[str, typing.Any]) -> None: ans['select_by_word_characters'] = str(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 3ba61186a..cff644784 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -254,7 +254,7 @@ convert_from_opts_cursor_trail_color(PyObject *py_opts, Options *opts) { static void convert_from_python_scrollbar(PyObject *val, Options *opts) { - scrollbar(val, opts); + opts->scrollbar = scrollbar(val); } static void @@ -265,6 +265,162 @@ convert_from_opts_scrollbar(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_scrollbar_interactive(PyObject *val, Options *opts) { + opts->scrollbar_interactive = PyObject_IsTrue(val); +} + +static void +convert_from_opts_scrollbar_interactive(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_interactive"); + if (ret == NULL) return; + convert_from_python_scrollbar_interactive(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_jump_on_click(PyObject *val, Options *opts) { + opts->scrollbar_jump_on_click = PyObject_IsTrue(val); +} + +static void +convert_from_opts_scrollbar_jump_on_click(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_jump_on_click"); + if (ret == NULL) return; + convert_from_python_scrollbar_jump_on_click(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_width(PyObject *val, Options *opts) { + opts->scrollbar_width = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_scrollbar_width(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_width"); + if (ret == NULL) return; + convert_from_python_scrollbar_width(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_handle_opacity(PyObject *val, Options *opts) { + opts->scrollbar_handle_opacity = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_scrollbar_handle_opacity(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_handle_opacity"); + if (ret == NULL) return; + convert_from_python_scrollbar_handle_opacity(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_radius(PyObject *val, Options *opts) { + opts->scrollbar_radius = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_scrollbar_radius(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_radius"); + if (ret == NULL) return; + convert_from_python_scrollbar_radius(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_gap(PyObject *val, Options *opts) { + opts->scrollbar_gap = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_scrollbar_gap(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_gap"); + if (ret == NULL) return; + convert_from_python_scrollbar_gap(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_min_handle_height(PyObject *val, Options *opts) { + opts->scrollbar_min_handle_height = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_scrollbar_min_handle_height(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_min_handle_height"); + if (ret == NULL) return; + convert_from_python_scrollbar_min_handle_height(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_hitbox_expansion(PyObject *val, Options *opts) { + opts->scrollbar_hitbox_expansion = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_scrollbar_hitbox_expansion(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_hitbox_expansion"); + if (ret == NULL) return; + convert_from_python_scrollbar_hitbox_expansion(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_track_opacity(PyObject *val, Options *opts) { + opts->scrollbar_track_opacity = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_scrollbar_track_opacity(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_track_opacity"); + if (ret == NULL) return; + convert_from_python_scrollbar_track_opacity(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_track_hover_opacity(PyObject *val, Options *opts) { + opts->scrollbar_track_hover_opacity = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_scrollbar_track_hover_opacity(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_track_hover_opacity"); + if (ret == NULL) return; + convert_from_python_scrollbar_track_hover_opacity(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_handle_color(PyObject *val, Options *opts) { + opts->scrollbar_handle_color = PyLong_AsUnsignedLong(val); +} + +static void +convert_from_opts_scrollbar_handle_color(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_handle_color"); + if (ret == NULL) return; + convert_from_python_scrollbar_handle_color(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_track_color(PyObject *val, Options *opts) { + opts->scrollbar_track_color = PyLong_AsUnsignedLong(val); +} + +static void +convert_from_opts_scrollbar_track_color(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_track_color"); + if (ret == NULL) return; + convert_from_python_scrollbar_track_color(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_scrollback_pager_history_size(PyObject *val, Options *opts) { opts->scrollback_pager_history_size = PyLong_AsUnsignedLong(val); @@ -1230,6 +1386,30 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_scrollbar(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_interactive(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_jump_on_click(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_width(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_handle_opacity(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_radius(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_gap(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_min_handle_height(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_hitbox_expansion(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_track_opacity(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_track_hover_opacity(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_handle_color(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_track_color(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_scrollback_pager_history_size(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_scrollback_fill_enlarged_window(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index d78d2a8e9..ee2300e66 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -58,35 +58,17 @@ window_title_in(PyObject *title_in) { return ALL; } -static inline void -scrollbar(PyObject *src, Options *opts) { -#define G(conv, attr) { RAII_PyObject(x, PyObject_GetAttrString(src, #attr)); if (x) opts->scrollbar.attr = conv(x); } - G(PyFloat_AsFloat, opacity); - G(PyFloat_AsFloat, track_opacity); - G(PyFloat_AsFloat, track_hover_opacity); - G(PyFloat_AsFloat, width); - G(PyFloat_AsFloat, radius); - G(PyFloat_AsFloat, gap); - G(PyFloat_AsFloat, min_handle_height); - G(PyFloat_AsFloat, hitbox_expansion); - G(PyLong_AsUnsignedLong, color); - G(PyLong_AsUnsignedLong, track_color); - G(PyLong_AsLong, visible_when); - G(PyObject_IsTrue, interactive); -#undef G - RAII_PyObject(x, PyObject_GetAttrString(src, "jump_on_track_click")); - opts->scrollbar.track_behavior = (x && PyObject_IsTrue(x)) ? SCROLLBAR_TRACK_JUMP : SCROLLBAR_TRACK_PAGE; -} - -static inline ScrollbarTrackBehavior -scrollbar_track_behavior(PyObject *val) { - const char *v = PyUnicode_AsUTF8(val); - switch(v[0]) { - case 'j': return SCROLLBAR_TRACK_JUMP; - case 'p': return SCROLLBAR_TRACK_PAGE; - default: break; +static inline ScrollbarVisibilityPolicy +scrollbar(PyObject *src) { + const char *q = PyUnicode_AsUTF8(src); + switch (q[0]) { + case 'a': return SCROLLBAR_ALWAYS; + case 'n': return SCROLLBAR_NEVER; + case 'h': return SCROLLBAR_ON_HOVERED; + case 's': + return strcmp(q, "scrolled") == 0 ? SCROLLBAR_ON_SCROLLED : SCROLLBAR_ON_SCROLL_AND_HOVER; } - return SCROLLBAR_TRACK_JUMP; + return SCROLLBAR_ON_SCROLLED; } static inline unsigned diff --git a/kitty/options/types.py b/kitty/options/types.py index faa8e40da..a32aea2fe 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -12,7 +12,7 @@ import kitty.fonts from kitty.options.utils import ( AliasMap, KeyDefinition, KeyboardModeMap, MouseHideWait, MouseMap, MouseMapping, NotifyOnCmdFinish, - ScrollbarSettings, TabBarMarginHeight + TabBarMarginHeight ) import kitty.options.utils from kitty.types import FloatEdges @@ -27,6 +27,7 @@ choices_for_macos_show_window_title_in = typing.Literal['all', 'menubar', 'none', 'window'] choices_for_placement_strategy = typing.Literal['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'] choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape +choices_for_scrollbar = typing.Literal['scrolled', 'always', 'never', 'hovered', 'scrolled-and-hovered'] choices_for_strip_trailing_spaces = typing.Literal['always', 'never', 'smart'] choices_for_tab_bar_align = typing.Literal['left', 'center', 'right'] choices_for_tab_bar_style = typing.Literal['fade', 'hidden', 'powerline', 'separator', 'slant', 'custom'] @@ -415,6 +416,18 @@ 'scrollback_pager', 'scrollback_pager_history_size', 'scrollbar', + 'scrollbar_gap', + 'scrollbar_handle_color', + 'scrollbar_handle_opacity', + 'scrollbar_hitbox_expansion', + 'scrollbar_interactive', + 'scrollbar_jump_on_click', + 'scrollbar_min_handle_height', + 'scrollbar_radius', + 'scrollbar_track_color', + 'scrollbar_track_hover_opacity', + 'scrollbar_track_opacity', + 'scrollbar_width', 'select_by_word_characters', 'select_by_word_characters_forward', 'selection_background', @@ -588,7 +601,19 @@ class Options: scrollback_lines: int = 2000 scrollback_pager: list[str] = ['less', '--chop-long-lines', '--RAW-CONTROL-CHARS', '+INPUT_LINE_NUMBER'] scrollback_pager_history_size: int = 0 - scrollbar: ScrollbarSettings = ScrollbarSettings() + scrollbar: choices_for_scrollbar = 'scrolled' + scrollbar_gap: float = 0.1 + scrollbar_handle_color: int = 0 + scrollbar_handle_opacity: float = 0.5 + scrollbar_hitbox_expansion: float = 0.25 + scrollbar_interactive: bool = True + scrollbar_jump_on_click: bool = True + scrollbar_min_handle_height: float = 1.0 + scrollbar_radius: float = 0.3 + scrollbar_track_color: int = 0 + scrollbar_track_hover_opacity: float = 0.1 + scrollbar_track_opacity: float = 0 + scrollbar_width: float = 0.5 select_by_word_characters: str = '@-./_~?&=%+#' select_by_word_characters_forward: str = '' selection_background: kitty.fast_data_types.Color | None = Color(255, 250, 205) diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 018073004..4b6fed3dc 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -41,7 +41,7 @@ from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_HOLLOW, CURSOR_UNDERLINE, NO_CURSOR_SHAPE, Color, Shlex, SingleKey from kitty.fonts import FontModification, FontSpec, ModificationType, ModificationUnit, ModificationValue from kitty.key_names import character_key_name_aliases, functional_key_name_aliases, get_key_name_lookup -from kitty.rgb import color_as_int, color_from_int +from kitty.rgb import color_as_int from kitty.types import FloatEdges, MouseEvent from kitty.utils import expandvars, log_error, resolve_abs_or_config_path, shlex_split @@ -859,11 +859,11 @@ def allow_hyperlinks(x: str) -> int: def color_with_special_values(x: str, special_values: dict[str, int], error_msg: str) -> int: x = x.strip('"') - if x in special_values: - return special_values[x] + if (ans := special_values.get(x)) is not None: + return ans & 0xff try: - return (color_as_int(to_color(x)) << 8) | 2 - except ValueError: + return (color_as_int(to_color(x)) << 8) | len(special_values) + except Exception: log_error(error_msg.format(x=x)) return 0 @@ -876,6 +876,14 @@ def titlebar_color(x: str) -> int: ) +def scrollbar_color(x: str) -> int: + return color_with_special_values( + x, + {'foreground': 0, 'selection_background': 1}, + 'Ignoring invalid scroll bar color: {x}' + ) + + def macos_titlebar_color(x: str) -> int: x = x.strip('"') if x == 'light': @@ -1704,107 +1712,6 @@ def transparent_background_colors(spec: str) -> tuple[tuple[Color, float], ...]: return tuple(ans[:7]) -class ScrollbarSettings(NamedTuple): - opacity: float = 0.5 - track_opacity: float = 0 - track_hover_opacity: float = 0.1 - color: int = 0 - track_color: int = 0 - interactive: bool = True - width: float = 0.5 - radius: float = 0.3 - gap: float = 0.1 - min_handle_height: float = 1.0 - hitbox_expansion: float = 0.25 - jump_on_track_click: bool = True - visible_when: int = 1 - - @classmethod - def color_as_string(cls, color_val: int) -> str: - match color_val: - case 0: - return 'foreground' - case 1: - return 'selection_background' - return color_from_int(color_val >> 8).as_sharp - - def differences(self, other: 'ScrollbarSettings') -> Iterator[str]: - for key in self._fields: - if getattr(self, key) != (o := getattr(other, key)): - yield f'{key}: {o}' - - - def __repr__(self) -> str: - defaults = self._field_defaults - parts = [] - for field_name in self._fields: - if (value := getattr(self, field_name)) != defaults[field_name]: - parts.append(f'{field_name}={value!r}') - return f'{self.__class__.__name__}({", ".join(parts)})' - - -default_scrollbar = ScrollbarSettings() - - -def scrollbar(spec: str) -> ScrollbarSettings: - if not spec: - return default_scrollbar - ans = ScrollbarSettings() - def scrollbar_color(x: str) -> int: - return color_with_special_values( - x, {'foreground': 0, 'selection_background': 1}, 'Ignoring invalid scrollbar color: {x}') - for x in shlex_split(spec): - k, sep, v = x.partition('=') - if sep != '=': - log_error(f'Ignoring invalid scrollbar option: {x}') - continue - match k: - case 'opacity': - ans = ans._replace(opacity=unit_float(v)) - case 'track_opacity': - ans = ans._replace(track_opacity=unit_float(v)) - case 'track_hover_opacity': - ans = ans._replace(track_hover_opacity=unit_float(v)) - case 'color': - ans = ans._replace(color=scrollbar_color(v)) - case 'track_color': - ans = ans._replace(track_color=scrollbar_color(v)) - case 'interactive': - ans = ans._replace(interactive=to_bool(v)) - case 'width': - ans = ans._replace(width=positive_float(v)) - case 'radius': - ans = ans._replace(radius=positive_float(v)) - case 'gap': - ans = ans._replace(gap=positive_float(v)) - case 'min_handle_height': - ans = ans._replace(min_handle_height=positive_float(v)) - case 'hitbox_expansion': - ans = ans._replace(hitbox_expansion=positive_float(v)) - case 'track_click': - ans = ans._replace(jump_on_track_click=v == 'jump') - case 'visible_when': - val = -1 - match v: - case 'never': - val = defines.SCROLLBAR_NEVER - case 'scrolled': - val = defines.SCROLLBAR_ON_SCROLLED - case 'hovered': - val = defines.SCROLLBAR_ON_HOVERED - case 'scrolled-and-hovered': - val = defines.SCROLLBAR_ON_SCROLL_AND_HOVER - case 'always': - val = defines.SCROLLBAR_ALWAYS - case _: - log_error(f'Ignoring unknown scrollbar visibility policy: {v}') - if val > -1: - ans = ans._replace(visible_when=val) - case _: - log_error(f'Ignoring unknown scrollbar option: {k}') - return ans - - def deprecated_hide_window_decorations_aliases(key: str, val: str, ans: dict[str, Any]) -> None: if not hasattr(deprecated_hide_window_decorations_aliases, key): setattr(deprecated_hide_window_decorations_aliases, key, True) @@ -1864,4 +1771,8 @@ def deprecated_scrollback_indicator_opacity(key: str, val: str, ans: dict[str, A if not hasattr(deprecated_scrollback_indicator_opacity, key): setattr(deprecated_scrollback_indicator_opacity, key, True) log_error(f'The option {key} is deprecated. Use scrollbar instead.') - ans['scrollbar'] = ScrollbarSettings(opacity=unit_float(val)) + op = unit_float(val) + if op <= 0.001: + ans['scrollbar'] = 'never' + else: + ans['scrollbar_handle_opacity'] = op diff --git a/kitty/shaders.c b/kitty/shaders.c index 371ecc413..249933640 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -743,7 +743,7 @@ draw_visual_bell(const UIRenderData *ui) { static bool has_scrollbar(Window *w, Screen *screen) { if (screen->linebuf != screen->main_linebuf || !screen->historybuf->count) return false; - switch (OPT(scrollbar.visible_when)) { + switch (OPT(scrollbar)) { case SCROLLBAR_NEVER: return false; case SCROLLBAR_ALWAYS: return true; case SCROLLBAR_ON_SCROLLED: return screen->scrolled_by > 0; @@ -887,8 +887,10 @@ set_color_uniform_with_opacity(color_type color, float opacity) { static color_type scrollbar_color(Screen *screen, unsigned val) { switch (val & 0xff) { - case 0: return colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.default_fg, screen->color_profile->configured.default_fg).rgb; - case 1: return colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.highlight_bg, screen->color_profile->configured.highlight_fg).rgb; +#define C(which) colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.which, screen->color_profile->configured.which).rgb + case 0: return C(default_fg); + case 1: return C(highlight_bg); +#undef C default: return val >> 8; } } @@ -899,13 +901,13 @@ draw_scrollbar(const UIRenderData *ui) { Window *window = ui->window; if (!window || !screen || !has_scrollbar(window, screen)) return; - color_type bar_color = scrollbar_color(screen, OPT(scrollbar.color)), track_color = scrollbar_color(screen, OPT(scrollbar.track_color)); + color_type bar_color = scrollbar_color(screen, OPT(scrollbar_handle_color)), track_color = scrollbar_color(screen, OPT(scrollbar_track_color)); float bar_frac = (float)screen->scrolled_by / MAX(1u, (float)screen->historybuf->count); - float opacity = OPT(scrollbar.opacity); - float track_opacity = window->scrollbar.is_hovering ? OPT(scrollbar.track_hover_opacity) : OPT(scrollbar.track_opacity); - GLsizei scrollbar_width_px = (GLsizei)(OPT(scrollbar.width) * ui->cell_width); - GLsizei scrollbar_gap_px = (GLsizei)(OPT(scrollbar.gap) * ui->cell_width); - unsigned scrollbar_radius = (unsigned)(OPT(scrollbar.radius) * ui->cell_width); + float opacity = OPT(scrollbar_handle_opacity); + float track_opacity = window->scrollbar.is_hovering ? OPT(scrollbar_track_hover_opacity) : OPT(scrollbar_track_opacity); + GLsizei scrollbar_width_px = (GLsizei)(OPT(scrollbar_width) * ui->cell_width); + GLsizei scrollbar_gap_px = (GLsizei)(OPT(scrollbar_gap) * ui->cell_width); + unsigned scrollbar_radius = (unsigned)(OPT(scrollbar_radius) * ui->cell_width); // Calculate window boundaries including padding GLsizei window_right_edge = ui->screen_left + ui->screen_width + window->render_data.geometry.spaces.right; @@ -919,7 +921,7 @@ draw_scrollbar(const UIRenderData *ui) { // Calculate thumb size and position float visible_fraction = (float)screen->lines / (float)(screen->lines + screen->historybuf->count); - float min_thumb_height_fraction = (OPT(scrollbar.min_handle_height) * ui->cell_height) / (float)window_height; + float min_thumb_height_fraction = (OPT(scrollbar_min_handle_height) * ui->cell_height) / (float)window_height; float thumb_height_fraction = MAX(min_thumb_height_fraction, visible_fraction); // Convert to OpenGL coordinates (range -1.0 to 1.0, total span = 2.0) diff --git a/kitty/state.h b/kitty/state.h index bc83e311c..2ccacf489 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -71,14 +71,12 @@ typedef struct Options { WindowTitleIn macos_show_window_title_in; char *bell_path, *bell_theme; float background_opacity, dim_opacity; - struct { - float opacity, track_opacity, track_hover_opacity; - color_type color, track_color; - bool interactive; - float width, radius, gap, min_handle_height, hitbox_expansion; - ScrollbarTrackBehavior track_behavior; - ScrollbarVisibilityPolicy visible_when; - } scrollbar; + + ScrollbarVisibilityPolicy scrollbar; + bool scrollbar_interactive, scrollbar_jump_on_click; + float scrollbar_width, scrollbar_radius, scrollbar_gap, scrollbar_min_handle_height, scrollbar_hitbox_expansion; + float scrollbar_handle_opacity, scrollbar_track_opacity, scrollbar_track_hover_opacity; + color_type scrollbar_handle_color, scrollbar_track_color; float text_contrast, text_gamma_adjustment; bool text_old_gamma; diff --git a/tools/themes/collection.go b/tools/themes/collection.go index f03db45ed..94022f2ed 100644 --- a/tools/themes/collection.go +++ b/tools/themes/collection.go @@ -310,6 +310,8 @@ var AllColorSettingNames = map[string]bool{ // {{{ "mark2_foreground": true, "mark3_background": true, "mark3_foreground": true, + "scrollbar_handle_color": true, + "scrollbar_track_color": true, "selection_background": true, "selection_foreground": true, "tab_bar_background": true,