Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cursor Trail Feature to Enhance Cursor Visibility #7970

Merged
merged 13 commits into from
Oct 18, 2024
Merged
5 changes: 5 additions & 0 deletions kitty/child-monitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,10 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int *
WD.screen->cursor_render_info.is_focused = os_window->is_focused;
set_os_window_title_from_window(w, os_window);
*active_window_bg = window_bg;
if (OPT(enable_cursor_trail) && update_cursor_trail(&tab->cursor_trail, w, now, os_window)) {
needs_render = true;
set_maximum_wait(OPT(repaint_delay));
}
} else {
if (WD.screen->cursor_render_info.render_even_when_unfocused) {
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
Expand Down Expand Up @@ -808,6 +812,7 @@ render_prepared_os_window(OSWindow *os_window, unsigned int active_window_id, co
w->cursor_opacity_at_last_render = WD.screen->cursor_render_info.opacity; w->last_cursor_shape = WD.screen->cursor_render_info.shape;
}
}
if (OPT(enable_cursor_trail) && tab->cursor_trail.needs_render) draw_cursor_trail(&tab->cursor_trail);
if (os_window->live_resize.in_progress) draw_resizing_text(os_window);
swap_window_buffers(os_window);
os_window->last_active_tab = os_window->active_tab; os_window->last_num_tabs = os_window->num_tabs; os_window->last_active_window_id = active_window_id;
Expand Down
106 changes: 106 additions & 0 deletions kitty/cursor_trail.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include "state.h"

inline static float
norm(float x, float y) {
return sqrtf(x * x + y * y);
}

inline static bool
get_cursor_edge(float *left, float *right, float *top, float *bottom, Window *w) {
#define WD w->render_data
*left = WD.xstart + WD.screen->cursor_render_info.x * WD.dx;
*bottom = WD.ystart - (WD.screen->cursor_render_info.y + 1) * WD.dy;
switch (WD.screen->cursor_render_info.shape) {
case CURSOR_BLOCK:
case CURSOR_HOLLOW:
*right = *left + WD.dx;
*top = *bottom + WD.dy;
return true;
case CURSOR_BEAM:
*right = *left + WD.dx / WD.screen->cell_size.width * OPT(cursor_beam_thickness);
*top = *bottom + WD.dy;
return true;
case CURSOR_UNDERLINE:
*right = *left + WD.dx;
*top = *bottom + WD.dy / WD.screen->cell_size.height * OPT(cursor_underline_thickness);
return true;
default:
return false;
}
}

bool
update_cursor_trail(CursorTrail *ct, Window *w, monotonic_t now, OSWindow *os_window) {
// the trail corners move towards the cursor corner at a speed proportional to their distance from the cursor corner.
// equivalent to exponential ease out animation.

static const int ci[4][2] = {{1, 0}, {1, 1}, {0, 1}, {0, 0}};
const float dx_threshold = WD.dx / WD.screen->cell_size.width;
const float dy_threshold = WD.dy / WD.screen->cell_size.height;
bool needs_render_prev = ct->needs_render;
ct->needs_render = false;

#define EDGE(axis, index) ct->cursor_edge_##axis[index]

if (!WD.screen->paused_rendering.expires_at && !get_cursor_edge(&EDGE(x, 0), &EDGE(x, 1), &EDGE(y, 0), &EDGE(y, 1), w)) {
return needs_render_prev;
}

// the decay time for the trail to reach 1/1024 of its distance from the cursor corner
float decay_fast = OPT(cursor_trail_decay_fast);
float decay_slow = OPT(cursor_trail_decay_slow);

if (os_window->live_resize.in_progress) {
for (int i = 0; i < 4; ++i) {
ct->corner_x[i] = EDGE(x, ci[i][0]);
ct->corner_y[i] = EDGE(y, ci[i][1]);
}
}
else if (OPT(input_delay) < now - WD.screen->cursor->updated_at && ct->updated_at < now) {
float cursor_center_x = (EDGE(x, 0) + EDGE(x, 1)) * 0.5f;
float cursor_center_y = (EDGE(y, 0) + EDGE(y, 1)) * 0.5f;
float cursor_diag_2 = norm(EDGE(x, 1) - EDGE(x, 0), EDGE(y, 1) - EDGE(y, 0)) * 0.5f;
float dt = (float)monotonic_t_to_s_double(now - ct->updated_at);

for (int i = 0; i < 4; ++i) {
float dx = EDGE(x, ci[i][0]) - ct->corner_x[i];
float dy = EDGE(y, ci[i][1]) - ct->corner_y[i];
if (fabsf(dx) < dx_threshold && fabsf(dy) < dy_threshold) {
ct->corner_x[i] = EDGE(x, ci[i][0]);
ct->corner_y[i] = EDGE(y, ci[i][1]);
continue;
}

// Corner that is closer to the cursor moves faster.
// It creates dynamic effect that looks like the trail is being pulled towards the cursor.
float dot = (dx * (EDGE(x, ci[i][0]) - cursor_center_x) +
dy * (EDGE(y, ci[i][1]) - cursor_center_y)) /
cursor_diag_2 / norm(dx, dy);

float decay_seconds = decay_slow + (decay_fast - decay_slow) * (1.0f + dot) * 0.5f;
float step = 1.0f - 1.0f / exp2f(10.0f * dt / decay_seconds);

ct->corner_x[i] += dx * step;
ct->corner_y[i] += dy * step;
}
}
ct->updated_at = now;
for (int i = 0; i < 4; ++i) {
float dx = fabsf(EDGE(x, ci[i][0]) - ct->corner_x[i]);
float dy = fabsf(EDGE(y, ci[i][1]) - ct->corner_y[i]);
if (dx_threshold <= dx || dy_threshold <= dy) {
ct->needs_render = true;
break;
}
}

if (ct->needs_render) {
ColorProfile *cp = WD.screen->color_profile;
ct->color = colorprofile_to_color(cp, cp->overridden.cursor_color, cp->configured.cursor_color).rgb;
}

#undef WD
#undef EDGE
// returning true here will cause the cells to be drawn
return ct->needs_render || needs_render_prev;
}
1 change: 1 addition & 0 deletions kitty/data-types.h
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ typedef struct {
PyObject_HEAD

bool bold, italic, reverse, strikethrough, dim, non_blinking;
monotonic_t updated_at;
unsigned int x, y;
uint8_t decoration;
CursorShape shape;
Expand Down
3 changes: 3 additions & 0 deletions kitty/fast_data_types.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ FC_WIDTH_NORMAL: int
FC_SLANT_ROMAN: int
FC_SLANT_ITALIC: int
BORDERS_PROGRAM: int
TRAIL_PROGRAM: int
PRESS: int
RELEASE: int
DRAG: int
Expand Down Expand Up @@ -540,6 +541,8 @@ def add_borders_rect(
def init_borders_program() -> None:
pass

def init_trail_program() -> None:
pass

def os_window_has_background_image(os_window_id: int) -> bool:
pass
Expand Down
22 changes: 22 additions & 0 deletions kitty/options/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,28 @@
'''
)

opt('enable_cursor_trail', 'no',
option_type='to_bool', ctype='bool',
long_text='''
Enables or disables the cursor trail effect. When set to `yes`, a trailing
effect is rendered behind the cursor as it moves, creating a motion trail.
'''
)

opt('cursor_trail_decay', '0.1 0.3',
option_type='cursor_trail_decay',
ctype='!cursor_trail_decay',
long_text='''
Controls the decay times for the cursor trail effect when :code:`enable_cursor_trail`
is set to :code:`yes`. This option accepts two positive float values specifying the
fastest and slowest decay times in seconds. The first value corresponds to the
fastest decay time (minimum), and the second value corresponds to the slowest
decay time (maximum). The second value must be equal to or greater than the
first value. Smaller values result in a faster decay of the cursor trail.
Adjust these values to control how quickly the cursor trail fades away.
''',
)

egr() # }}}


Expand Down
8 changes: 7 additions & 1 deletion kitty/options/parse.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions kitty/options/to-c-generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions kitty/options/to-c.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ visual_bell_duration(PyObject *src, Options *opts) {

#undef parse_animation

static inline void
cursor_trail_decay(PyObject *src, Options *opts) {
opts->cursor_trail_decay_fast = PyFloat_AsFloat(PyTuple_GET_ITEM(src, 0));
opts->cursor_trail_decay_slow = PyFloat_AsFloat(PyTuple_GET_ITEM(src, 1));
}

static void
parse_font_mod_size(PyObject *val, float *sz, AdjustmentUnit *unit) {
Expand Down
4 changes: 4 additions & 0 deletions kitty/options/types.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions kitty/options/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,11 @@ def to_cursor_unfocused_shape(x: str) -> int:
)
)

def cursor_trail_decay(x: str) -> Tuple[float, float]:
fast, slow = map(positive_float, x.split())
slow = max(slow, fast)
return fast, slow


def scrollback_lines(x: str) -> int:
ans = int(x)
Expand Down
1 change: 1 addition & 0 deletions kitty/screen.c
Original file line number Diff line number Diff line change
Expand Up @@ -1765,6 +1765,7 @@ screen_cursor_position(Screen *self, unsigned int line, unsigned int column) {
line += self->margin_top;
line = MAX(self->margin_top, MIN(line, self->margin_bottom));
}
self->cursor->updated_at = monotonic();
self->cursor->x = column; self->cursor->y = line;
screen_ensure_bounds(self, false, in_margins);
}
Expand Down
37 changes: 35 additions & 2 deletions kitty/shaders.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#define BLEND_ONTO_OPAQUE_WITH_OPAQUE_OUTPUT glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE); // blending onto opaque colors with final color having alpha 1
#define BLEND_PREMULT glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); // blending of pre-multiplied colors

enum { CELL_PROGRAM, CELL_BG_PROGRAM, CELL_SPECIAL_PROGRAM, CELL_FG_PROGRAM, BORDERS_PROGRAM, GRAPHICS_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_ALPHA_MASK_PROGRAM, BGIMAGE_PROGRAM, TINT_PROGRAM, NUM_PROGRAMS };
enum { CELL_PROGRAM, CELL_BG_PROGRAM, CELL_SPECIAL_PROGRAM, CELL_FG_PROGRAM, BORDERS_PROGRAM, GRAPHICS_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_ALPHA_MASK_PROGRAM, BGIMAGE_PROGRAM, TINT_PROGRAM, TRAIL_PROGRAM, NUM_PROGRAMS };
enum { SPRITE_MAP_UNIT, GRAPHICS_UNIT, BGIMAGE_UNIT };

// Sprites {{{
Expand Down Expand Up @@ -1133,6 +1133,36 @@ draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_bu

// }}}

// Cursor Trail {{{
typedef struct {
TrailUniforms uniforms;
} TrailProgramLayout;
static TrailProgramLayout trail_program_layout;

static void
init_trail_program(void) {
get_uniform_locations_trail(TRAIL_PROGRAM, &trail_program_layout.uniforms);
}

void
draw_cursor_trail(CursorTrail *trail) {
bind_program(TRAIL_PROGRAM);

glUniform4fv(trail_program_layout.uniforms.x_coords, 1, trail->corner_x);
glUniform4fv(trail_program_layout.uniforms.y_coords, 1, trail->corner_y);

glUniform2fv(trail_program_layout.uniforms.cursor_edge_x, 1, trail->cursor_edge_x);
glUniform2fv(trail_program_layout.uniforms.cursor_edge_y, 1, trail->cursor_edge_y);

color_vec3(trail_program_layout.uniforms.trail_color, trail->color);

glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

unbind_program();
}

// }}}

// Python API {{{

static bool
Expand Down Expand Up @@ -1205,6 +1235,8 @@ NO_ARG(init_borders_program)

NO_ARG(init_cell_program)

NO_ARG(init_trail_program)

static PyObject*
sprite_map_set_limits(PyObject UNUSED *self, PyObject *args) {
unsigned int w, h;
Expand All @@ -1229,6 +1261,7 @@ static PyMethodDef module_methods[] = {
MW(unbind_program, METH_NOARGS),
MW(init_borders_program, METH_NOARGS),
MW(init_cell_program, METH_NOARGS),
MW(init_trail_program, METH_NOARGS),

{NULL, NULL, 0, NULL} /* Sentinel */
};
Expand All @@ -1241,7 +1274,7 @@ finalize(void) {
bool
init_shaders(PyObject *module) {
#define C(x) if (PyModule_AddIntConstant(module, #x, x) != 0) { PyErr_NoMemory(); return false; }
C(CELL_PROGRAM); C(CELL_BG_PROGRAM); C(CELL_SPECIAL_PROGRAM); C(CELL_FG_PROGRAM); C(BORDERS_PROGRAM); C(GRAPHICS_PROGRAM); C(GRAPHICS_PREMULT_PROGRAM); C(GRAPHICS_ALPHA_MASK_PROGRAM); C(BGIMAGE_PROGRAM); C(TINT_PROGRAM);
C(CELL_PROGRAM); C(CELL_BG_PROGRAM); C(CELL_SPECIAL_PROGRAM); C(CELL_FG_PROGRAM); C(BORDERS_PROGRAM); C(GRAPHICS_PROGRAM); C(GRAPHICS_PREMULT_PROGRAM); C(GRAPHICS_ALPHA_MASK_PROGRAM); C(BGIMAGE_PROGRAM); C(TINT_PROGRAM); C(TRAIL_PROGRAM);
C(GLSL_VERSION);
C(GL_VERSION);
C(GL_VENDOR);
Expand Down
Loading