From b4b2b44c92d408fb6b4a13d3cba10be9b3770068 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 15 Feb 2025 13:07:48 +0530 Subject: [PATCH] Multicell selections: Clamp to line When the end point of the selection is within a multicell character of the same size and vertical position as the start point, clamp the selection line to the start line to avoid un-intuitive multiline selections. Fixes #8310 --- kitty/screen.c | 44 +++++++++++++++++++++++++++++++++++----- kitty_tests/multicell.py | 30 ++++++++++++++++----------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/kitty/screen.c b/kitty/screen.c index 7d95d371a..36d4be1fa 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -4811,11 +4811,28 @@ continue_line_downwards(Screen *self, index_type bottom_line, SelectionBoundary return bottom_line; } -void -screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, SelectionUpdate upd) { - if (!self->selections.count) return; - self->selections.in_progress = !upd.ended; - Selection *s = self->selections.items; +static int +clamp_selection_input_to_multicell(Screen *self, const Selection *s, index_type x, index_type y, bool in_left_half_of_cell) { + int delta = 0; + int abs_y = y - self->scrolled_by, abs_start_y = s->start.y - s->start_scrolled_by; + if (abs_y == abs_start_y) return delta; + Line *line = checked_range_line(self, abs_start_y); + CPUCell *start, *current; + if (!line || s->start.x >= line->xnum || !(start = &line->cpu_cells[s->start.x])->is_multicell || start->scale < 2) return delta; + int abs_start_top = abs_start_y - start->y; + line = checked_range_line(self, abs_y); + if (x > s->start.x && in_left_half_of_cell) x--; + else if (x < s->start.x && !in_left_half_of_cell) x++; + if (!line || x >= line->xnum) return delta; + current = line->cpu_cells + x; + if (!current->is_multicell) return delta; + int abs_current_top = abs_y - current->y; + if (current->scale == start->scale && current->subscale_n == start->subscale_n && current->subscale_d == start->subscale_d && abs_current_top == abs_start_top) delta = abs_y - abs_start_y; + return delta; +} + +static void +do_update_selection(Screen *self, Selection *s, index_type x, index_type y, bool in_left_half_of_cell, SelectionUpdate upd) { s->input_current.x = x; s->input_current.y = y; s->input_current.in_left_half_of_cell = in_left_half_of_cell; SelectionBoundary start, end, *a = &s->start, *b = &s->end, abs_start, abs_end, abs_current_input; @@ -4965,6 +4982,23 @@ screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_h } } +void +screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, SelectionUpdate upd) { + if (!self->selections.count) return; + self->selections.in_progress = !upd.ended; + Selection *s = self->selections.items; + int delta = clamp_selection_input_to_multicell(self, s, x, y, in_left_half_of_cell); + index_type orig = self->scrolled_by; + if (delta) { + int new_y = y - delta; + if (new_y < 0) { + y = 0; self->scrolled_by += - new_y; + } else y = new_y; + } + do_update_selection(self, s, x, y, in_left_half_of_cell, upd); + self->scrolled_by = orig; +} + static PyObject* mark_as_dirty(Screen *self, PyObject *a UNUSED) { self->is_dirty = true; diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index daf649c24..afccb0846 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -444,30 +444,30 @@ def test_multicell(self: TestMulticell) -> None: s.reset() multicell(s, 'a', width=2, scale=3, subscale_n=1, subscale_d=2, vertical_align=1) - ta('\x1b]66;w=2:s=3:n=1:d=2:v=1;a\x07') + ta(f'\x1b]{TEXT_SIZE_CODE};w=2:s=3:n=1:d=2:v=1;a\x07') s.draw('a') multicell(s, 'b', width=2) s.draw('c') - ta('a\x1b]66;w=2;b\x07c') + ta(f'a\x1b]{TEXT_SIZE_CODE};w=2;b\x07c') multicell(s, 'a') s.cursor.fg = as_rgb(0xffffff) multicell(s, 'b') ta('a\x1b[38:2:255:255:255mb') multicell(s, 'a', scale=2) multicell(s, 'b', scale=2) - ta('\x1b]66;s=2;ab\x07') + ta(f'\x1b]{TEXT_SIZE_CODE};s=2;ab\x07') multicell(s, '😀a', scale=2) - ta('\x1b]66;s=2;😀a\x07') + ta(f'\x1b]{TEXT_SIZE_CODE};s=2;😀a\x07') multicell(s, '😀', scale=2) multicell(s, 'b', width=1, scale=2) - ta('\x1b]66;s=2;😀\x07\x1b]66;w=1:s=2;b\x07') + ta(f'\x1b]{TEXT_SIZE_CODE};s=2;😀\x07\x1b]{TEXT_SIZE_CODE};w=1:s=2;b\x07') multicell(s, 'a', scale=2) s.cursor.fg = as_rgb(0xffffff) multicell(s, 'b', scale=2) - ta('\x1b]66;s=2;a\x07\x1b[38:2:255:255:255m\x1b]66;s=2;b\x07\n\x1b[m\x1b[38:2:255:255:255m') + ta(f'\x1b]{TEXT_SIZE_CODE};s=2;a\x07\x1b[38:2:255:255:255m\x1b]{TEXT_SIZE_CODE};s=2;b\x07\n\x1b[m\x1b[38:2:255:255:255m') multicell(s, 'a', scale=3) multicell(s, 'b', scale=2) - ta('\x1b]66;s=3;a\x07\x1b]66;s=2;b\x07') + ta(f'\x1b]{TEXT_SIZE_CODE};s=3;a\x07\x1b]{TEXT_SIZE_CODE};s=2;b\x07') # rewrap with multicells s = self.create_screen(cols=6, lines=6, scrollback=20) @@ -521,14 +521,14 @@ def test_multicell(self: TestMulticell) -> None: reset() multicell(s, 'a', scale=3), s.draw('b'*(s.columns-3)) resize(s.lines, s.columns-1, 5, 0) - self.ae('\x1b[m\x1b]66;s=3;a\x07bb\x1b[mb', as_ansi().rstrip()) # ]] + self.ae(f'\x1b[m\x1b]{TEXT_SIZE_CODE};s=3;a\x07bb\x1b[mb', as_ansi().rstrip()) # ]] ac(0, 0, is_multicell=True) ac(0, 1, is_multicell=True) ac(3, 1, is_multicell=False, text='b') reset() s.draw('X'), multicell(s, 'a', scale=3), s.draw('12345') resize(s.lines, s.columns-1, 4, 0) - self.ae('\x1b[mX\x1b]66;s=3;a\x071\x1b[m23\x1b[m45', as_ansi().rstrip()) # ]] + self.ae(f'\x1b[mX\x1b]{TEXT_SIZE_CODE};s=3;a\x071\x1b[m23\x1b[m45', as_ansi().rstrip()) # ]] for y in (0, 1): ac(0, y, is_multicell=False), ac(1, y, is_multicell=True), ac(3, y, is_multicell=True) ac(0, 1, is_multicell=False, text='2'), ac(4, 1, is_multicell=False, text='3') @@ -554,7 +554,7 @@ def test_multicell(self: TestMulticell) -> None: reset() s.draw('a'*(s.columns - 1)), multicell(s, 'X', scale=2), s.draw('bcd') resize(s.lines, s.columns + 1, 0, 2) - self.ae('\x1b[maaaaa\x1b]66;s=2;X\x07\x1b[mbcd', as_ansi().rstrip()) # ]]]]]]] + self.ae(f'\x1b[maaaaa\x1b]{TEXT_SIZE_CODE};s=2;X\x07\x1b[mbcd', as_ansi().rstrip()) # ]]]]]]] for y in (0, 1): for x in (1, 2): ac(s.columns-x, y, is_multicell=True) @@ -563,7 +563,7 @@ def test_multicell(self: TestMulticell) -> None: reset() s.draw('a'*(s.columns - 1)), multicell(s, 'X', scale=2), s.draw('bcd1234!') s.resize(s.lines, s.columns + 2) - self.ae('\x1b[maaaaa\x1b]66;s=2;X\x07b\x1b[mcd1234\x1b[m!', as_ansi().rstrip()) # ]]]]]]] + self.ae(f'\x1b[maaaaa\x1b]{TEXT_SIZE_CODE};s=2;X\x07b\x1b[mcd1234\x1b[m!', as_ansi().rstrip()) # ]]]]]]] for y in (0, 1): for x in (1, 2): ac(s.columns-x -1, y, is_multicell=True) @@ -692,7 +692,13 @@ def test_multicell(self: TestMulticell) -> None: s.reset() multicell(s, 'ab cd ef', scale=2) ss(p(6, 1), p(9, 0, in_left_half_of_cell=False)) - ast('ab ef') + ast('cd') + asa(f'\x1b]{TEXT_SIZE_CODE};s=2;cd\x07', '\x1b[m') + + s.reset() + multicell(s, 'ab', scale=2), s.draw(' '), multicell(s, 'cd', scale=2), s.draw(' '), multicell(s, 'ef', scale=2) + ss(p(6, 1), p(9, 0, in_left_half_of_cell=False)) + ast('cd') # Hyperlinks s = self.create_screen(lines=5, cols=8)