diff --git a/docs/changelog.rst b/docs/changelog.rst index f8d2a10e4..46494e529 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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`) diff --git a/kitty/cell_vertex.glsl b/kitty/cell_vertex.glsl index 32b50de52..f35a5d3a8 100644 --- a/kitty/cell_vertex.glsl +++ b/kitty/cell_vertex.glsl @@ -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 diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 356b7a50d..60fb26a1c 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -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) { diff --git a/kitty/data-types.h b/kitty/data-types.h index 4867f9927..8a799d9a2 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -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 { diff --git a/kitty/options/definition.py b/kitty/options/definition.py index c8c7e58d6..fe32de614 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -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 `. 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', diff --git a/kitty/screen.c b/kitty/screen.c index a18ab3cda..f5c660016 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -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; } } diff --git a/kitty/screen.h b/kitty/screen.h index f899dc345..51dbc19c3 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -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; diff --git a/kitty/shaders.c b/kitty/shaders.c index 70173ce3d..d443ce59b 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -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;