diff --git a/docs/changelog.rst b/docs/changelog.rst index cac05a47d..1e9254b5b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -668,7 +668,7 @@ Detailed list of changes - Wayland labwc: Fix kitty timing out waiting for compositor to quit fucking around with scales on labwc (:iss:`7540`) -- Fix :opt:`scrollback_indicator_opacity` not actually controlling the opacity (:iss:`7557`) +- Fix ``scrollback_indicator_opacity`` not actually controlling the opacity (:iss:`7557`) - URL detection: Fix IPv6 hostnames breaking URL detection (:iss:`7565`) @@ -752,7 +752,7 @@ Detailed list of changes using the panel kitten for all compositors that support the `requisite Wayland protocol `__ which is practically speaking all of them but GNOME (:pull:`2590`) -- Show a small :opt:`scrollback indicator ` along the right window edge when viewing +- Show a small scrollback indicator along the right window edge when viewing the scrollback to keep track of scroll position (:iss:`2502`) - Wayland: Support fractional scales so that there is no wasted drawing at larger scale followed by resizing in the compositor diff --git a/docs/overview.rst b/docs/overview.rst index 9d478d9be..a918f3fd6 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -197,9 +197,10 @@ The scrollback buffer ----------------------- |kitty| supports scrolling back to view history, just like most terminals. You -can use either keyboard shortcuts or the mouse scroll wheel to do so. While -you are browsing the scrollback a :opt:`small indicator ` -is displayed along the right edge of the window to show how far back you are. +can use either keyboard shortcuts or the mouse scroll wheel to do so. |kitty| +displays an interactive :opt:`scrollbar ` along the right edge +of the window that shows your current position in the scrollback. You can click +and drag the scrollbar to quickly navigate through the history. However, |kitty| has an extra, neat feature. Sometimes you need to explore the scrollback buffer in more detail, maybe search for some text or refer to it side-by-side diff --git a/kitty/data-types.h b/kitty/data-types.h index 8a799d9a2..274a96d80 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -113,6 +113,7 @@ typedef enum MouseShapes { /* end mouse shapes */ } MouseShape; typedef enum { NONE, MENUBAR, WINDOW, ALL } WindowTitleIn; +typedef enum { SCROLLBAR_TRACK_JUMP=1, SCROLLBAR_TRACK_PAGE=2 } ScrollbarTrackBehavior; typedef enum { TILING, SCALED, MIRRORED, CLAMPED, CENTER_CLAMPED, CENTER_SCALED } BackgroundImageLayout; typedef struct ImageAnchorPosition { float canvas_x, canvas_y, image_x, image_y; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 4d4ef0799..d0d65bada 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1387,7 +1387,8 @@ def set_tab_bar_render_data( def set_window_render_data( os_window_id: int, tab_id: int, window_id: int, screen: Screen, - left: int, top: int, right: int, bottom: int + left: int, top: int, right: int, bottom: int, + spaces_left: int, spaces_top: int, spaces_right: int, spaces_bottom: int ) -> None: pass diff --git a/kitty/mouse.c b/kitty/mouse.c index 706a1e52f..c25907a28 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -146,6 +146,26 @@ encode_mouse_scroll(Window *w, int button, int mods) { // }}} +// Scrollbar types and function declarations {{{ +typedef enum { + SCROLLBAR_HIT_NONE, + SCROLLBAR_HIT_TRACK, + SCROLLBAR_HIT_THUMB +} ScrollbarHitType; + +typedef struct { + double left, right, top, bottom; + double width, gap, hitbox_expansion; +} ScrollbarGeometry; + +static ScrollbarGeometry calculate_scrollbar_geometry(Window *w); +static ScrollbarHitType get_scrollbar_hit_type(Window *w, double mouse_x, double mouse_y); +static bool handle_scrollbar_mouse(Window *w, int button, MouseAction action, int modifiers); +static void handle_scrollbar_drag(Window *w, double mouse_y); +static void end_drag(Window *w); +static void update_scrollbar_hover_state(Window *w, bool hovering); +// }}} + static Window* window_for_id(id_type window_id) { if (global_state.callback_os_window && global_state.callback_os_window->num_tabs) { @@ -164,6 +184,9 @@ send_mouse_leave_event_if_needed(id_type currently_over_window, int modifiers) { Window *left_window = window_for_id(global_state.mouse_hover_in_window); global_state.mouse_hover_in_window = currently_over_window; if (left_window) { + if (left_window->scrollbar.is_hovering) { + update_scrollbar_hover_state(left_window, false); + } int sz = encode_mouse_event(left_window, 0, LEAVE, modifiers); if (sz > 0) { mouse_event_buf[sz] = 0; @@ -405,6 +428,11 @@ set_mouse_position(Window *w, bool *mouse_cell_changed, bool *cell_half_changed) HANDLER(handle_move_event) { modifiers &= ~GLFW_LOCK_MASK; + + if (handle_scrollbar_mouse(w, -1, MOVE, modifiers)) { + return; + } + if (OPT(focus_follows_mouse)) { Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; if (window_idx != t->active_window) { @@ -413,7 +441,12 @@ HANDLER(handle_move_event) { } bool mouse_cell_changed = false; bool cell_half_changed = false; - if (!set_mouse_position(w, &mouse_cell_changed, &cell_half_changed)) return; + if (!set_mouse_position(w, &mouse_cell_changed, &cell_half_changed)) { + if (w->scrollbar.is_hovering) { + update_scrollbar_hover_state(w, false); + } + return; + } Screen *screen = w->render_data.screen; if (OPT(detect_urls)) detect_url(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y); if (should_handle_in_kitty(w, screen, button)) { @@ -588,8 +621,15 @@ dispatch_possible_click(Window *w, int button, int modifiers) { HANDLER(handle_button_event) { modifiers &= ~GLFW_LOCK_MASK; + if (!global_state.callback_os_window) return; + Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; bool is_release = !global_state.callback_os_window->mouse_button_pressed[button]; + + if (handle_scrollbar_mouse(w, button, is_release ? RELEASE : PRESS, modifiers)) { + return; + } + if (window_idx != t->active_window && !is_release) { call_boss(switch_focus_to, "K", t->windows[window_idx].id); } @@ -712,10 +752,17 @@ update_mouse_pointer_shape(void) { bool in_tab_bar; unsigned int window_idx = 0; Window *w = window_for_event(&window_idx, &in_tab_bar); - if (in_tab_bar) { mouse_cursor_shape = POINTER_POINTER; } - else if (w && w->render_data.screen) { - screen_mark_url(w->render_data.screen, 0, 0, 0, 0); - set_mouse_cursor_for_screen(w->render_data.screen); + if (in_tab_bar) { + mouse_cursor_shape = POINTER_POINTER; + } else if (w) { + if (handle_scrollbar_mouse(w, -1, MOVE, 0)) { + return; + } + + if (w->render_data.screen) { + screen_mark_url(w->render_data.screen, 0, 0, 0, 0); + set_mouse_cursor_for_screen(w->render_data.screen); + } } set_mouse_cursor(mouse_cursor_shape); } @@ -748,6 +795,11 @@ enter_event(int modifiers) { send_mouse_leave_event_if_needed(w ? w->id : 0, modifiers); if (!w || in_tab_bar) return; global_state.mouse_hover_in_window = w->id; + + if (handle_scrollbar_mouse(w, -1, MOVE, modifiers)) { + return; + } + bool mouse_cell_changed = false, cell_half_changed = false; if (!set_mouse_position(w, &mouse_cell_changed, &cell_half_changed)) return; Screen *screen = w->render_data.screen; @@ -757,12 +809,194 @@ enter_event(int modifiers) { if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf); } } +static bool +validate_scrollbar_state(Window *w) { + return w && w->render_data.screen && + w->render_data.screen->historybuf && + w->render_data.screen->historybuf->count > 0; +} + +static ScrollbarGeometry +calculate_scrollbar_geometry(Window *w) { + ScrollbarGeometry geom = {0}; + if (!w) return geom; + + WindowGeometry *g = &w->render_data.geometry; + geom.width = OPT(scrollbar_width); + geom.gap = OPT(scrollbar_gap); + geom.hitbox_expansion = OPT(scrollbar_hitbox_expansion); + + double right_edge = g->right + g->spaces.right; + geom.left = right_edge - geom.gap - geom.width - geom.hitbox_expansion; + geom.right = right_edge + geom.gap; + geom.top = g->top - g->spaces.top; + geom.bottom = g->bottom + g->spaces.bottom; + + return geom; +} + +static void +update_scrollbar_hover_state(Window *w, bool hovering) { + if (!w) return; + bool changed = w->scrollbar.is_hovering != hovering; + w->scrollbar.is_hovering = hovering; + + if (changed && OPT(scrollbar_autohide) && global_state.callback_os_window) { + global_state.callback_os_window->needs_render = true; + request_tick_callback(); + } +} + +static ScrollbarHitType +get_scrollbar_hit_type(Window *w, double mouse_x, double mouse_y) { + if (!w || !validate_scrollbar_state(w)) return SCROLLBAR_HIT_NONE; + + ScrollbarGeometry geom = calculate_scrollbar_geometry(w); + + if (mouse_x < geom.left || mouse_x > geom.right || + mouse_y < geom.top || mouse_y > geom.bottom) { + return SCROLLBAR_HIT_NONE; + } + + OSWindow *os_window = global_state.callback_os_window; + if (!os_window) return SCROLLBAR_HIT_TRACK; + double mouse_window_fraction = mouse_y / os_window->viewport_height; + double hitbox_expansion_fraction = (double)OPT(scrollbar_hitbox_expansion) / 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)) { + return SCROLLBAR_HIT_THUMB; + } + + return SCROLLBAR_HIT_TRACK; +} + +static void +handle_scrollbar_track_click(Window *w, double mouse_y) { + if (!w) return; + Screen *screen = w->render_data.screen; + if (!validate_scrollbar_state(w)) return; + + if (OPT(scrollbar_track_behavior) == SCROLLBAR_TRACK_JUMP) { + ScrollbarGeometry geom = calculate_scrollbar_geometry(w); + double scrollbar_height = geom.bottom - geom.top; + double mouse_pane_fraction = (mouse_y - geom.top) / scrollbar_height; + unsigned int target_scrolled_by = (unsigned int)(screen->historybuf->count * (1.0 - mouse_pane_fraction)); + screen_history_scroll_to_absolute(screen, target_scrolled_by); + } else { + OSWindow *os_window = global_state.callback_os_window; + if (!os_window) return; + double mouse_window_fraction = mouse_y / os_window->viewport_height; + bool click_above_thumb = mouse_window_fraction < w->scrollbar.thumb_top; + screen_history_scroll(screen, SCROLL_PAGE, click_above_thumb); + } +} + +static void +start_scrollbar_drag(Window *w, double mouse_y) { + if (!w) return; + Screen *screen = w->render_data.screen; + if (!validate_scrollbar_state(w)) return; + + ScrollbarGeometry geom = calculate_scrollbar_geometry(w); + double scrollbar_height = geom.bottom - geom.top; + double mouse_pane_fraction = (mouse_y - geom.top) / scrollbar_height; + w->scrollbar.is_dragging = true; + w->scrollbar.drag_start_y = mouse_pane_fraction; + w->scrollbar.drag_start_scrolled_by = screen->scrolled_by; +} + +static bool +handle_scrollbar_mouse(Window *w, int button, MouseAction action, int modifiers UNUSED) { + if (!w || !OPT(scrollbar_interactive) || !global_state.callback_os_window) return false; + + double mouse_x = global_state.callback_os_window->mouse_x; + double mouse_y = global_state.callback_os_window->mouse_y; + + if (action == MOVE && w->scrollbar.is_dragging) { + handle_scrollbar_drag(w, mouse_y); + mouse_cursor_shape = DEFAULT_POINTER; + set_mouse_cursor(mouse_cursor_shape); + return true; + } + + ScrollbarHitType hit_type = get_scrollbar_hit_type(w, mouse_x, mouse_y); + bool hovering = (hit_type != SCROLLBAR_HIT_NONE); + update_scrollbar_hover_state(w, hovering); + + if (!hovering) return false; + + mouse_cursor_shape = DEFAULT_POINTER; + set_mouse_cursor(mouse_cursor_shape); + + if (button == GLFW_MOUSE_BUTTON_LEFT && action != MOVE) { + bool is_release = (action == RELEASE); + + if (is_release) { + if (w->scrollbar.is_dragging) { + end_drag(w); + } else if (hit_type == SCROLLBAR_HIT_TRACK) { + handle_scrollbar_track_click(w, mouse_y); + } + } else { + if (hit_type == SCROLLBAR_HIT_THUMB) { + start_scrollbar_drag(w, mouse_y); + global_state.active_drag_in_window = w->id; + global_state.active_drag_button = button; + } + } + } + + return true; +} + +static void +handle_scrollbar_drag(Window *w, double mouse_y) { + if (!w || !w->scrollbar.is_dragging) return; + + Screen *screen = w->render_data.screen; + if (!validate_scrollbar_state(w)) return; + + ScrollbarGeometry geom = calculate_scrollbar_geometry(w); + double scrollbar_height = geom.bottom - geom.top; + double mouse_pane_fraction = (mouse_y - geom.top) / scrollbar_height; + double delta_y = mouse_pane_fraction - w->scrollbar.drag_start_y; + double visible_fraction = (double)screen->lines / (screen->lines + screen->historybuf->count); + double min_thumb_height_fraction = (double)OPT(scrollbar_min_thumb_height) / scrollbar_height; + double thumb_height = MAX(min_thumb_height_fraction, visible_fraction); + double available_space = 1.0 - thumb_height; + + if (available_space > 0) { + double scroll_fraction = delta_y / available_space; + double target = w->scrollbar.drag_start_scrolled_by - scroll_fraction * screen->historybuf->count; + unsigned int new_scrolled_by; + if (target < 0) new_scrolled_by = 0; + else if (target > screen->historybuf->count) new_scrolled_by = screen->historybuf->count; + else new_scrolled_by = (unsigned int)target; + + if (new_scrolled_by != screen->scrolled_by) { + screen_history_scroll_to_absolute(screen, new_scrolled_by); + } + } +} + static void end_drag(Window *w) { Screen *screen = w->render_data.screen; global_state.active_drag_in_window = 0; global_state.active_drag_button = -1; w->last_drag_scroll_at = 0; + w->scrollbar.is_dragging = false; + + if (global_state.callback_os_window && + get_scrollbar_hit_type(w, + global_state.callback_os_window->mouse_x, + global_state.callback_os_window->mouse_y + ) == SCROLLBAR_HIT_NONE) { + mouse_cursor_shape = TEXT_POINTER; + set_mouse_cursor(mouse_cursor_shape); + } + if (screen->selections.in_progress) { screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, (SelectionUpdate){.ended=true}); } @@ -905,6 +1139,9 @@ mouse_event(const int button, int modifiers, int action) { if (global_state.mouse_hover_in_window) { Window *old_window = window_for_id(global_state.mouse_hover_in_window); if (old_window && old_window != w) { + if (old_window->scrollbar.is_hovering) { + update_scrollbar_hover_state(old_window, false); + } global_state.mouse_hover_in_window = 0; screen_mark_url(old_window->render_data.screen, 0, 0, 0, 0); } diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 7eb1247a9..5f8a37e8b 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -20,6 +20,7 @@ definition.add_deprecation('deprecated_hide_window_decorations_aliases', 'x11_hi definition.add_deprecation('deprecated_macos_show_window_title_in_menubar_alias', 'macos_show_window_title_in_menubar') definition.add_deprecation('deprecated_send_text', 'send_text') definition.add_deprecation('deprecated_adjust_line_height', 'adjust_line_height', 'adjust_column_width', 'adjust_baseline') +definition.add_deprecation('deprecated_scrollback_indicator_opacity', 'scrollback_indicator_opacity') agr = definition.add_group egr = definition.end_group @@ -441,12 +442,70 @@ is changed it will only affect newly created windows, not existing ones. ''' ) -opt('scrollback_indicator_opacity', '1.0', +opt('scrollbar_opacity', '0.5', option_type='unit_float', ctype='float', long_text=''' -The opacity of the scrollback indicator which is a small colored rectangle that moves -along the right hand side of the window as you scroll, indicating what fraction you -have scrolled. The default is one which means fully opaque, aka visible. -Set to a value between zero and one to make the indicator less visible.''') +The opacity of the scrollbar handle. The default is 0.5 which means 50% opaque. +Set to a value between zero and one.''') + +opt('scrollbar_track_opacity', '0', + option_type='unit_float', ctype='float', long_text=''' +The opacity of the scrollbar track (the background behind the scrollbar handle). +The default is 0 which means completely transparent. Set to a value between zero and one.''') + +opt('scrollbar_color', 'foreground', + option_type='scrollbar_color', long_text=''' +The color of the scrollbar. The default value :code:`foreground` uses the +current text color. You can specify any color as a hexadecimal RGB triplet (e.g. +:code:`#ff0000` for red), or use one of the standard color names (e.g. :code:`red`), +or use a color from the 256 color table (e.g. :code:`color120`). +Additionally, special values like :code:`background`, :code:`foreground`, or any other +configured color variable are supported. The scrollbar appearance is also affected by +:opt:`scrollbar_opacity` and :opt:`scrollbar_track_opacity` which control transparency.''') + +opt('scrollbar_interactive', 'yes', + option_type='to_bool', ctype='bool', long_text=''' +Enable or disable interactive scrollbar functionality. When enabled, you can click +and drag the scrollbar to scroll. When disabled, the scrollbar is only a visual +indicator of the scroll position. Set to :code:`yes` to enable or :code:`no` to disable.''') + +opt('scrollbar_width', '10', + option_type='positive_int', ctype='uint', long_text=''' +The width of the scrollbar in pixels. The default is 10 pixels.''') + +opt('scrollbar_gap', '5', + option_type='positive_int', ctype='uint', long_text=''' +The gap between the scrollbar and the window edge in pixels. The default is 5 pixels.''') +opt('scrollbar_min_thumb_height', '50', + option_type='positive_int', ctype='uint', long_text=''' +The minimum height of the scrollbar thumb in pixels. This prevents the thumb from +becoming too small when there is a lot of content to scroll. The default is 50 pixels.''') +opt('scrollbar_hitbox_expansion', '5', + option_type='positive_int', ctype='uint', long_text=''' +Extra pixels added to the scrollbar thumb hitbox for easier interaction. +This makes it easier to grab the scrollbar even if the visual representation +is thin. The default is 5 pixels on each side.''') + +opt('scrollbar_radius', '5', + option_type='positive_int', ctype='uint', long_text=''' +The radius of the scrollbar thumb corners in pixels. This controls how rounded +the scrollbar thumb appears. Set to 0 for square corners, or a positive value +for rounded corners. The default is 5 pixels.''') + +opt('scrollbar_autohide', 'yes', + option_type='to_bool', ctype='bool', long_text=''' +Hide the scrollbar by default and only show it when scrolling or hovering. +When enabled, the scrollbar will only be visible when you are scrolling +(not at the bottom) or when the mouse is hovering over the scrollbar area. +Set to :code:`yes` to enable or :code:`no` to disable.''') + +opt('scrollbar_track_behavior', 'jump', + option_type='choices', ctype='scrollbar_track_behavior', + choices=('jump', 'page'), + long_text=''' +Control the behavior when clicking on the scrollbar track (the area outside +the thumb). With :code:`jump`, clicking on the track will jump directly to +that position. With :code:`page`, clicking on the track will scroll up or +down by one page, similar to traditional scrollbar behavior.''') 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 a98aa5ccb..f0ef9ccb1 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -12,13 +12,14 @@ from kitty.options.utils import ( clear_all_mouse_actions, clear_all_shortcuts, clipboard_control, clone_source_strategies, config_or_absolute_path, confirm_close, copy_on_select, cursor_blink_interval, cursor_text_color, cursor_trail_decay, deprecated_adjust_line_height, deprecated_hide_window_decorations_aliases, - deprecated_macos_show_window_title_in_menubar_alias, deprecated_send_text, disable_ligatures, - edge_width, env, filter_notification, font_features, 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, 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, + deprecated_macos_show_window_title_in_menubar_alias, deprecated_scrollback_indicator_opacity, + deprecated_send_text, disable_ligatures, edge_width, env, filter_notification, font_features, + 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_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, @@ -1196,9 +1197,6 @@ class Parser: def scrollback_fill_enlarged_window(self, val: str, ans: dict[str, typing.Any]) -> None: ans['scrollback_fill_enlarged_window'] = to_bool(val) - def scrollback_indicator_opacity(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['scrollback_indicator_opacity'] = unit_float(val) - def scrollback_lines(self, val: str, ans: dict[str, typing.Any]) -> None: ans['scrollback_lines'] = scrollback_lines(val) @@ -1208,6 +1206,44 @@ class Parser: def scrollback_pager_history_size(self, val: str, ans: dict[str, typing.Any]) -> None: ans['scrollback_pager_history_size'] = scrollback_pager_history_size(val) + def scrollbar_autohide(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_autohide'] = to_bool(val) + + def scrollbar_color(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_color'] = scrollbar_color(val) + + def scrollbar_gap(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_gap'] = positive_int(val) + + def scrollbar_hitbox_expansion(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_hitbox_expansion'] = positive_int(val) + + def scrollbar_interactive(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_interactive'] = to_bool(val) + + def scrollbar_min_thumb_height(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_min_thumb_height'] = positive_int(val) + + def scrollbar_opacity(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_opacity'] = unit_float(val) + + def scrollbar_radius(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_radius'] = positive_int(val) + + def scrollbar_track_behavior(self, val: str, ans: dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_scrollbar_track_behavior: + raise ValueError(f"The value {val} is not a valid choice for scrollbar_track_behavior") + ans["scrollbar_track_behavior"] = val + + choices_for_scrollbar_track_behavior = frozenset(('jump', 'page')) + + def scrollbar_track_opacity(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_track_opacity'] = unit_float(val) + + def scrollbar_width(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['scrollbar_width'] = positive_int(val) + def select_by_word_characters(self, val: str, ans: dict[str, typing.Any]) -> None: ans['select_by_word_characters'] = str(val) @@ -1459,6 +1495,9 @@ class Parser: def adjust_baseline(self, val: str, ans: dict[str, typing.Any]) -> None: deprecated_adjust_line_height('adjust_baseline', val, ans) + def scrollback_indicator_opacity(self, val: str, ans: dict[str, typing.Any]) -> None: + deprecated_scrollback_indicator_opacity('scrollback_indicator_opacity', val, ans) + def map(self, val: str, ans: dict[str, typing.Any]) -> None: for k in parse_map(val): ans['map'].append(k) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 788312c05..52d5d3cc8 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -253,15 +253,132 @@ convert_from_opts_cursor_trail_color(PyObject *py_opts, Options *opts) { } static void -convert_from_python_scrollback_indicator_opacity(PyObject *val, Options *opts) { - opts->scrollback_indicator_opacity = PyFloat_AsFloat(val); +convert_from_python_scrollbar_opacity(PyObject *val, Options *opts) { + opts->scrollbar_opacity = PyFloat_AsFloat(val); } static void -convert_from_opts_scrollback_indicator_opacity(PyObject *py_opts, Options *opts) { - PyObject *ret = PyObject_GetAttrString(py_opts, "scrollback_indicator_opacity"); +convert_from_opts_scrollbar_opacity(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_opacity"); if (ret == NULL) return; - convert_from_python_scrollback_indicator_opacity(ret, opts); + convert_from_python_scrollbar_opacity(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_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_width(PyObject *val, Options *opts) { + opts->scrollbar_width = PyLong_AsUnsignedLong(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_gap(PyObject *val, Options *opts) { + opts->scrollbar_gap = PyLong_AsUnsignedLong(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_thumb_height(PyObject *val, Options *opts) { + opts->scrollbar_min_thumb_height = PyLong_AsUnsignedLong(val); +} + +static void +convert_from_opts_scrollbar_min_thumb_height(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_min_thumb_height"); + if (ret == NULL) return; + convert_from_python_scrollbar_min_thumb_height(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_hitbox_expansion(PyObject *val, Options *opts) { + opts->scrollbar_hitbox_expansion = PyLong_AsUnsignedLong(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_radius(PyObject *val, Options *opts) { + opts->scrollbar_radius = PyLong_AsUnsignedLong(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_autohide(PyObject *val, Options *opts) { + opts->scrollbar_autohide = PyObject_IsTrue(val); +} + +static void +convert_from_opts_scrollbar_autohide(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_autohide"); + if (ret == NULL) return; + convert_from_python_scrollbar_autohide(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_scrollbar_track_behavior(PyObject *val, Options *opts) { + opts->scrollbar_track_behavior = scrollbar_track_behavior(val); +} + +static void +convert_from_opts_scrollbar_track_behavior(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "scrollbar_track_behavior"); + if (ret == NULL) return; + convert_from_python_scrollbar_track_behavior(ret, opts); Py_DECREF(ret); } @@ -1228,7 +1345,25 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_cursor_trail_color(py_opts, opts); if (PyErr_Occurred()) return false; - convert_from_opts_scrollback_indicator_opacity(py_opts, opts); + convert_from_opts_scrollbar_opacity(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_interactive(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_gap(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_min_thumb_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_radius(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_autohide(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_scrollbar_track_behavior(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_scrollback_pager_history_size(py_opts, opts); if (PyErr_Occurred()) return false; diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index 2089aa17c..5924c183d 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -58,6 +58,17 @@ window_title_in(PyObject *title_in) { return ALL; } +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; + } + return SCROLLBAR_TRACK_JUMP; +} + static inline unsigned undercurl_style(PyObject *x) { RAII_PyObject(thick, PyUnicode_FromString("thick")); diff --git a/kitty/options/types.py b/kitty/options/types.py index d68dabe70..c69737645 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -27,6 +27,7 @@ choices_for_macos_colorspace = typing.Literal['srgb', 'default', 'displayp3'] 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_track_behavior = typing.Literal['jump', 'page'] 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'] @@ -411,10 +412,20 @@ option_names = ( 'resize_debounce_time', 'resize_in_steps', 'scrollback_fill_enlarged_window', - 'scrollback_indicator_opacity', 'scrollback_lines', 'scrollback_pager', 'scrollback_pager_history_size', + 'scrollbar_autohide', + 'scrollbar_color', + 'scrollbar_gap', + 'scrollbar_hitbox_expansion', + 'scrollbar_interactive', + 'scrollbar_min_thumb_height', + 'scrollbar_opacity', + 'scrollbar_radius', + 'scrollbar_track_behavior', + 'scrollbar_track_opacity', + 'scrollbar_width', 'select_by_word_characters', 'select_by_word_characters_forward', 'selection_background', @@ -585,10 +596,20 @@ class Options: resize_debounce_time: tuple[float, float] = (0.1, 0.5) resize_in_steps: bool = False scrollback_fill_enlarged_window: bool = False - scrollback_indicator_opacity: float = 1.0 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_autohide: bool = True + scrollbar_color: int = 0 + scrollbar_gap: int = 5 + scrollbar_hitbox_expansion: int = 5 + scrollbar_interactive: bool = True + scrollbar_min_thumb_height: int = 50 + scrollbar_opacity: float = 0.5 + scrollbar_radius: int = 5 + scrollbar_track_behavior: choices_for_scrollbar_track_behavior = 'jump' + scrollbar_track_opacity: float = 0 + scrollbar_width: int = 10 select_by_word_characters: str = '@-./_~?&=%+#' select_by_word_characters_forward: str = '' selection_background: kitty.fast_data_types.Color | None = Color(255, 250, 205) @@ -939,7 +960,7 @@ defaults.map = [ ] if is_macos: - defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=99), definition='copy_to_clipboard')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=99), definition='copy_or_noop')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=118), definition='paste_from_clipboard')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=57354), definition='scroll_line_up')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57352), definition='scroll_line_up')) diff --git a/kitty/options/utils.py b/kitty/options/utils.py index d02365a1f..1849d7418 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -857,19 +857,25 @@ def allow_hyperlinks(x: str) -> int: return 1 if to_bool(x) else 0 -def titlebar_color(x: str) -> int: +def color_with_special_values(x: str, special_values: dict[str, int], error_msg: str) -> int: x = x.strip('"') - if x == 'system': - return 0 - if x == 'background': - return 1 + if x in special_values: + return special_values[x] try: return (color_as_int(to_color(x)) << 8) | 2 except ValueError: - log_error(f'Ignoring invalid title bar color: {x}') + log_error(error_msg.format(x=x)) return 0 +def titlebar_color(x: str) -> int: + return color_with_special_values( + x, + {'system': 0, 'background': 1}, + 'Ignoring invalid title bar color: {x}' + ) + + def macos_titlebar_color(x: str) -> int: x = x.strip('"') if x == 'light': @@ -1751,3 +1757,19 @@ def deprecated_adjust_line_height(key: str, x: str, opts_dict: dict[str, Any]) - opts_dict['modify_font'][fm] = FontModification(mtype, ModificationValue(ans, ModificationUnit.percent)) else: opts_dict['modify_font'][fm] = FontModification(mtype, ModificationValue(int(x), ModificationUnit.pixel)) + + +def deprecated_scrollback_indicator_opacity(key: str, val: str, ans: dict[str, Any]) -> None: + 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_opacity instead.') + from kitty.conf.utils import unit_float + ans['scrollbar_opacity'] = unit_float(val) + + +def scrollbar_color(x: str) -> int: + return color_with_special_values( + x, + {'foreground': 0, 'selection_foreground': 1}, + 'Ignoring invalid scrollbar color: {x}' + ) diff --git a/kitty/screen.c b/kitty/screen.c index f2d3e774d..9fc1a0a2b 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -4921,6 +4921,16 @@ screen_selection_range_for_word(Screen *self, const index_type x, const index_ty #undef is_ok } +void +screen_history_scroll_to_absolute(Screen *self, unsigned int target_scrolled_by) { + if (self->linebuf != self->main_linebuf) return; + if (target_scrolled_by > self->historybuf->count) target_scrolled_by = self->historybuf->count; + if (target_scrolled_by != self->scrolled_by) { + self->scrolled_by = target_scrolled_by; + dirty_scroll(self); + } +} + bool screen_history_scroll(Screen *self, int amt, bool upwards) { switch(amt) { diff --git a/kitty/screen.h b/kitty/screen.h index fb6122e00..6e9a0f097 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -276,6 +276,7 @@ typedef struct SelectionUpdate { } SelectionUpdate; void screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half, SelectionUpdate upd); bool screen_history_scroll(Screen *self, int amt, bool upwards); +void screen_history_scroll_to_absolute(Screen *self, unsigned int target_scrolled_by); PyObject* as_text_history_buf(HistoryBuf *self, PyObject *args, ANSIBuf *output); Line* screen_visual_line(Screen *self, index_type y); void screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y); diff --git a/kitty/shaders.c b/kitty/shaders.c index 495d9e727..6704c1d3f 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -10,9 +10,11 @@ #include "cleanup.h" #include "colors.h" #include +#include #include "window_logo.h" #include "srgb_gamma.h" #include "uniforms_generated.h" +#include "state.h" enum { CELL_PROGRAM, CELL_FG_PROGRAM, CELL_BG_PROGRAM, CELL_PROGRAM_SENTINEL, @@ -739,22 +741,9 @@ draw_visual_bell(const UIRenderData *ui) { static bool has_scrollbar(Screen *screen) { - return OPT(scrollback_indicator_opacity) > 0 && screen->linebuf == screen->main_linebuf && screen->scrolled_by; + return OPT(scrollbar_opacity) > 0 && screen->linebuf == screen->main_linebuf && screen->historybuf->count > 0; } -static bool -draw_scroll_indicator(color_type bar_color, GLfloat alpha, float frac, const UIRenderData *ui) { - bind_program(TINT_PROGRAM); -#define C(shift) srgb_color((bar_color >> shift) & 0xFF) * alpha - glUniform4f(tint_program_layout.uniforms.tint_color, C(16), C(8), C(0), alpha); -#undef C - float bar_width = 0.5f * gl_size(ui->cell_width, ui->screen_width); - float bar_height = gl_size(ui->cell_height, ui->screen_height); - float bottom = -1.f + MAX(0, 2.f - bar_height) * frac; - glUniform4f(tint_program_layout.uniforms.edges, 1.f - bar_width, bottom + bar_height, 1.f, bottom); - draw_quad(true, 0); - return true; -} static unsigned render_a_bar(const UIRenderData *ui, WindowBarData *bar, PyObject *title, bool along_bottom) { @@ -878,13 +867,115 @@ draw_window_number(const UIRenderData *ui) { #undef lr } +// Helper function to extract and apply opacity to color components +static void +set_color_uniform_with_opacity(color_type color, float opacity) { + float r = srgb_color((color >> 16) & 0xFF) * opacity; + float g = srgb_color((color >> 8) & 0xFF) * opacity; + float b = srgb_color(color & 0xFF) * opacity; + glUniform4f(tint_program_layout.uniforms.tint_color, r, g, b, opacity); +} + static void draw_scrollbar(const UIRenderData *ui) { if (!has_scrollbar(ui->screen)) return; Screen *screen = ui->screen; - color_type bar_color = colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.highlight_bg, screen->color_profile->configured.highlight_bg).rgb; + Window *window = ui->window; + if (!window) return; + + color_type bar_color; + unsigned int val = OPT(scrollbar_color); + switch (val & 0xff) { + case 0: bar_color = colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.default_fg, screen->color_profile->configured.default_fg).rgb; break; + case 1: bar_color = colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.highlight_fg, screen->color_profile->configured.highlight_fg).rgb; break; + default: bar_color = val >> 8; break; + } + + // Division by zero is safe here because has_scrollbar() ensures historybuf->count > 0 float bar_frac = (float)screen->scrolled_by / (float)screen->historybuf->count; - draw_scroll_indicator(bar_color, OPT(scrollback_indicator_opacity), bar_frac, ui); + + if (OPT(scrollbar_autohide) && !window->scrollbar.is_hovering && screen->scrolled_by == 0) return; + + float opacity = OPT(scrollbar_opacity); + float track_opacity = OPT(scrollbar_track_opacity); + + GLsizei scrollbar_width_px = OPT(scrollbar_width); + GLsizei scrollbar_gap_px = OPT(scrollbar_gap); + unsigned int scrollbar_radius = OPT(scrollbar_radius); + + // Calculate window boundaries including padding + GLsizei window_right_edge = ui->screen_left + ui->screen_width + window->render_data.geometry.spaces.right; + GLsizei window_top_edge = ui->screen_top - window->render_data.geometry.spaces.top; + GLsizei window_height = ui->screen_height + window->render_data.geometry.spaces.top + window->render_data.geometry.spaces.bottom; + + // Position scrollbar on right side with gap + GLsizei scrollbar_left = window_right_edge - scrollbar_width_px - scrollbar_gap_px; + GLsizei scrollbar_top = window_top_edge + scrollbar_gap_px; + GLsizei scrollbar_height = window_height - 2 * scrollbar_gap_px; + + // Calculate thumb size and position + float visible_fraction = (float)screen->lines / (float)(screen->lines + screen->historybuf->count); + float min_thumb_height_fraction = (float)OPT(scrollbar_min_thumb_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) + const float GL_COORD_SPAN = 2.0f; + float thumb_height_gl = thumb_height_fraction * GL_COORD_SPAN; + float available_space = GL_COORD_SPAN - thumb_height_gl; + float thumb_bottom_gl = -1.0f + available_space * bar_frac; + float thumb_top_gl = thumb_bottom_gl + thumb_height_gl; + + // Store thumb position for mouse interaction (normalized window coordinates) + float scrollbar_top_in_window = (float)(window_top_edge + scrollbar_gap_px) / (float)ui->full_framebuffer_height; + float scrollbar_height_in_window = (float)(window_height - 2 * scrollbar_gap_px) / (float)ui->full_framebuffer_height; + float thumb_top_fraction = (1.0f - thumb_top_gl) / 2.0f; + float thumb_bottom_fraction = (1.0f - thumb_bottom_gl) / 2.0f; + window->scrollbar.thumb_top = scrollbar_top_in_window + thumb_top_fraction * scrollbar_height_in_window; + window->scrollbar.thumb_bottom = scrollbar_top_in_window + thumb_bottom_fraction * scrollbar_height_in_window; + + // Set viewport for scrollbar area + save_viewport_using_top_left_origin( + scrollbar_left, scrollbar_top, scrollbar_width_px, scrollbar_height, + ui->full_framebuffer_height + ); + + // Draw scrollbar track (background) + bind_program(TINT_PROGRAM); + set_color_uniform_with_opacity(bar_color, track_opacity); + glUniform4f(tint_program_layout.uniforms.edges, -1.f, 1.f, 1.f, -1.f); + draw_quad(true, 0); + + // Draw scrollbar thumb (handle) + if (scrollbar_radius > 0) { + // Rounded thumb - use separate viewport and rounded rect program + GLsizei thumb_height_px = (GLsizei)(thumb_height_fraction * scrollbar_height); + GLsizei thumb_top_px = scrollbar_top + (GLsizei)(thumb_top_fraction * scrollbar_height); + + restore_viewport(); + + bind_program(ROUNDED_RECT_PROGRAM); + color_vec4(rounded_rect_program_layout.uniforms.color, bar_color, opacity); + color_vec4(rounded_rect_program_layout.uniforms.background_color, 0, 0.0f); + + float y = (float)ui->full_framebuffer_height - (float)(thumb_top_px + thumb_height_px); + glUniform4f(rounded_rect_program_layout.uniforms.rect, + (float)scrollbar_left, y, + (float)scrollbar_width_px, (float)thumb_height_px); + + float thickness = (float)MAX(scrollbar_width_px, thumb_height_px); + glUniform2f(rounded_rect_program_layout.uniforms.params, thickness, (float)scrollbar_radius); + + save_viewport_using_top_left_origin(scrollbar_left, thumb_top_px, + scrollbar_width_px, thumb_height_px, + ui->full_framebuffer_height); + draw_quad(true, 0); + restore_viewport(); + } else { + set_color_uniform_with_opacity(bar_color, opacity); + glUniform4f(tint_program_layout.uniforms.edges, -1.f, thumb_top_gl, 1.f, thumb_bottom_gl); + draw_quad(true, 0); + restore_viewport(); + } } static void diff --git a/kitty/state.c b/kitty/state.c index 059b4c50c..13e9119ab 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -293,6 +293,7 @@ initialize_window(Window *w, PyObject *title, bool init_gpu_resources) { w->visible = true; w->title = title; Py_XINCREF(title); + w->scrollbar.is_hovering = false; if (!set_window_logo(w, OPT(default_window_logo), OPT(window_logo_position), OPT(window_logo_alpha), true, NULL, 0)) { log_error("Failed to load default window logo: %s", OPT(default_window_logo)); if (PyErr_Occurred()) PyErr_Print(); @@ -987,16 +988,20 @@ PYWRAP1(set_window_padding) { PYWRAP1(set_window_render_data) { #define B(name) &(g.name) +#define S(name) &(g.spaces.name) id_type os_window_id, tab_id, window_id; WindowGeometry g = {0}; Screen *screen; - PA("KKKOIIII", &os_window_id, &tab_id, &window_id, &screen, B(left), B(top), B(right), B(bottom)); + PA("KKKOIIIIIIII", &os_window_id, &tab_id, &window_id, &screen, + B(left), B(top), B(right), B(bottom), + S(left), S(top), S(right), S(bottom)); WITH_WINDOW(os_window_id, tab_id, window_id); init_window_render_data(&window->render_data, g, screen); END_WITH_WINDOW; Py_RETURN_NONE; #undef B +#undef S } PYWRAP1(update_window_visibility) { diff --git a/kitty/state.h b/kitty/state.h index 6498be60b..fb5998e6c 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -71,6 +71,17 @@ typedef struct Options { WindowTitleIn macos_show_window_title_in; char *bell_path, *bell_theme; float background_opacity, dim_opacity, scrollback_indicator_opacity; + bool scrollbar_interactive; + float scrollbar_opacity; + float scrollbar_track_opacity; + color_type scrollbar_color; + unsigned int scrollbar_width; + unsigned int scrollbar_gap; + unsigned int scrollbar_min_thumb_height; + unsigned int scrollbar_hitbox_expansion; + unsigned int scrollbar_radius; + bool scrollbar_autohide; + ScrollbarTrackBehavior scrollbar_track_behavior; float text_contrast, text_gamma_adjustment; bool text_old_gamma; @@ -144,6 +155,9 @@ typedef struct WindowLogoRenderData { typedef struct { unsigned int left, top, right, bottom; + struct { + unsigned int left, top, right, bottom; + } spaces; } WindowGeometry; typedef struct WindowRenderData { @@ -214,6 +228,13 @@ typedef struct Window { PendingClick *clicks; size_t num, capacity; } pending_clicks; + struct { + double thumb_top, thumb_bottom; + bool is_dragging; + double drag_start_y; + double drag_start_scrolled_by; + bool is_hovering; + } scrollbar; } Window; typedef struct BorderRect { diff --git a/kitty/window.py b/kitty/window.py index df4c67f41..41ace3326 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -970,7 +970,9 @@ class Window: mark_os_window_dirty(self.os_window_id) self.geometry = g = new_geometry - set_window_render_data(self.os_window_id, self.tab_id, self.id, self.screen, *g[:4]) + set_window_render_data(self.os_window_id, self.tab_id, self.id, self.screen, + g.left, g.top, g.right, g.bottom, + g.spaces.left, g.spaces.top, g.spaces.right, g.spaces.bottom) self.update_effective_padding() if update_ime_position: update_ime_position_for_window(self.id, True)