Merge branch 'feat/scroll-per-pixel' of https://github.com/idelice/kitty

This commit is contained in:
Kovid Goyal
2026-01-08 09:12:34 +05:30
13 changed files with 293 additions and 78 deletions

View File

@@ -206,6 +206,16 @@ 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.
If you use a highprecision input device such as a touchpad, you can enable
smooth, perpixel scrollback with :opt:`pixel_scroll`. For example, add this to
your :file:`kitty.conf`::
pixel_scroll yes
You can further tune the sensitivity with :opt:`touch_scroll_multiplier`. Note
that this only affects scrolling kitty's own scrollback, not applications
running inside the terminal that handle their own scrolling.
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
while typing in a follow-up command. |kitty| allows you to do this by pressing

View File

@@ -21,6 +21,7 @@ layout(std140) uniform CellRenderData {
uniform float gamma_lut[256];
uniform uint draw_bg_bitfield;
uniform usampler2D sprite_decorations_map;
uniform float row_offset;
// Have to use fixed locations here as all variants of the cell program share the same VAOs
layout(location=0) in uvec3 colors;
@@ -212,7 +213,7 @@ CellData set_vertex_position(vec3 cell_fg, vec3 cell_bg) {
uint column = instance_id - row * columns;
/* The position of this vertex, at a corner of the cell */
float left = -1.0 + column * dx;
float top = 1.0 - row * dy;
float top = 1.0 - (float(row) + row_offset) * dy;
uvec2 pos = cell_pos_map[gl_VertexID];
gl_Position = vec4(vec2(left, left + dx)[pos.x], vec2(top, top - dy)[pos.y], 0, 1);
// The character sprite being rendered

View File

@@ -249,6 +249,7 @@ grman_pause_rendering(GraphicsManager *self, GraphicsManager *dest) {
dest->window_id = self->window_id;
dest->layers_dirty = true;
dest->last_scrolled_by = 0;
dest->last_scroll_offset_lines = 0.0f;
iter_images(self) {
Image *clone = calloc(1, sizeof(Image)), *img = i.data->val;
@@ -1202,9 +1203,10 @@ resolve_parent_offset(const GraphicsManager *self, const ImageRef *ref, int32_t
bool
grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows, CellPixelSize cell) {
if (self->last_scrolled_by != scrolled_by) set_layers_dirty(self);
grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scroll_offset_lines, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows, CellPixelSize cell) {
if (self->last_scrolled_by != scrolled_by || self->last_scroll_offset_lines != scroll_offset_lines) set_layers_dirty(self);
self->last_scrolled_by = scrolled_by;
self->last_scroll_offset_lines = scroll_offset_lines;
if (!self->layers_dirty) return false;
self->layers_dirty = false;
size_t i;
@@ -1216,7 +1218,7 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree
float screen_bottom = screen_top - screen_height;
float screen_width_px = num_cols * cell.width;
float screen_height_px = num_rows * cell.height;
float y0 = screen_top - dy * scrolled_by;
float y0 = screen_top - dy * ((float)scrolled_by + scroll_offset_lines);
// Iterate over all visible refs and create render data
self->render_data.count = 0;
@@ -2367,10 +2369,10 @@ W(shm_unlink) {
}
W(update_layers) {
unsigned int scrolled_by, sx, sy; float xstart, ystart, dx, dy;
unsigned int scrolled_by, sx, sy; float xstart, ystart, dx, dy, scroll_offset_lines = 0.0f;
CellPixelSize cell;
PA("IffffIIII", &scrolled_by, &xstart, &ystart, &dx, &dy, &sx, &sy, &cell.width, &cell.height);
grman_update_layers(self, scrolled_by, xstart, ystart, dx, dy, sx, sy, cell);
PA("IffffIIII|f", &scrolled_by, &xstart, &ystart, &dx, &dy, &sx, &sy, &cell.width, &cell.height, &scroll_offset_lines);
grman_update_layers(self, scrolled_by, scroll_offset_lines, xstart, ystart, dx, dy, sx, sy, cell);
PyObject *ans = PyTuple_New(self->render_data.count);
for (size_t i = 0; i < self->render_data.count; i++) {
ImageRenderData *r = self->render_data.item + i;

View File

@@ -145,6 +145,7 @@ typedef struct {
// The number of images below MIN_ZINDEX / 2, then the number of refs between MIN_ZINDEX / 2 and -1 inclusive, then the number of refs above 0 inclusive.
size_t num_of_below_refs, num_of_negative_refs, num_of_positive_refs;
unsigned int last_scrolled_by;
float last_scroll_offset_lines;
size_t used_storage;
PyObject *disk_cache;
bool has_images_needing_animation, context_made_current_for_this_command;
@@ -202,7 +203,7 @@ GraphicsManager* grman_alloc(bool for_paused_rendering);
void grman_clear(GraphicsManager*, bool, CellPixelSize fg);
const char* grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty, CellPixelSize fg);
void grman_put_cell_image(GraphicsManager *self, uint32_t row, uint32_t col, uint32_t image_id, uint32_t placement_id, uint32_t x, uint32_t y, uint32_t w, uint32_t h, CellPixelSize cell);
bool grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows, CellPixelSize);
bool grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scroll_offset_lines, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows, CellPixelSize);
void grman_scroll_images(GraphicsManager *self, const ScrollData*, CellPixelSize fg);
void grman_resize(GraphicsManager*, index_type, index_type, index_type, index_type, index_type, index_type);
void grman_rescale(GraphicsManager *self, CellPixelSize fg);

View File

@@ -6,6 +6,7 @@
*/
#include "state.h"
#include "screen.h"
#include "charsets.h"
#include <limits.h>
#include <math.h>
@@ -1232,6 +1233,11 @@ scroll_phase(GLFWMomentumType t) {
}
static inline bool
pixel_scroll_enabled_for_screen(const Screen *screen) {
return OPT(pixel_scroll) && screen->linebuf == screen->main_linebuf;
}
void
scroll_event(const GLFWScrollEvent *ev) {
debug("\x1b[36mScroll\x1b[m %s x: %f y: %f momentum: %s modifiers: %s\n", scroll_offset_type(ev->offset_type), ev->x_offset, ev->y_offset, scroll_phase(ev->momentum_type), format_mods(ev->keyboard_modifiers));
@@ -1285,30 +1291,41 @@ scroll_event(const GLFWScrollEvent *ev) {
case GLFW_MOMENTUM_PHASE_MAY_BEGIN:
break;
}
int s;
if (ev->y_offset != 0.0) {
s = scale_scroll(screen->modes.mouse_tracking_mode, ev->y_offset, ev->offset_type, &screen->pending_scroll_pixels_y, global_state.callback_os_window->fonts_data->fcm.cell_height);
if (s) {
bool upwards = s > 0;
if (screen->modes.mouse_tracking_mode) {
int sz = encode_mouse_scroll(w, upwards ? 4 : 5, ev->keyboard_modifiers);
if (sz > 0) {
mouse_event_buf[sz] = 0;
for (s = abs(s); s > 0; s--) {
write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf);
}
}
if (!screen->modes.mouse_tracking_mode && pixel_scroll_enabled_for_screen(screen) && (ev->offset_type == GLFW_SCROLL_OFFEST_HIGHRES || ev->offset_type == GLFW_SCROLL_OFFEST_V120)) {
double delta_pixels = 0.0;
if (ev->offset_type == GLFW_SCROLL_OFFEST_HIGHRES) {
delta_pixels = ev->y_offset * OPT(touch_scroll_multiplier);
} else {
if (screen->linebuf == screen->main_linebuf) {
screen_history_scroll(screen, abs(s), upwards);
if (screen->selections.in_progress) update_drag(w);
const double offset_lines = (ev->y_offset / 120.) * OPT(wheel_scroll_multiplier);
delta_pixels = offset_lines * global_state.callback_os_window->fonts_data->fcm.cell_height;
}
screen->pending_scroll_pixels_y = 0.0;
if (screen_apply_pixel_scroll(screen, delta_pixels) && screen->selections.in_progress) update_drag(w);
} else {
int s = scale_scroll(screen->modes.mouse_tracking_mode, ev->y_offset, ev->offset_type, &screen->pending_scroll_pixels_y, global_state.callback_os_window->fonts_data->fcm.cell_height);
if (s) {
bool upwards = s > 0;
if (screen->modes.mouse_tracking_mode) {
int sz = encode_mouse_scroll(w, upwards ? 4 : 5, ev->keyboard_modifiers);
if (sz > 0) {
mouse_event_buf[sz] = 0;
for (s = abs(s); s > 0; s--) {
write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf);
}
}
} else {
if (screen->linebuf == screen->main_linebuf) {
screen_history_scroll(screen, abs(s), upwards);
if (screen->selections.in_progress) update_drag(w);
}
else fake_scroll(w, abs(s), upwards);
}
else fake_scroll(w, abs(s), upwards);
}
}
}
if (ev->x_offset != 0.0) {
s = scale_scroll(screen->modes.mouse_tracking_mode, ev->x_offset, ev->offset_type, &screen->pending_scroll_pixels_x, global_state.callback_os_window->fonts_data->fcm.cell_width);
int s = scale_scroll(screen->modes.mouse_tracking_mode, ev->x_offset, ev->offset_type, &screen->pending_scroll_pixels_x, global_state.callback_os_window->fonts_data->fcm.cell_width);
if (s) {
if (screen->modes.mouse_tracking_mode) {
int sz = encode_mouse_scroll(w, s > 0 ? 6 : 7, ev->keyboard_modifiers);

View File

@@ -586,6 +586,16 @@ opt('touch_scroll_multiplier', '1.0',
Multiplier for the number of lines scrolled by a touchpad. Note that this is
only used for high precision scrolling devices on platforms such as macOS and
Wayland. Use negative numbers to change scroll direction.
'''
)
opt('pixel_scroll', 'no',
option_type='to_bool', ctype='bool',
long_text='''
Enable per-pixel scrolling for high precision input devices (for example
touchpads). When enabled, kitty's own scrollback can move by sub-line increments
instead of only whole lines. This does not affect applications running inside
the terminal (for example full-screen TUIs) that handle scrolling themselves.
'''
)
egr() # }}}

View File

@@ -1389,6 +1389,9 @@ class Parser:
def touch_scroll_multiplier(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['touch_scroll_multiplier'] = float(val)
def pixel_scroll(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['pixel_scroll'] = to_bool(val)
def transparent_background_colors(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['transparent_background_colors'] = transparent_background_colors(val)

View File

@@ -499,6 +499,19 @@ convert_from_opts_touch_scroll_multiplier(PyObject *py_opts, Options *opts) {
Py_DECREF(ret);
}
static void
convert_from_python_pixel_scroll(PyObject *val, Options *opts) {
opts->pixel_scroll = PyObject_IsTrue(val);
}
static void
convert_from_opts_pixel_scroll(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "pixel_scroll");
if (ret == NULL) return;
convert_from_python_pixel_scroll(ret, opts);
Py_DECREF(ret);
}
static void
convert_from_python_mouse_hide_wait(PyObject *val, Options *opts) {
mouse_hide_wait(val, opts);
@@ -1422,6 +1435,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
if (PyErr_Occurred()) return false;
convert_from_opts_touch_scroll_multiplier(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_pixel_scroll(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_mouse_hide_wait(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_url_color(py_opts, opts);

View File

@@ -464,6 +464,7 @@ option_names = (
'text_composition_strategy',
'text_fg_override_threshold',
'touch_scroll_multiplier',
'pixel_scroll',
'transparent_background_colors',
'undercurl_style',
'underline_exclusion',
@@ -651,6 +652,7 @@ class Options:
text_composition_strategy: str = 'platform'
text_fg_override_threshold: tuple[float, typing.Literal['%', 'ratio']] = (0.0, '%')
touch_scroll_multiplier: float = 1.0
pixel_scroll: bool = False
transparent_background_colors: tuple[tuple[kitty.fast_data_types.Color, float], ...] = ()
undercurl_style: choices_for_undercurl_style = 'thin-sparse'
underline_exclusion: tuple[float, typing.Literal['', 'px', 'pt']] = (1.0, '')
@@ -1111,4 +1113,4 @@ special_colors = frozenset({
})
secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass')
secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass')

View File

@@ -162,6 +162,15 @@ new_screen_object(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
init_tabstops(self->alt_tabstops, self->columns);
self->key_encoding_flags = self->main_key_encoding_flags;
if (!init_overlay_line(self, self->columns, false)) { Py_CLEAR(self); return NULL; }
self->blank_line_cpu = PyMem_Calloc(self->columns, sizeof(CPUCell));
self->blank_line_gpu = PyMem_Calloc(self->columns, sizeof(GPUCell));
if (!self->blank_line_cpu || !self->blank_line_gpu) { Py_CLEAR(self); return NULL; }
self->blank_line.cpu_cells = self->blank_line_cpu;
self->blank_line.gpu_cells = self->blank_line_gpu;
self->blank_line.xnum = self->columns;
self->blank_line.ynum = 0;
self->blank_line.attrs.val = 0;
self->blank_line.text_cache = self->text_cache;
self->hyperlink_pool = alloc_hyperlink_pool();
if (!self->hyperlink_pool) { Py_CLEAR(self); return PyErr_NoMemory(); }
self->as_ansi_buf.hyperlink_pool = self->hyperlink_pool;
@@ -550,6 +559,17 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
}
// Resize overlay line
if (!init_overlay_line(self, columns, true)) return false;
PyMem_Free(self->blank_line_cpu);
PyMem_Free(self->blank_line_gpu);
self->blank_line_cpu = PyMem_Calloc(columns, sizeof(CPUCell));
self->blank_line_gpu = PyMem_Calloc(columns, sizeof(GPUCell));
if (!self->blank_line_cpu || !self->blank_line_gpu) return false;
self->blank_line.cpu_cells = self->blank_line_cpu;
self->blank_line.gpu_cells = self->blank_line_gpu;
self->blank_line.xnum = columns;
self->blank_line.ynum = 0;
self->blank_line.attrs.val = 0;
self->blank_line.text_cache = self->text_cache;
// Resize main linebuf
RAII_PyObject(prompt_copy, NULL);
@@ -1482,6 +1502,8 @@ cursor_within_margins(Screen *self) {
return self->margin_top <= self->cursor->y && self->cursor->y <= self->margin_bottom;
}
static void reset_pixel_scroll(Screen *self);
// Remove all cell images from a portion of the screen and mark lines that
// contain image placeholders as dirty to make sure they are redrawn. This is
// needed when we perform commands that may move some lines without marking them
@@ -1528,6 +1550,7 @@ void
screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_screen) {
bool to_alt = self->linebuf == self->main_linebuf;
self->active_hyperlink_id = 0;
reset_pixel_scroll(self);
if (to_alt) {
if (clear_alt_screen) {
linebuf_clear(self->alt_linebuf, BLANK_CHAR);
@@ -2420,9 +2443,35 @@ dirty_scroll(Screen *self) {
screen_pause_rendering(self, false, 0);
}
static inline bool
pixel_scroll_enabled(const Screen *self) {
return OPT(pixel_scroll) && self->linebuf == self->main_linebuf && !self->paused_rendering.expires_at;
}
static inline bool
pixel_scroll_enabled_for_render(const Screen *self) {
return OPT(pixel_scroll) && !self->paused_rendering.expires_at && self->linebuf == self->main_linebuf;
}
static inline unsigned int
render_lines_for_screen(const Screen *self) {
return self->lines + (pixel_scroll_enabled_for_render(self) ? 2u : 0u);
}
static inline int
render_row_offset_for_screen(const Screen *self) {
return pixel_scroll_enabled_for_render(self) ? 1 : 0;
}
static inline void
reset_pixel_scroll(Screen *self) {
self->pixel_scroll_offset_y = 0.0;
}
static void
screen_clear_scrollback(Screen *self) {
historybuf_clear(self->historybuf);
reset_pixel_scroll(self);
if (self->scrolled_by != 0) {
self->scrolled_by = 0;
dirty_scroll(self);
@@ -3120,7 +3169,10 @@ screen_history_scroll_to_prompt(Screen *self, int num_of_prompts_to_jump, int sc
self->scrolled_by = y >= 0 ? 0 : -y;
screen_set_last_visited_prompt(self, 0);
}
if (old != self->scrolled_by) dirty_scroll(self);
if (old != self->scrolled_by) {
reset_pixel_scroll(self);
dirty_scroll(self);
}
return old != self->scrolled_by;
}
@@ -3347,6 +3399,32 @@ update_line_data(Line *line, unsigned int dest_y, uint8_t *data) {
memcpy(data + base, line->gpu_cells, line->xnum * sizeof(GPUCell));
}
static Line*
render_line_for_virtual_y(Screen *self, int y, Line *line, index_type *lnum, bool *is_history) {
if (self->scrolled_by) {
if (y < (int)self->scrolled_by) {
int idx = (int)self->scrolled_by - 1 - y;
if (idx >= 0 && (unsigned)idx < self->historybuf->count) {
historybuf_init_line(self->historybuf, idx, line);
line->xnum = self->columns;
line->ynum = (index_type)MIN(MAX(y, 0), (int)self->lines - 1);
if (lnum) *lnum = (index_type)idx;
if (is_history) *is_history = true;
return line;
}
return &self->blank_line;
}
y -= self->scrolled_by;
}
if (y >= 0 && y < (int)self->lines) {
linebuf_init_line_at(self->linebuf, (index_type)y, line);
if (lnum) *lnum = (index_type)y;
if (is_history) *is_history = false;
return line;
}
return &self->blank_line;
}
static void
screen_reset_dirty(Screen *self) {
@@ -3466,24 +3544,22 @@ screen_render_line_graphics(Screen *self, Line *line, int32_t row) {
static void
screen_update_only_line_graphics_data(Screen *self) {
unsigned int history_line_added_count = self->history_line_added_count;
index_type lnum;
index_type lnum = 0;
if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count);
screen_reset_dirty(self);
self->scroll_changed = false;
for (index_type y = 0; y < MIN(self->lines, self->scrolled_by); y++) {
lnum = self->scrolled_by - 1 - y;
historybuf_init_line(self->historybuf, lnum, self->historybuf->line);
screen_render_line_graphics(self, self->historybuf->line, y - self->scrolled_by);
if (self->historybuf->line->attrs.has_dirty_text) {
historybuf_mark_line_clean(self->historybuf, lnum);
}
}
for (index_type y = self->scrolled_by; y < self->lines; y++) {
lnum = y - self->scrolled_by;
linebuf_init_line(self->linebuf, lnum);
if (self->linebuf->line->attrs.has_dirty_text) {
screen_render_line_graphics(self, self->linebuf->line, y - self->scrolled_by);
linebuf_mark_line_clean(self->linebuf, lnum);
const unsigned int render_lines = render_lines_for_screen(self);
const int render_row_offset = render_row_offset_for_screen(self);
Line line = {.text_cache = self->text_cache};
for (unsigned int render_row = 0; render_row < render_lines; render_row++) {
const int virtual_y = (int)render_row - render_row_offset;
bool is_history = false;
Line *linep = render_line_for_virtual_y(self, virtual_y, &line, &lnum, &is_history);
if (linep == &self->blank_line) continue;
screen_render_line_graphics(self, linep, virtual_y - (int)self->scrolled_by);
if (linep->attrs.has_dirty_text) {
if (is_history) historybuf_mark_line_clean(self->historybuf, lnum);
else linebuf_mark_line_clean(self->linebuf, lnum);
}
}
}
@@ -3512,34 +3588,41 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat
index_type lnum;
screen_reset_dirty(self);
update_overlay_position(self);
const bool force_history_render = pixel_scroll_enabled(self) && self->scroll_changed;
if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count);
self->scroll_changed = false;
for (index_type y = 0; y < MIN(self->lines, self->scrolled_by); y++) {
lnum = self->scrolled_by - 1 - y;
historybuf_init_line(self->historybuf, lnum, self->historybuf->line);
// we render line graphics even if the line is not dirty as graphics commands received after
// the unicode placeholder was first scanned can alter it.
screen_render_line_graphics(self, self->historybuf->line, y - self->scrolled_by);
if (self->historybuf->line->attrs.has_dirty_text) {
render_line(fonts_data, self->historybuf->line, lnum, self->cursor, self->disable_ligatures, self->lc);
if (screen_has_marker(self)) mark_text_in_line(self->marker, self->historybuf->line, &self->as_ansi_buf);
historybuf_mark_line_clean(self->historybuf, lnum);
const unsigned int render_lines = render_lines_for_screen(self);
const int render_row_offset = render_row_offset_for_screen(self);
Line line = {.text_cache = self->text_cache};
for (unsigned int render_row = 0; render_row < render_lines; render_row++) {
const int virtual_y = (int)render_row - render_row_offset;
bool is_history = false;
Line *linep = render_line_for_virtual_y(self, virtual_y, &line, &lnum, &is_history);
if (linep == &self->blank_line) {
update_line_data(linep, render_row, address);
continue;
}
update_line_data(self->historybuf->line, y, address);
}
for (index_type y = self->scrolled_by; y < self->lines; y++) {
lnum = y - self->scrolled_by;
linebuf_init_line(self->linebuf, lnum);
if (self->linebuf->line->attrs.has_dirty_text ||
(cursor_has_moved && (self->cursor->y == lnum || self->last_rendered.cursor.y == lnum))) {
render_line(fonts_data, self->linebuf->line, lnum, self->cursor, self->disable_ligatures, self->lc);
screen_render_line_graphics(self, self->linebuf->line, y - self->scrolled_by);
if (self->linebuf->line->attrs.has_dirty_text && screen_has_marker(self)) mark_text_in_line(
self->marker, self->linebuf->line, &self->as_ansi_buf);
if (is_overlay_active && lnum == self->overlay_line.ynum) render_overlay_line(self, self->linebuf->line, fonts_data);
linebuf_mark_line_clean(self->linebuf, lnum);
if (is_history) {
// we render line graphics even if the line is not dirty as graphics commands received after
// the unicode placeholder was first scanned can alter it.
screen_render_line_graphics(self, linep, virtual_y - (int)self->scrolled_by);
if (force_history_render || linep->attrs.has_dirty_text) {
render_line(fonts_data, linep, lnum, self->cursor, self->disable_ligatures, self->lc);
if (screen_has_marker(self)) mark_text_in_line(self->marker, linep, &self->as_ansi_buf);
historybuf_mark_line_clean(self->historybuf, lnum);
}
} else {
if (linep->attrs.has_dirty_text ||
(cursor_has_moved && (self->cursor->y == lnum || self->last_rendered.cursor.y == lnum))) {
render_line(fonts_data, linep, lnum, self->cursor, self->disable_ligatures, self->lc);
screen_render_line_graphics(self, linep, virtual_y - (int)self->scrolled_by);
if (linep->attrs.has_dirty_text && screen_has_marker(self)) mark_text_in_line(
self->marker, linep, &self->as_ansi_buf);
if (is_overlay_active && lnum == self->overlay_line.ynum) render_overlay_line(self, linep, fonts_data);
linebuf_mark_line_clean(self->linebuf, lnum);
}
}
update_line_data(self->linebuf->line, y, address);
update_line_data(linep, render_row, address);
}
if (is_overlay_active && self->overlay_line.ynum + self->scrolled_by < self->lines) {
if (self->overlay_line.is_dirty) {
@@ -3696,7 +3779,7 @@ iteration_data_is_empty(const Screen *self, const IterationData *idata) {
}
static void
apply_selection(Screen *self, uint8_t *data, Selection *s, uint8_t set_mask) {
apply_selection(Screen *self, uint8_t *data, Selection *s, uint8_t set_mask, int render_row_offset) {
iteration_data(s, &s->last_rendered, self->columns, -self->historybuf->count, self->scrolled_by);
Line *line;
const int y_min = MAX(0, s->last_rendered.y), y_limit = MIN(s->last_rendered.y_limit, (int)self->lines);
@@ -3705,14 +3788,14 @@ apply_selection(Screen *self, uint8_t *data, Selection *s, uint8_t set_mask) {
linebuf_init_line(self->paused_rendering.linebuf, y);
line = self->paused_rendering.linebuf->line;
} else line = visual_line_(self, y);
uint8_t *line_start = data + self->columns * y;
uint8_t *line_start = data + self->columns * (y + render_row_offset);
XRange xr = xrange_for_iteration_with_multicells(&s->last_rendered, y, line);
for (index_type x = xr.x; x < xr.x_limit; x++) {
line_start[x] |= set_mask;
CPUCell *c = &line->cpu_cells[x];
if (c->is_multicell && c->scale > 1) {
for (int ym = MAX(0, y - c->y); ym < y; ym++) data[self->columns * ym + x] |= set_mask;
for (int ym = y + 1; ym < MIN((int)self->lines, y + c->scale - c->y); ym++) data[self->columns * ym + x] |= set_mask;
for (int ym = MAX(0, y - c->y); ym < y; ym++) data[self->columns * (ym + render_row_offset) + x] |= set_mask;
for (int ym = y + 1; ym < MIN((int)self->lines, y + c->scale - c->y); ym++) data[self->columns * (ym + render_row_offset) + x] |= set_mask;
}
}
}
@@ -3735,20 +3818,24 @@ screen_has_selection(Screen *self) {
void
screen_apply_selection(Screen *self, void *address, size_t size) {
memset(address, 0, size);
const int render_row_offset = render_row_offset_for_screen(self);
Selections *sel = self->paused_rendering.expires_at ? &self->paused_rendering.selections : &self->selections;
for (size_t i = 0; i < sel->count; i++) apply_selection(self, address, sel->items + i, 1);
for (size_t i = 0; i < sel->count; i++) apply_selection(self, address, sel->items + i, 1, render_row_offset);
sel->last_rendered_count = sel->count;
sel = self->paused_rendering.expires_at ? &self->paused_rendering.url_ranges : &self->url_ranges;
for (size_t i = 0; i < sel->count; i++) {
Selection *s = sel->items + i;
if (OPT(underline_hyperlinks) == UNDERLINE_NEVER && s->is_hyperlink) continue;
apply_selection(self, address, s, 2);
apply_selection(self, address, s, 2, render_row_offset);
}
uint8_t *a = address;
sel->last_rendered_count = sel->count;
ExtraCursors *ec = self->paused_rendering.expires_at ? &self->paused_rendering.extra_cursors : &self->extra_cursors;
const size_t render_offset_cells = (size_t)render_row_offset * self->columns;
for (unsigned i = 0; i < ec->count; i++) {
if (ec->locations[i].cell < size) a[ec->locations[i].cell] |= (ec->locations[i].shape & 7) << 2;
if (ec->locations[i].cell + render_offset_cells < size) {
a[ec->locations[i].cell + render_offset_cells] |= (ec->locations[i].shape & 7) << 2;
}
}
ec->dirty = false;
}
@@ -4135,6 +4222,7 @@ screen_update_overlay_text(Screen *self, const char *utf8_text) {
// Since we are typing, scroll to the bottom
if (self->scrolled_by != 0) {
self->scrolled_by = 0;
reset_pixel_scroll(self);
dirty_scroll(self);
}
}
@@ -4250,7 +4338,8 @@ render_overlay_line(Screen *self, Line *line, FONTS_DATA_HANDLE fonts_data) {
static void
update_overlay_line_data(Screen *self, uint8_t *data) {
const size_t base = sizeof(GPUCell) * (self->overlay_line.ynum + self->scrolled_by) * self->columns;
const int render_row_offset = render_row_offset_for_screen(self);
const size_t base = sizeof(GPUCell) * (self->overlay_line.ynum + self->scrolled_by + render_row_offset) * self->columns;
memcpy(data + base, self->overlay_line.gpu_cells, self->columns * sizeof(GPUCell));
}
@@ -4943,10 +5032,38 @@ screen_history_scroll_to_absolute(Screen *self, unsigned int target_scrolled_by)
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;
reset_pixel_scroll(self);
dirty_scroll(self);
}
}
bool
screen_apply_pixel_scroll(Screen *self, double delta_pixels) {
if (!pixel_scroll_enabled(self)) return false;
if (!self->historybuf->count) return false;
const double cell_height = (double)self->cell_size.height;
if (cell_height <= 0.0 || delta_pixels == 0.0) return false;
double total = self->pixel_scroll_offset_y + (double)self->scrolled_by * cell_height + delta_pixels;
const double max_total = (double)self->historybuf->count * cell_height;
if (total < 0.0) total = 0.0;
if (total > max_total) total = max_total;
const unsigned int new_scrolled_by = (unsigned int)floor(total / cell_height);
const double offset = total - (double)new_scrolled_by * cell_height;
bool changed = false;
if (new_scrolled_by != self->scrolled_by) {
self->scrolled_by = new_scrolled_by;
changed = true;
}
if (offset != self->pixel_scroll_offset_y) {
self->pixel_scroll_offset_y = offset;
changed = true;
}
if (changed) dirty_scroll(self);
return changed;
}
bool
screen_history_scroll(Screen *self, int amt, bool upwards) {
switch(amt) {
@@ -4971,6 +5088,7 @@ screen_history_scroll(Screen *self, int amt, bool upwards) {
unsigned int new_scroll = MIN(self->scrolled_by + amt, self->historybuf->count);
if (new_scroll != self->scrolled_by) {
self->scrolled_by = new_scroll;
reset_pixel_scroll(self);
dirty_scroll(self);
return true;
}
@@ -5613,6 +5731,7 @@ scroll_prompt_to_bottom(Screen *self, PyObject *args UNUSED) {
// always scroll to the bottom
if (self->scrolled_by != 0) {
self->scrolled_by = 0;
reset_pixel_scroll(self);
dirty_scroll(self);
}
Py_RETURN_NONE;

View File

@@ -106,8 +106,12 @@ typedef struct {
unsigned int columns, lines, margin_top, margin_bottom, scrolled_by;
double pending_scroll_pixels_x, pending_scroll_pixels_y;
double pixel_scroll_offset_y;
CellPixelSize cell_size;
OverlayLine overlay_line;
Line blank_line;
CPUCell *blank_line_cpu;
GPUCell *blank_line_gpu;
id_type window_id;
Selections selections, url_ranges;
struct {
@@ -279,6 +283,7 @@ typedef struct 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);
bool screen_apply_pixel_scroll(Screen *self, double delta_pixels);
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

@@ -40,6 +40,28 @@ typedef struct UIRenderData {
color_type background_color; // RGB only
} UIRenderData;
static inline bool
pixel_scroll_enabled_for_render(const Screen *screen) {
return OPT(pixel_scroll) && !screen->paused_rendering.expires_at && screen->linebuf == screen->main_linebuf;
}
static inline unsigned int
render_lines_for_screen(const Screen *screen) {
return screen->lines + (pixel_scroll_enabled_for_render(screen) ? 2u : 0u);
}
static inline float
row_offset_for_screen(const Screen *screen) {
if (!pixel_scroll_enabled_for_render(screen) || !screen->cell_size.height) return 0.f;
return -1.f + (float)(screen->pixel_scroll_offset_y / (double)screen->cell_size.height);
}
static inline float
scroll_offset_lines_for_screen(const Screen *screen) {
if (!pixel_scroll_enabled_for_render(screen) || !screen->cell_size.height) return 0.f;
return (float)(screen->pixel_scroll_offset_y / (double)screen->cell_size.height);
}
// Sprites {{{
typedef struct {
int xnum, ynum, x, y, z, last_num_of_layers, last_ynum;
@@ -486,6 +508,10 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, c
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;
if (pixel_scroll_enabled_for_render(screen)) {
rd->cursor_y1 += 1;
rd->cursor_y2 += 1;
}
CursorShape cs = (cursor->is_focused || OPT(cursor_shape_unfocused) == NO_CURSOR_SHAPE) ? cursor->shape : OPT(cursor_shape_unfocused);
rd->cursor_shape = cs;
color_type cell_fg = rd->default_fg, cell_bg = rd->bg_colors0;
@@ -573,7 +599,8 @@ cell_prepare_to_render(ssize_t vao_idx, Screen *screen, FONTS_DATA_HANDLE fonts_
bool screen_resized = screen->last_rendered.columns != screen->columns || screen->last_rendered.lines != screen->lines;
#define update_cell_data { \
sz = sizeof(GPUCell) * screen->lines * screen->columns; \
const unsigned int render_lines = render_lines_for_screen(screen); \
sz = sizeof(GPUCell) * render_lines * screen->columns; \
address = alloc_and_map_vao_buffer(vao_idx, sz, cell_data_buffer, true); \
screen_update_cell_data(screen, address, fonts_data, disable_ligatures && cursor_pos_changed); \
unmap_vao_buffer(vao_idx, cell_data_buffer); address = NULL; \
@@ -585,7 +612,8 @@ cell_prepare_to_render(ssize_t vao_idx, Screen *screen, FONTS_DATA_HANDLE fonts_
} else if (screen->reload_all_gpu_data || screen->scroll_changed || screen->is_dirty || screen_resized || (disable_ligatures && cursor_pos_changed)) update_cell_data;
#define update_selection_data { \
sz = (size_t)screen->lines * screen->columns; \
const unsigned int render_lines = render_lines_for_screen(screen); \
sz = (size_t)render_lines * screen->columns; \
address = alloc_and_map_vao_buffer(vao_idx, sz, selection_buffer, true); \
screen_apply_selection(screen, address, sz); \
unmap_vao_buffer(vao_idx, selection_buffer); address = NULL; \
@@ -593,7 +621,7 @@ cell_prepare_to_render(ssize_t vao_idx, Screen *screen, FONTS_DATA_HANDLE fonts_
}
#define update_graphics_data(grman) \
grman_update_layers(grman, screen->scrolled_by, -1.f, 1.f, 2.f/screen->columns, 2.f/screen->lines, screen->columns, screen->lines, screen->cell_size)
grman_update_layers(grman, screen->scrolled_by, scroll_offset_lines_for_screen(screen), -1.f, 1.f, 2.f/screen->columns, 2.f/screen->lines, screen->columns, screen->lines, screen->cell_size)
if (screen->paused_rendering.expires_at) {
if (!screen->paused_rendering.cell_data_updated) {
@@ -1069,8 +1097,9 @@ call_cell_program(int program, const UIRenderData *ui, ssize_t vao_idx, bool for
CELL_BUFFERS;
bind_vao_uniform_buffer(vao_idx, uniform_buffer, cell_program_layouts[program].render_data.index);
glUniform1ui(cell_program_layouts[program].uniforms.draw_bg_bitfield, draw_bg_bitfield);
glUniform1f(cell_program_layouts[program].uniforms.row_offset, row_offset_for_screen(ui->screen));
if (for_final_output) glEnable(GL_FRAMEBUFFER_SRGB);
draw_quad(!for_final_output, ui->screen->lines * ui->screen->columns);
draw_quad(!for_final_output, render_lines_for_screen(ui->screen) * ui->screen->columns);
if (for_final_output) glDisable(GL_FRAMEBUFFER_SRGB);
}

View File

@@ -47,6 +47,7 @@ typedef struct Options {
} mouse_hide;
double wheel_scroll_multiplier, touch_scroll_multiplier;
int wheel_scroll_min_lines;
bool pixel_scroll;
bool enable_audio_bell;
CursorShape cursor_shape, cursor_shape_unfocused;
float cursor_beam_thickness;