diff --git a/docs/overview.rst b/docs/overview.rst index be315b8d5..4cdf1f09e 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -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 high‑precision input device such as a touchpad, you can enable +smooth, per‑pixel 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 diff --git a/kitty/cell_vertex.glsl b/kitty/cell_vertex.glsl index e9644bf33..60b99e0db 100644 --- a/kitty/cell_vertex.glsl +++ b/kitty/cell_vertex.glsl @@ -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 diff --git a/kitty/graphics.c b/kitty/graphics.c index f985a7575..3cfec4ba3 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -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; diff --git a/kitty/graphics.h b/kitty/graphics.h index 84e5d6d57..0d348abc1 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -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); diff --git a/kitty/mouse.c b/kitty/mouse.c index ab562fce4..4be87271a 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -6,6 +6,7 @@ */ #include "state.h" +#include "screen.h" #include "charsets.h" #include #include @@ -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); diff --git a/kitty/options/definition.py b/kitty/options/definition.py index b885e8acc..baf3ef143 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -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() # }}} diff --git a/kitty/options/parse.py b/kitty/options/parse.py index c6a8b2dad..284b2ff6b 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -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) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index a50442051..5ca2b9ec3 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -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); diff --git a/kitty/options/types.py b/kitty/options/types.py index 09d4164cc..525911322 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -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') \ No newline at end of file +secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass') diff --git a/kitty/screen.c b/kitty/screen.c index cd2adb1ef..e86d77b3e 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -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; diff --git a/kitty/screen.h b/kitty/screen.h index cdd450b9d..d6883e13f 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -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); diff --git a/kitty/shaders.c b/kitty/shaders.c index d0b698bf9..8ad1dfe15 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -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); } diff --git a/kitty/state.h b/kitty/state.h index fa74a1f6d..810f4cd92 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -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;