Merge branch 'master' of https://github.com/lxe/kitty into scrollbar

This commit is contained in:
Kovid Goyal 2025-09-14 09:48:08 +05:30
commit a6e64845ef
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
17 changed files with 716 additions and 59 deletions

View file

@ -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 <https://wayland.app/protocols/wlr-layer-shell-unstable-v1>`__ which is practically speaking all of them but GNOME (:pull:`2590`)
- Show a small :opt:`scrollback indicator <scrollback_indicator_opacity>` 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

View file

@ -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 <scrollback_indicator_opacity>`
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 <scrollbar_opacity>` 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

View file

@ -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;

View file

@ -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

View file

@ -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);
}

View file

@ -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',

59
kitty/options/parse.py generated
View file

@ -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)

View file

@ -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;

View file

@ -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"));

27
kitty/options/types.py generated
View file

@ -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'))

View file

@ -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}'
)

View file

@ -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) {

View file

@ -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);

View file

@ -10,9 +10,11 @@
#include "cleanup.h"
#include "colors.h"
#include <stddef.h>
#include <string.h>
#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

View file

@ -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) {

View file

@ -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 {

View file

@ -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)