From ac32f91a2ebcaacc37e5f95d0ef34197a843b092 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 8 Jan 2025 20:41:44 +0530 Subject: [PATCH] Implement rendering of selections that intersect multicell cells --- kitty/screen.c | 39 +++++++++++++++++++++++++++++++---- kitty_tests/multicell.py | 44 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/kitty/screen.c b/kitty/screen.c index 3fcdeadd2..96beb517b 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -7,11 +7,13 @@ #define EXTRA_INIT { \ PyModule_AddIntMacro(module, SCROLL_LINE); PyModule_AddIntMacro(module, SCROLL_PAGE); PyModule_AddIntMacro(module, SCROLL_FULL); \ + PyModule_AddIntMacro(module, EXTEND_CELL); PyModule_AddIntMacro(module, EXTEND_WORD); PyModule_AddIntMacro(module, EXTEND_LINE); \ if (PyModule_AddFunctions(module, module_methods) != 0) return false; \ } #include "data-types.h" #include "control-codes.h" +#include "screen.h" #include "state.h" #include "iqsort.h" #include "fonts.h" @@ -3359,6 +3361,19 @@ xrange_for_iteration(const IterationData *idata, const int y, const Line *line) return ans; } +static XRange +xrange_for_iteration_with_multicells(const IterationData *idata, const int y, const Line *line) { + XRange ans = xrange_for_iteration(idata, y, line); + if (ans.x_limit > ans.x) { + CPUCell *c; index_type ml; + if (ans.x && (c = &line->cpu_cells[ans.x])->is_multicell && c->x) ans.x = ans.x > c->x ? ans.x - c->x : 0; + if (ans.x_limit < line->xnum && (c = &line->cpu_cells[ans.x_limit-1])->is_multicell && c->x + 1u < (ml = mcd_x_limit(c))) { + ans.x_limit += ml - 1 - c->x; if (ans.x_limit > line->xnum) ans.x_limit = line->xnum; + } + } + return ans; +} + static bool iteration_data_is_empty(const Screen *self, const IterationData *idata) { if (idata->y >= idata->y_limit) return true; @@ -3375,15 +3390,22 @@ static void apply_selection(Screen *self, uint8_t *data, Selection *s, uint8_t set_mask) { iteration_data(s, &s->last_rendered, self->columns, -self->historybuf->count, self->scrolled_by); Line *line; - - for (int y = MAX(0, s->last_rendered.y); y < s->last_rendered.y_limit && y < (int)self->lines; y++) { + const int y_min = MAX(0, s->last_rendered.y), y_limit = MIN(s->last_rendered.y_limit, (int)self->lines); + for (int y = y_min; y < y_limit; y++) { if (self->paused_rendering.expires_at) { 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; - XRange xr = xrange_for_iteration(&s->last_rendered, y, line); - for (index_type x = xr.x; x < xr.x_limit; x++) line_start[x] |= set_mask; + 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; + } + } } s->last_rendered.y = MAX(0, s->last_rendered.y); } @@ -5178,6 +5200,14 @@ line_edge_colors(Screen *self, PyObject *a UNUSED) { return Py_BuildValue("kk", (unsigned long)left, (unsigned long)right); } +static PyObject* +current_selections(Screen *self, PyObject *a UNUSED) { + PyObject *ans = PyBytes_FromStringAndSize(NULL, self->lines * self->columns); + if (!ans) return NULL; + screen_apply_selection(self, PyBytes_AS_STRING(ans), PyBytes_GET_SIZE(ans)); + return ans; +} + WRAP0(update_only_line_graphics_data) WRAP0(bell) @@ -5364,6 +5394,7 @@ static PyMethodDef methods[] = { MND(scroll_to_next_mark, METH_VARARGS) MND(update_only_line_graphics_data, METH_NOARGS) MND(bell, METH_NOARGS) + MND(current_selections, METH_NOARGS) {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""}, {NULL} /* Sentinel */ diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index 77a70d892..9c0c80b92 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -2,7 +2,7 @@ # License: GPLv3 Copyright: 2024, Kovid Goyal -from kitty.fast_data_types import TEXT_SIZE_CODE, test_ch_and_idx, wcswidth +from kitty.fast_data_types import EXTEND_CELL, TEXT_SIZE_CODE, test_ch_and_idx, wcswidth from . import BaseTest, parse_bytes from . import draw_multicell as multicell @@ -591,3 +591,45 @@ def test_multicell(self: TestMulticell) -> None: for y in range(2): for x in range(4): ac(x, y, is_multicell=True) + + # selections + s = self.create_screen(lines=5, cols=8) + + def p(x=0, y=0, in_left_half_of_cell=True): + return (x, y, in_left_half_of_cell) + + def ss(start, end, rectangle_select=False, extend_mode=EXTEND_CELL): + s.start_selection(start[0], start[1], rectangle_select, extend_mode, start[2]) + s.update_selection(end[0], end[1], end[2]) + + def asl(*ranges, bp=1): + actual = s.current_selections() + def as_lists(x): + a = [] + for y in range(s.lines): + a.append(x[y*s.columns: (y+1)*s.columns ]) + return a + + expected = bytearray(s.lines * s.columns) + for (y, x1, x2) in ranges: + pos = y * s.columns + for x in range(x1, x2 + 1): + expected[pos + x] = bp + for i, (e, a) in enumerate(zip(as_lists(bytes(expected)), as_lists(actual))): + self.ae(e, a, f'Row: {i}') + + s.reset() + s.draw('a'), multicell(s, 'b', width=2), s.draw('c') + ss(p(), p(x=1, in_left_half_of_cell=False)) + asl((0, 0, 2)) + ss(p(x=2), p(x=3, in_left_half_of_cell=False)) + asl((0, 1, 3)) + + s.reset() + s.draw('a'), multicell(s, 'b', scale=2), s.draw('c'), multicell(s, 'd', scale=2) + ss(p(), p(x=4, in_left_half_of_cell=False)) + asl((0, 0, 5), (1, 1, 2), (1, 4, 5)) + ss(p(y=1, x=1), p(y=1, x=1, in_left_half_of_cell=False)) + asl((0, 1, 2), (1, 1, 2)) + ss(p(y=1, x=0), p(y=1, x=1, in_left_half_of_cell=False)) # empty leading cell before multiline on y=1 + asl((0, 1, 2), (1, 0, 2))