Implement rendering of blinking text

Fixes #8551
This commit is contained in:
Kovid Goyal 2025-08-25 13:55:23 +05:30
parent 24049a1a5a
commit 14741b1b29
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
8 changed files with 41 additions and 19 deletions

View file

@ -135,6 +135,11 @@ Detailed list of changes
- Fix :opt:`background_opacity` being non-linear with light color themes
(:iss:`8869`)
- Add support for blinking text. Text marked as blinking now blinks in exact
rhythm with the cursor. The blinking animation and max duration are
controlled by :opt:`cursor_blink_interval` and
:opt:`cursor_stop_blinking_after`. (:pull:`8551`)
- Wayland: Fix incorrect window size calculation when transitioning from
full screen to non-full screen with client side decorations (:iss:`8826`)

View file

@ -11,7 +11,7 @@ layout(std140) uniform CellRenderData {
uint columns, lines, sprites_xnum, sprites_ynum, cursor_shape, cell_width, cell_height;
uint cursor_x1, cursor_x2, cursor_y1, cursor_y2;
float cursor_opacity, inactive_text_alpha, dim_opacity;
float cursor_opacity, inactive_text_alpha, dim_opacity, blink_opacity;
// must have unique entries with 0 being default_bg and unset being UINT32_MAX
uint bg_colors0, bg_colors1, bg_colors2, bg_colors3, bg_colors4, bg_colors5, bg_colors6, bg_colors7;
@ -262,8 +262,9 @@ void main() {
#ifndef ONLY_BACKGROUND // background does not depend on foreground
fg_as_uint = has_mark * color_table[NUM_COLORS + MARK_MASK + mark] + (ONE - has_mark) * fg_as_uint;
foreground = color_to_vec(fg_as_uint);
float has_dim = float((text_attrs >> DIM_SHIFT) & ONE);
effective_text_alpha = inactive_text_alpha * mix(1.0, dim_opacity, has_dim);
float has_dim = float((text_attrs >> DIM_SHIFT) & ONE), has_blink = float((text_attrs >> BLINK_SHIFT) & ONE);
effective_text_alpha = inactive_text_alpha * if_one_then(has_dim, dim_opacity, 1.0) * if_one_then(
has_blink, blink_opacity, 1.0);
float in_url = float((is_selected >> 1) & ONE);
decoration_fg = if_one_then(in_url, color_to_vec(url_color), to_color(colors[2], fg_as_uint));
// Selection

View file

@ -668,26 +668,32 @@ collect_cursor_info(CursorRenderInfo *ans, Window *w, monotonic_t now, OSWindow
cursor = rd->screen->paused_rendering.expires_at ? &rd->screen->paused_rendering.cursor : rd->screen->cursor;
ans->x = cursor->x; ans->y = cursor->y;
}
ans->is_visible = false; ans->multicursor_count = 0; ans->opacity = 1;
if (rd->screen->scrolled_by) return cursor_needs_render(w);
ans->multicursor_count = screen_multi_cursor_count(rd->screen);
ans->is_visible = screen_is_cursor_visible(rd->screen);
if (!ans->is_visible && ans->multicursor_count == 0) return cursor_needs_render(w);
ans->is_visible = false; ans->multicursor_count = 0; ans->cursor_opacity = 1; ans->text_blink_opacity = 1;
if (!rd->screen->scrolled_by) {
ans->multicursor_count = screen_multi_cursor_count(rd->screen);
ans->is_visible = screen_is_cursor_visible(rd->screen);
}
if (!ans->is_visible && ans->multicursor_count == 0 && !rd->screen->sgr_blink_was_used) return cursor_needs_render(w);
monotonic_t time_since_start_blink = now - os_window->cursor_blink_zero_time;
bool cursor_blinking = OPT(cursor_blink_interval) > 0 && !cursor->non_blinking && os_window->is_focused && (OPT(cursor_stop_blinking_after) == 0 || time_since_start_blink <= OPT(cursor_stop_blinking_after));
if (cursor_blinking) {
const bool allow_blinking = OPT(cursor_blink_interval) > 0;
const bool blink_has_ceased = OPT(cursor_stop_blinking_after) != 0 && time_since_start_blink > OPT(cursor_stop_blinking_after);
const bool cursor_blinking = !cursor->non_blinking && os_window->is_focused;
float blink_opacity = 1.f;
if (allow_blinking && !blink_has_ceased && (cursor_blinking || rd->screen->sgr_blink_was_used)) {
if (animation_is_valid(OPT(animation.cursor))) {
monotonic_t duration = OPT(cursor_blink_interval) * 2;
monotonic_t time_into_cycle = time_since_start_blink % duration;
double frac_into_cycle = (double)time_into_cycle / (double)duration;
ans->opacity = (float)apply_easing_curve(OPT(animation.cursor), frac_into_cycle, duration);
blink_opacity = (float)apply_easing_curve(OPT(animation.cursor), frac_into_cycle, duration);
set_maximum_wait(ANIMATION_SAMPLE_WAIT);
} else {
monotonic_t n = time_since_start_blink / OPT(cursor_blink_interval);
ans->opacity = 1 - n % 2;
blink_opacity = 1 - n % 2;
set_maximum_wait((n + 1) * OPT(cursor_blink_interval) - time_since_start_blink);
}
}
ans->text_blink_opacity = blink_opacity;
ans->cursor_opacity = cursor_blinking ? blink_opacity: 1.0f;
ans->shape = cursor->shape ? cursor->shape : OPT(cursor_shape);
ans->is_focused = os_window->is_focused;
return cursor_needs_render(w);
@ -776,7 +782,13 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int *
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
WD.screen->cursor_render_info.is_focused = false;
} else {
WD.screen->cursor_render_info.opacity = 0;
if (WD.screen->sgr_blink_was_used) {
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
WD.screen->cursor_render_info.is_focused = false;
} else {
WD.screen->cursor_render_info.text_blink_opacity = 1;
}
WD.screen->cursor_render_info.cursor_opacity = 0;
}
}
if (scan_for_animated_images) {

View file

@ -227,7 +227,7 @@ typedef struct {
bool is_focused, render_even_when_unfocused, is_visible;
CursorShape shape;
unsigned x, y, multicursor_count;
float opacity;
float cursor_opacity, text_blink_opacity;
} CursorRenderInfo;
typedef enum DynamicColorType {

View file

@ -363,14 +363,16 @@ it will go from opaque to transparent and then back again over the next half. Yo
different easing functions for the two halves, for example: :code:`-1 linear ease-out`. kitty
supports all the :link:`CSS easing functions <https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function>`.
Note that turning on animations uses extra power as it means the screen is redrawn multiple times
per blink interval. See also, :opt:`cursor_stop_blinking_after`.
per blink interval. See also, :opt:`cursor_stop_blinking_after`. This setting also controls blinking
text, which blinks in exact rhythm with the cursor.
''')
opt('cursor_stop_blinking_after', '15.0',
option_type='positive_float', ctype='time',
long_text='''
Stop blinking cursor after the specified number of seconds of keyboard
inactivity. Set to zero to never stop blinking.
inactivity. Set to zero to never stop blinking. This setting also controls
blinking text, which blinks in exact rhythm with the cursor.
''')
opt('cursor_trail', '0',

View file

@ -1399,6 +1399,7 @@ select_graphic_rendition(Screen *self, int *params, unsigned int count, bool is_
}
} else {
cursor_from_sgr(self->cursor, params, count, is_group);
self->sgr_blink_was_used |= self->cursor->sgr.blink;
}
}

View file

@ -114,7 +114,7 @@ typedef struct {
color_type cursor_bg;
CursorRenderInfo cursor;
} last_rendered;
bool is_dirty, scroll_changed, reload_all_gpu_data;
bool is_dirty, scroll_changed, reload_all_gpu_data, sgr_blink_was_used;
Cursor *cursor;
Savepoint main_savepoint, alt_savepoint;
PyObject *callbacks, *test_child;

View file

@ -442,7 +442,7 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, C
GLuint columns, lines, sprites_xnum, sprites_ynum, cursor_shape, cell_width, cell_height;
GLuint cursor_x1, cursor_x2, cursor_y1, cursor_y2;
GLfloat cursor_opacity, inactive_text_alpha, dim_opacity;
GLfloat cursor_opacity, inactive_text_alpha, dim_opacity, blink_opacity;
GLuint bg_colors0, bg_colors1, bg_colors2, bg_colors3, bg_colors4, bg_colors5, bg_colors6, bg_colors7;
GLfloat bg_opacities0, bg_opacities1, bg_opacities2, bg_opacities3, bg_opacities4, bg_opacities5, bg_opacities6, bg_opacities7;
@ -475,7 +475,8 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, C
rd->use_cell_for_selection_bg = IS_SPECIAL_COLOR(highlight_bg) ? 1. : 0.;
// Cursor position
Line *line_for_cursor = NULL;
rd->cursor_opacity = MAX(0, MIN(cursor->opacity, 1));
rd->cursor_opacity = MAX(0, MIN(cursor->cursor_opacity, 1));
rd->blink_opacity = MAX(0, MIN(cursor->text_blink_opacity, 1));
if (rd->cursor_opacity != 0 && cursor->is_visible) {
rd->cursor_x1 = cursor->x, rd->cursor_y1 = cursor->y;
rd->cursor_x2 = cursor->x, rd->cursor_y2 = cursor->y;