From b5dff921c4811784624e8ed7a0b0651424e04d09 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Nov 2024 13:56:28 +0530 Subject: [PATCH] Start work on multicell support --- kitty/control-codes.h | 2 + kitty/cursor.c | 2 +- kitty/fonts.c | 68 +++--- kitty/history.c | 10 +- kitty/line-buf.c | 18 +- kitty/line.c | 195 +++++++++-------- kitty/line.h | 91 ++++++-- kitty/lineops.h | 34 +-- kitty/rewrap.h | 4 +- kitty/screen.c | 451 ++++++++++++++++++++++++++++----------- kitty/shaders.c | 10 +- kitty/text-cache.c | 6 + kitty/text-cache.h | 2 + kitty_tests/datatypes.py | 13 -- 14 files changed, 585 insertions(+), 321 deletions(-) diff --git a/kitty/control-codes.h b/kitty/control-codes.h index 04359cda4..7df0c6c2e 100644 --- a/kitty/control-codes.h +++ b/kitty/control-codes.h @@ -233,3 +233,5 @@ #define FILE_TRANSFER_CODE 5113 // Pending mode CSI code #define PENDING_MODE 2026 +// Text size OSC number +#define TEXT_SIZE_CODE 66 diff --git a/kitty/cursor.c b/kitty/cursor.c index 6e69d043e..40924d019 100644 --- a/kitty/cursor.c +++ b/kitty/cursor.c @@ -223,7 +223,7 @@ END_ALLOW_CASE_RANGE const char* cursor_as_sgr(const Cursor *self) { GPUCell blank_cell = { 0 }, cursor_cell = { - .attrs = cursor_to_attrs(self, 1), + .attrs = cursor_to_attrs(self), .fg = self->fg & COL_MASK, .bg = self->bg & COL_MASK, .decoration_fg = self->decoration_fg & COL_MASK, diff --git a/kitty/fonts.c b/kitty/fonts.c index 970675c10..a67b4f4fe 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -466,8 +466,8 @@ face_has_codepoint(const void* face, char_type cp) { } static bool -has_emoji_presentation(const GPUCell *gpu_cell, const ListOfChars *lc) { - if (gpu_cell->attrs.width != 2 || !lc->count) return false; +has_emoji_presentation(const ListOfChars *lc) { + if (!lc->is_multicell || !lc->is_topleft || !lc->count) return false; return is_emoji(lc->chars[0]) && (lc->count == 1 || lc->chars[1] != VS15); } @@ -569,7 +569,7 @@ static ssize_t fallback_font(FontGroup *fg, const GPUCell *gpu_cell, const ListOfChars *lc) { bool bold = gpu_cell->attrs.bold; bool italic = gpu_cell->attrs.italic; - bool emoji_presentation = has_emoji_presentation(gpu_cell, lc); + bool emoji_presentation = has_emoji_presentation(lc); char style = emoji_presentation ? 'a' : 'A'; if (bold) style += italic ? 3 : 2; else style += italic ? 1 : 0; char cell_text[4 * 32] = {style}; @@ -625,7 +625,7 @@ START_ALLOW_CASE_RANGE case 0xf5d0 ... 0xf60d: // branch drawing characters return BOX_FONT; default: - *is_emoji_presentation = has_emoji_presentation(gpu_cell, lc); + *is_emoji_presentation = has_emoji_presentation(lc); ans = in_symbol_maps(fg, lc->chars[0]); if (ans > -1) return ans; switch(gpu_cell->attrs.bold | (gpu_cell->attrs.italic << 1)) { @@ -718,14 +718,12 @@ load_hb_buffer(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_ hb_buffer_clear_contents(harfbuzz_buffer); RAII_ListOfChars(lc); while (num_cells) { - uint16_t prev_width = 0; for (num = 0; num_cells; first_cpu_cell++, first_gpu_cell++, num_cells--) { - if (prev_width == 2) { prev_width = 0; continue; } text_in_cell(first_cpu_cell, tc, &lc); + if (lc.is_multicell && !lc.is_topleft) continue; if (lc.count + num > arraysz(shape_buffer)) break; memcpy(shape_buffer + num, lc.chars, lc.count * sizeof(shape_buffer[0])); num += lc.count; - prev_width = first_gpu_cell->attrs.width; } hb_buffer_add_utf32(harfbuzz_buffer, shape_buffer, num, 0, num); } @@ -752,6 +750,7 @@ typedef struct GlyphRenderScratch { SpritePosition* *sprite_positions; glyph_index *glyphs; size_t sz; + ListOfChars *lc; } GlyphRenderScratch; static GlyphRenderScratch global_glyph_render_scratch = {0}; @@ -777,7 +776,8 @@ render_group(FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPU } ensure_canvas_can_fit(fg, num_cells + 1); - bool was_colored = gpu_cells->attrs.width == 2 && is_emoji(cell_first_char(cpu_cells, tc)); + text_in_cell(cpu_cells, tc, global_glyph_render_scratch.lc); + bool was_colored = has_emoji_presentation(global_glyph_render_scratch.lc); render_glyphs_in_cells(font->face, font->bold, font->italic, info, positions, num_glyphs, fg->canvas.buf, fg->cell_width, fg->cell_height, num_cells, fg->baseline, &was_colored, (FONTS_DATA_HANDLE)fg, center_glyph); if (PyErr_Occurred()) PyErr_Print(); @@ -891,9 +891,13 @@ static unsigned int check_cell_consumed(CellData *cell_data, CPUCell *last_cpu_cell, const TextCache *tc) { cell_data->codepoints_consumed++; if (cell_data->codepoints_consumed >= cell_data->num_codepoints) { - uint16_t width = cell_data->gpu_cell->attrs.width; - cell_data->cpu_cell += MAX(1, width); - cell_data->gpu_cell += MAX(1, width); + uint16_t width = 1; + if (cell_data->cpu_cell->is_multicell) { + MultiCellData mcd = cell_multicell_data(cell_data->cpu_cell, tc); + width = mcd.width * mcd.scale; + } + cell_data->cpu_cell += width; + cell_data->gpu_cell += width; cell_data->codepoints_consumed = 0; if (cell_data->cpu_cell <= last_cpu_cell) { cell_data->num_codepoints = num_codepoints_in_cell(cell_data->cpu_cell, tc); @@ -943,7 +947,7 @@ static void detect_spacer_strategy(hb_font_t *hbf, Font *font, const TextCache *tc) { CPUCell cpu_cells[3] = {0}; for (unsigned i = 0; i < arraysz(cpu_cells); i++) cell_set_char(&cpu_cells[i], '='); - const CellAttrs w1 = {.width=1}; + const CellAttrs w1 = {0}; GPUCell gpu_cells[3] = {{.attrs = w1}, {.attrs = w1}, {.attrs = w1}}; shape(cpu_cells, gpu_cells, arraysz(cpu_cells), hbf, font, false, tc); font->spacer_strategy = SPACERS_BEFORE; @@ -1150,13 +1154,7 @@ group_normal(Font *font, hb_font_t *hbf, const TextCache *tc) { current_group->first_glyph_idx = G(glyph_idx); current_group->first_cell_idx = G(cell_idx); } -#define MOVE_GLYPH_TO_NEXT_GROUP(start_cell_idx) { \ - current_group->num_glyphs--; \ - G(group_idx)++; current_group = G(groups) + G(group_idx); \ - current_group->first_cell_idx = start_cell_idx; \ - current_group->num_glyphs = 1; \ - current_group->first_glyph_idx = G(glyph_idx); \ -} + if (is_special) current_group->has_special_glyph = true; if (is_last_glyph) { // soak up all remaining cells @@ -1216,22 +1214,21 @@ collapse_pua_space_ligature(index_type num_cells) { g->num_glyphs = 1; } -#undef MOVE_GLYPH_TO_NEXT_GROUP - static bool -is_group_calt_ligature(const Group *group) { - GPUCell *first_cell = G(first_gpu_cell) + group->first_cell_idx; - return group->num_cells > 1 && group->has_special_glyph && first_cell->attrs.width == 1; +is_group_calt_ligature(const Group *group, const TextCache *tc) { + if (group->num_cells < 2 || !group->has_special_glyph) return false; + const CPUCell *first_cell = G(first_cpu_cell) + group->first_cell_idx; + return !first_cell->is_multicell || cell_multicell_data(first_cell, tc).width == 1; } static void -split_run_at_offset(index_type cursor_offset, index_type *left, index_type *right) { +split_run_at_offset(index_type cursor_offset, index_type *left, index_type *right, const TextCache *tc) { *left = 0; *right = 0; for (unsigned idx = 0; idx < G(group_idx) + 1; idx++) { Group *group = G(groups) + idx; if (group->first_cell_idx <= cursor_offset && cursor_offset < group->first_cell_idx + group->num_cells) { - if (is_group_calt_ligature(group)) { + if (is_group_calt_ligature(group, tc)) { // likely a calt ligature *left = group->first_cell_idx; *right = group->first_cell_idx + group->num_cells; } @@ -1256,6 +1253,7 @@ render_groups(FontGroup *fg, Font *font, bool center_glyph, const TextCache *tc) a(glyphs); a(sprite_positions); #undef a global_glyph_render_scratch.sz = sz; + if (!global_glyph_render_scratch.lc) global_glyph_render_scratch.lc = alloc_list_of_chars(); } for (unsigned i = 0; i < group->num_glyphs; i++) global_glyph_render_scratch.glyphs[i] = G(info)[group->first_glyph_idx + i].codepoint; render_group(fg, group->num_cells, group->num_glyphs, G(first_cpu_cell) + group->first_cell_idx, G(first_gpu_cell) + group->first_cell_idx, G(info) + group->first_glyph_idx, G(positions) + group->first_glyph_idx, font, global_glyph_render_scratch.glyphs, group->num_glyphs, center_glyph, tc); @@ -1271,7 +1269,15 @@ test_shape(PyObject UNUSED *self, PyObject *args) { int index = 0; if(!PyArg_ParseTuple(args, "O!|zi", &Line_Type, &line, &path, &index)) return NULL; index_type num = 0; - while(num < line->xnum && cell_has_text(line->cpu_cells + num)) num += line->gpu_cells[num].attrs.width; + const CPUCell *c; + while(num < line->xnum && cell_has_text(line->cpu_cells + num)) { + index_type width = 1; + if ((c = line->cpu_cells + num)->is_multicell) { + MultiCellData mcd = cell_multicell_data(c, line->text_cache); + width = mcd.width * mcd.scale; + } + num += width; + } PyObject *face = NULL; Font *font; if (!num_font_groups) { PyErr_SetString(PyExc_RuntimeError, "must create at least one font group first"); return NULL; } @@ -1313,7 +1319,7 @@ render_run(FontGroup *fg, CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, inde if (pua_space_ligature) collapse_pua_space_ligature(num_cells); else if (cursor_offset > -1) { // false if DISABLE_LIGATURES_NEVER index_type left, right; - split_run_at_offset(cursor_offset, &left, &right); + split_run_at_offset(cursor_offset, &left, &right, tc); if (right > left) { if (left) { shape_run(first_cpu_cell, first_gpu_cell, left, &fg->fonts[font_idx], false, tc); @@ -1377,12 +1383,11 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, bool center_glyph = false; bool disable_ligature_at_cursor = cursor != NULL && disable_ligature_strategy == DISABLE_LIGATURES_CURSOR && lnum == cursor->y; index_type first_cell_in_run, i; - uint16_t prev_width = 0; for (i=0, first_cell_in_run=0; i < line->xnum; i++) { - if (prev_width == 2) { prev_width = 0; continue; } CPUCell *cpu_cell = line->cpu_cells + i; GPUCell *gpu_cell = line->gpu_cells + i; text_in_cell(cpu_cell, line->text_cache, lc); + if (lc->is_multicell && !lc->is_topleft) continue; bool is_main_font, is_emoji_presentation; ssize_t cell_font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_main_font, &is_emoji_presentation, line->text_cache, lc); const char_type first_ch = lc->chars[0]; @@ -1425,12 +1430,10 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, render_run(fg, line->cpu_cells + i, line->gpu_cells + i, num_spaces + 1, cell_font_idx, true, center_glyph, -1, disable_ligature_strategy, line->text_cache); run_font_idx = NO_FONT; first_cell_in_run = i + num_spaces + 1; - prev_width = line->gpu_cells[i+num_spaces].attrs.width; i += num_spaces; continue; } } - prev_width = gpu_cell->attrs.width; if (run_font_idx == NO_FONT) run_font_idx = cell_font_idx; if (run_font_idx == cell_font_idx) continue; RENDER @@ -1592,6 +1595,7 @@ finalize(void) { free(group_state.groups); group_state.groups = NULL; group_state.groups_capacity = 0; free(global_glyph_render_scratch.glyphs); free(global_glyph_render_scratch.sprite_positions); + if (global_glyph_render_scratch.lc) { cleanup_list_of_chars(global_glyph_render_scratch.lc); free(global_glyph_render_scratch.lc); } global_glyph_render_scratch = (GlyphRenderScratch){0}; } diff --git a/kitty/history.c b/kitty/history.c index e7c0eb438..e0aba9591 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -170,7 +170,7 @@ init_line(HistoryBuf *self, index_type num, Line *l) { l->gpu_cells = gpu_lineptr(self, num); l->attrs = *attrptr(self, num); if (num > 0) { - l->attrs.is_continued = gpu_lineptr(self, num - 1)[self->xnum-1].attrs.next_char_was_wrapped; + l->attrs.is_continued = cpu_lineptr(self, num - 1)[self->xnum-1].next_char_was_wrapped; } else { l->attrs.is_continued = false; size_t sz; @@ -188,7 +188,7 @@ historybuf_init_line(HistoryBuf *self, index_type lnum, Line *l) { bool history_buf_endswith_wrap(HistoryBuf *self) { - return gpu_lineptr(self, index_of(self, 0))[self->xnum-1].attrs.next_char_was_wrapped; + return cpu_lineptr(self, index_of(self, 0))[self->xnum-1].next_char_was_wrapped; } CPUCell* @@ -272,7 +272,7 @@ pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) { if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) { char line_end[2]; size_t num = 0; line_end[num++] = '\r'; - if (!l.gpu_cells[l.xnum - 1].attrs.next_char_was_wrapped) line_end[num++] = '\n'; + if (!l.cpu_cells[l.xnum - 1].next_char_was_wrapped) line_end[num++] = '\n'; pagerhist_write_bytes(ph, (const uint8_t*)line_end, num); } } @@ -307,7 +307,7 @@ historybuf_pop_line(HistoryBuf *self, Line *line) { static void history_buf_set_last_char_as_continuation(HistoryBuf *self, index_type y, bool wrapped) { if (self->count > 0) { - gpu_lineptr(self, index_of(self, y))[self->xnum-1].attrs.next_char_was_wrapped = wrapped; + cpu_lineptr(self, index_of(self, y))[self->xnum-1].next_char_was_wrapped = wrapped; } } @@ -358,7 +358,7 @@ as_ansi(HistoryBuf *self, PyObject *callback) { for(unsigned int i = 0; i < self->count; i++) { init_line(self, i, &l); line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0); - if (!l.gpu_cells[l.xnum - 1].attrs.next_char_was_wrapped) { + if (!l.cpu_cells[l.xnum - 1].next_char_was_wrapped) { ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false); output.buf[output.len++] = '\n'; } diff --git a/kitty/line-buf.c b/kitty/line-buf.c index 75edf6dd2..c64cf7cc3 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -131,6 +131,12 @@ linebuf_init_cells(LineBuf *lb, index_type idx, CPUCell **c, GPUCell **g) { *g = gpu_lineptr(lb, ynum); } +CPUCell* +linebuf_cpu_cells_for_line(LineBuf *lb, index_type idx) { + const index_type ynum = lb->line_map[idx]; + return cpu_lineptr(lb, ynum); +} + static void init_line(LineBuf *lb, Line *l, index_type ynum) { l->cpu_cells = cpu_lineptr(lb, ynum); @@ -142,7 +148,7 @@ linebuf_init_line(LineBuf *self, index_type idx) { self->line->ynum = idx; self->line->xnum = self->xnum; self->line->attrs = self->line_attrs[idx]; - self->line->attrs.is_continued = idx > 0 ? gpu_lineptr(self, self->line_map[idx - 1])[self->xnum - 1].attrs.next_char_was_wrapped : false; + self->line->attrs.is_continued = idx > 0 ? cpu_lineptr(self, self->line_map[idx - 1])[self->xnum - 1].next_char_was_wrapped : false; init_line(self, self->line, self->line_map[idx]); } @@ -180,20 +186,20 @@ line(LineBuf *self, PyObject *y) { return (PyObject*)self->line; } -unsigned int -linebuf_char_width_at(LineBuf *self, index_type x, index_type y) { - return gpu_lineptr(self, self->line_map[y])[x].attrs.width; +CPUCell* +linebuf_cpu_cell_at(LineBuf *self, index_type x, index_type y) { + return &cpu_lineptr(self, self->line_map[y])[x]; } bool linebuf_line_ends_with_continuation(LineBuf *self, index_type y) { - return y < self->ynum ? gpu_lineptr(self, self->line_map[y])[self->xnum - 1].attrs.next_char_was_wrapped : false; + return y < self->ynum ? cpu_lineptr(self, self->line_map[y])[self->xnum - 1].next_char_was_wrapped : false; } void linebuf_set_last_char_as_continuation(LineBuf *self, index_type y, bool continued) { if (y < self->ynum) { - gpu_lineptr(self, self->line_map[y])[self->xnum - 1].attrs.next_char_was_wrapped = continued; + cpu_lineptr(self, self->line_map[y])[self->xnum - 1].next_char_was_wrapped = continued; } } diff --git a/kitty/line.c b/kitty/line.c index 885e197a4..7ceba36a4 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -12,7 +12,7 @@ #include "unicode-data.h" #include "lineops.h" #include "charsets.h" -#include "wcwidth-std.h" +#include "control-codes.h" extern PyTypeObject Cursor_Type; static_assert(sizeof(char_type) == sizeof(Py_UCS4), "Need to perform conversion to Py_UCS4"); @@ -27,9 +27,66 @@ dealloc(Line* self) { Py_TYPE(self)->tp_free((PyObject*)self); } +static unsigned +nonnegative_integer_as_utf32(unsigned num, ANSIBuf *output) { + unsigned num_digits = 0; + if (!num) num_digits = 1; + else { + unsigned temp = num; + while (temp > 0) { + temp /= 10; + num_digits++; + } + } + ensure_space_for(output, buf, output->buf[0], output->len + num_digits, capacity, 2048, false); + if (!num) output->buf[output->len++] = '0'; + else { + char_type *result = output->buf + output->len; + unsigned i = num_digits - 1; + do { + uint32_t digit = num % 10; + result[i--] = '0' + digit; + num /= 10; + output->len++; + } while (num > 0); + } + return num_digits; +} + +static unsigned +write_multicell_ansi_prefix(MultiCellData mcd, ANSIBuf *output) { + unsigned pos = output->len; + ensure_space_for(output, buf, output->buf[0], output->len + 128, capacity, 2048, false); +#define w(x) output->buf[output->len++] = x + w(0x1b); w(']'); + for (unsigned i = 0; i < sizeof(xstr(TEXT_SIZE_CODE)) - 1; i++) w(xstr(TEXT_SIZE_CODE)[i]); + w(';'); + if (mcd.width > 1) { + w('w'); w('='); nonnegative_integer_as_utf32(mcd.width, output); w(':'); + } + if (mcd.scale > 1) { + w('s'); w('='); nonnegative_integer_as_utf32(mcd.scale, output); w(':'); + } + if (mcd.subscale) { + w('S'); w('='); nonnegative_integer_as_utf32(mcd.subscale, output); w(':'); + } + if (output->buf[output->len - 1] == ':') output->len--; + w(';'); +#undef w + return output->len - pos; +} + static unsigned text_in_cell_ansi(const CPUCell *c, TextCache *tc, ANSIBuf *output) { - if (c->ch_is_idx) return tc_chars_at_index_ansi(tc, c->ch_or_idx, output); + if (c->ch_is_idx) { + if (!c->is_multicell) return tc_chars_at_index_ansi(tc, c->ch_or_idx, output); + if (c->x || c->y) return 0; + MultiCellData mcd = cell_multicell_data(c, tc); + unsigned n = write_multicell_ansi_prefix(mcd, output); + n += tc_chars_at_index_ansi(tc, c->ch_or_idx, output); + output->buf[output->len] = '\a'; + return n; + } ensure_space_for(output, buf, output->buf[0], output->len + 1, capacity, 2048, false); output->buf[output->len++] = c->ch_or_idx; return 1; @@ -216,6 +273,10 @@ text_at(Line* self, Py_ssize_t xval) { if (cell->ch_is_idx) { RAII_ListOfChars(lc); tc_chars_at_index(self->text_cache, cell->ch_or_idx, &lc); + if (cell->is_multicell) { + if (cell->x || cell->y || !lc.count) return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, lc.chars, 0); + return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, lc.chars + 1, lc.count - 1); + } return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, lc.chars, lc.count); } Py_UCS4 ch = cell->ch_or_idx; @@ -256,7 +317,6 @@ PyObject* unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const bool add_trailing_newline, const bool skip_zero_cells) { size_t n = 0; ListOfChars lc; - char_type previous_width = 0; for (index_type i = start; i < limit; i++) { lc.chars = global_unicode_in_range_buf.chars + n; lc.capacity = global_unicode_in_range_buf.capacity - n; while (!text_in_cell_without_alloc(self->cpu_cells + i, self->text_cache, &lc)) { @@ -266,8 +326,8 @@ unicode_in_range(const Line *self, const index_type start, const index_type limi global_unicode_in_range_buf.capacity = ns; global_unicode_in_range_buf.chars = np; lc.chars = global_unicode_in_range_buf.chars + n; lc.capacity = global_unicode_in_range_buf.capacity - n; } + if (lc.is_multicell && !lc.is_topleft) continue; if (!lc.chars[0]) { - if (previous_width == 2) { previous_width = 0; continue; }; if (skip_zero_cells) continue; lc.chars[0] = ' '; } @@ -279,9 +339,8 @@ unicode_in_range(const Line *self, const index_type start, const index_type limi num_cells_to_skip_for_tab--; } } else n += include_cc ? lc.count : 1; - previous_width = self->gpu_cells[i].attrs.width; } - if (add_trailing_newline && !self->gpu_cells[self->xnum-1].attrs.next_char_was_wrapped && n < global_unicode_in_range_buf.capacity) global_unicode_in_range_buf.chars[n++] = '\n'; + if (add_trailing_newline && !self->cpu_cells[self->xnum-1].next_char_was_wrapped && n < global_unicode_in_range_buf.capacity) global_unicode_in_range_buf.chars[n++] = '\n'; return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, global_unicode_in_range_buf.chars, n); } @@ -351,8 +410,7 @@ line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell, index_type bool escape_code_written = false; output->len = 0; index_type limit = MIN(stop_before, xlimit_for_line(self)); - char_type previous_width = 0; - if (prefix_char) { WRITE_CH(prefix_char); previous_width = wcwidth_std(prefix_char); } + if (prefix_char) { WRITE_CH(prefix_char); } switch (self->attrs.prompt_kind) { case UNKNOWN_PROMPT_KIND: @@ -392,7 +450,6 @@ line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell, index_type unsigned n = text_in_cell_ansi(self->cpu_cells + pos, self->text_cache, output); if (output->buf[output->len - n] == 0) { - if (previous_width == 2) { previous_width = 0; output->len -= n; continue; } output->buf[output->len - n] = ' '; } @@ -407,7 +464,6 @@ line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell, index_type } } *prev_cell = cell; - previous_width = cell->attrs.width; } return escape_code_written; #undef CMP_ATTRS @@ -433,7 +489,7 @@ as_ansi(Line* self, PyObject *a UNUSED) { static PyObject* last_char_has_wrapped_flag(Line* self, PyObject *a UNUSED) { #define last_char_has_wrapped_flag_doc "Return True if the last cell of this line has the wrapped flags set" - if (self->gpu_cells[self->xnum - 1].attrs.next_char_was_wrapped) { Py_RETURN_TRUE; } + if (self->cpu_cells[self->xnum - 1].next_char_was_wrapped) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } @@ -457,22 +513,11 @@ width(Line *self, PyObject *val) { #define width_doc "width(x) -> the width of the character at x" unsigned long x = PyLong_AsUnsignedLong(val); if (x >= self->xnum) { PyErr_SetString(PyExc_ValueError, "Out of bounds"); return NULL; } - return PyLong_FromUnsignedLong((unsigned long) (self->gpu_cells[x].attrs.width)); -} - -bool -line_add_combining_char(CPUCell *cpu_cells, GPUCell *gpu_cells, TextCache *tc, ListOfChars *lc, uint32_t ch, unsigned int x) { - CPUCell *cell = cpu_cells + x; - if (!cell_has_text(cell)) { - if (x > 0 && (gpu_cells[x-1].attrs.width) == 2 && cell_has_text(cpu_cells + x - 1)) cell = cpu_cells + x - 1; - else return false; // don't allow adding combining chars to a null cell - } - text_in_cell(cell, tc, lc); - ensure_space_for_chars(lc, lc->count + 1); - lc->chars[lc->count++] = ch; - cell->ch_or_idx = tc_get_or_insert_chars(tc, lc); - cell->ch_is_idx = true; - return true; + const CPUCell *c = self->cpu_cells + x; + if (!cell_has_text(c)) return 0; + unsigned long ans = 1; + if (c->is_multicell) ans = c->x || c->y ? 0 : cell_multicell_data(c, self->text_cache).width; + return PyLong_FromUnsignedLong(ans); } static PyObject* @@ -485,8 +530,14 @@ add_combining_char(Line* self, PyObject *args) { PyErr_SetString(PyExc_ValueError, "Column index out of bounds"); return NULL; } + CPUCell *cell = self->cpu_cells + x; + if (cell->is_multicell) { PyErr_SetString(PyExc_IndexError, "cannot set combining char in a multicell"); return NULL; } RAII_ListOfChars(lc); - line_add_combining_char(self->cpu_cells, self->gpu_cells, self->text_cache, &lc, new_char, x); + text_in_cell(cell, self->text_cache, &lc); + ensure_space_for_chars(&lc, lc.count + 1); + lc.chars[lc.count++] = new_char; + cell->ch_or_idx = tc_get_or_insert_chars(self->text_cache, &lc); + cell->ch_is_idx = true; Py_RETURN_NONE; } @@ -512,7 +563,7 @@ set_text(Line* self, PyObject *args) { PyErr_SetString(PyExc_ValueError, "Out of bounds offset/sz"); return NULL; } - CellAttrs attrs = cursor_to_attrs(cursor, 1); + CellAttrs attrs = cursor_to_attrs(cursor); color_type fg = (cursor->fg & COL_MASK), bg = cursor->bg & COL_MASK; color_type dfg = cursor->decoration_fg & COL_MASK; @@ -550,11 +601,9 @@ cursor_from(Line* self, PyObject *args) { void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch) { - const uint16_t width = ch ? 1 : 0; const CPUCell cc = {.ch_or_idx=ch}; if (at + num > self->xnum) num = self->xnum > at ? self->xnum - at : 0; memset_array(self->cpu_cells + at, cc, num); - for (index_type i = at; i < at + num; i++) self->gpu_cells[i].attrs.width = width; } static PyObject* @@ -579,7 +628,6 @@ line_apply_cursor(Line *self, const Cursor *cursor, unsigned int at, unsigned in memset_array(self->gpu_cells + at, gc, num); } else { for (index_type i = at; i < self->xnum && i < at + num; i++) { - gc.attrs.width = self->gpu_cells[i].attrs.width; gc.attrs.mark = self->gpu_cells[i].attrs.mark; gc.sprite_x = self->gpu_cells[i].sprite_x; gc.sprite_y = self->gpu_cells[i].sprite_y; gc.sprite_z = self->gpu_cells[i].sprite_z; memcpy(self->gpu_cells + i, &gc, sizeof(gc)); @@ -598,48 +646,6 @@ apply_cursor(Line* self, PyObject *args) { Py_RETURN_NONE; } -void -line_right_shift(Line *self, unsigned int at, unsigned int num) { - for(index_type i = self->xnum - 1; i >= at + num; i--) { - COPY_SELF_CELL(i - num, i) - } - // Check if a wide character was split at the right edge - if (self->gpu_cells[self->xnum - 1].attrs.width != 1) { - self->cpu_cells[self->xnum - 1].val = 0; - self->cpu_cells[self->xnum - 1].hyperlink_id = 0; - self->gpu_cells[self->xnum - 1].attrs = (CellAttrs){0}; - clear_sprite_position(self->gpu_cells[self->xnum - 1]); - } -} - -static PyObject* -right_shift(Line *self, PyObject *args) { -#define right_shift_doc "right_shift(at, num) -> ..." - unsigned int at, num; - if (!PyArg_ParseTuple(args, "II", &at, &num)) return NULL; - if (at >= self->xnum || at + num > self->xnum) { - PyErr_SetString(PyExc_ValueError, "Out of bounds"); - return NULL; - } - if (num > 0) { - line_right_shift(self, at, num); - } - Py_RETURN_NONE; -} - -static PyObject* -left_shift(Line *self, PyObject *args) { -#define left_shift_doc "left_shift(at, num) -> ..." - unsigned int at, num; - if (!PyArg_ParseTuple(args, "II", &at, &num)) return NULL; - if (at >= self->xnum || at + num > self->xnum) { - PyErr_SetString(PyExc_ValueError, "Out of bounds"); - return NULL; - } - if (num > 0) left_shift_line(self, at, num); - Py_RETURN_NONE; -} - static color_type resolve_color(const ColorProfile *cp, color_type val, color_type defval) { switch(val & 0xff) { @@ -655,7 +661,7 @@ resolve_color(const ColorProfile *cp, color_type val, color_type defval) { bool colors_for_cell(Line *self, const ColorProfile *cp, index_type *x, color_type *fg, color_type *bg, bool *reversed) { if (*x >= self->xnum) return false; - if (*x > 0 && !self->gpu_cells[*x].attrs.width && self->gpu_cells[*x-1].attrs.width == 2) (*x)--; + while (self->cpu_cells[*x].is_multicell && self->cpu_cells[*x].x && *x) (*x)--; *fg = resolve_color(cp, self->gpu_cells[*x].fg, *fg); *bg = resolve_color(cp, self->gpu_cells[*x].bg, *bg); if (self->gpu_cells[*x].attrs.reverse) { @@ -672,28 +678,25 @@ line_get_char(Line *self, index_type at) { if (self->cpu_cells[at].ch_is_idx) { RAII_ListOfChars(lc); text_in_cell(self->cpu_cells + at, self->text_cache, &lc); + if (lc.is_multicell && !lc.is_topleft) return 0; return lc.chars[0]; - } else { - char_type ch = self->cpu_cells[at].ch_or_idx; - if (!ch && at > 0 && (self->gpu_cells[at-1].attrs.width) > 1) ch = line_get_char(self, at - 1); - return ch; - } + } else return self->cpu_cells[at].ch_or_idx; } void -line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, hyperlink_id_type hyperlink_id) { +line_set_char(Line *self, unsigned int at, uint32_t ch, Cursor *cursor, hyperlink_id_type hyperlink_id) { GPUCell *g = self->gpu_cells + at; - if (cursor == NULL) { - g->attrs.width = width; - } else { - g->attrs = cursor_to_attrs(cursor, width); + if (cursor != NULL) { + g->attrs = cursor_to_attrs(cursor); g->fg = cursor->fg & COL_MASK; g->bg = cursor->bg & COL_MASK; g->decoration_fg = cursor->decoration_fg & COL_MASK; } - cell_set_char(self->cpu_cells + at, ch); - self->cpu_cells[at].hyperlink_id = hyperlink_id; + CPUCell *c = self->cpu_cells + at; + c->val = 0; + cell_set_char(c, ch); + c->hyperlink_id = hyperlink_id; if (OPT(underline_hyperlinks) == UNDERLINE_ALWAYS && hyperlink_id) { g->decoration_fg = ((OPT(url_color) & COL_MASK) << 8) | 2; g->attrs.decoration = OPT(url_style); @@ -713,7 +716,10 @@ set_char(Line *self, PyObject *args) { PyErr_SetString(PyExc_ValueError, "Out of bounds"); return NULL; } - line_set_char(self, at, ch, width, cursor, hyperlink_id); + if (width != 1) { + PyErr_SetString(PyExc_NotImplementedError, "TODO: Implement setting wide char"); return NULL; + } + line_set_char(self, at, ch, cursor, hyperlink_id); Py_RETURN_NONE; } @@ -832,9 +838,12 @@ apply_mark(Line *line, const uint16_t mark, index_type *cell_pos, unsigned int * num_cells_to_skip_for_tab--; MARK; } - } else if ((line->gpu_cells[x].attrs.width) > 1 && x + 1 < line->xnum && !cell_has_text(line->cpu_cells+x+1)) { - x++; - MARK; + } else if (line->cpu_cells[x].is_multicell) { + MultiCellData mcd = {.val=lc.chars[lc.count]}; + *match_pos += lc.count - 1; + index_type x_limit = MIN(line->xnum, mcd_x_limit(mcd)); + for (; x < x_limit; x++) { MARK; } + x--; } else { *match_pos += lc.count - 1; } @@ -919,7 +928,7 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t } APPEND_AND_DECREF(t); if (insert_wrap_markers) APPEND(cr); - need_newline = !line->gpu_cells[line->xnum-1].attrs.next_char_was_wrapped; + need_newline = !line->cpu_cells[line->xnum-1].next_char_was_wrapped; } if (need_newline && add_trailing_newline) APPEND(nl); if (ansibuf->active_hyperlink_id) { @@ -964,8 +973,6 @@ static PyMethodDef methods[] = { METHOD(apply_cursor, METH_VARARGS) METHOD(clear_text, METH_VARARGS) METHOD(copy_char, METH_VARARGS) - METHOD(right_shift, METH_VARARGS) - METHOD(left_shift, METH_VARARGS) METHOD(set_char, METH_VARARGS) METHOD(set_attribute, METH_VARARGS) METHOD(as_ansi, METH_NOARGS) diff --git a/kitty/line.h b/kitty/line.h index 9077012b6..7aee2d75a 100644 --- a/kitty/line.h +++ b/kitty/line.h @@ -9,9 +9,16 @@ #include "text-cache.h" +// TODO: Test setting of ch_and_idx to make sure the right ch_is_idx bit is set +// TODO: Test handling of calt ligatures with scale see is_group_calt_ligature() +// TODO: Font Rendering of scale > 1 and width > 1 +// TODO: Handle selection with multicell +// TODO: URL detection with multicell +// TODO: Cursor rendering over multicell +// TODO: Test the escape codes to delete and insert characters and lines with multicell + typedef union CellAttrs { struct { - uint16_t width : 2; uint16_t decoration : 3; uint16_t bold : 1; uint16_t italic : 1; @@ -19,8 +26,7 @@ typedef union CellAttrs { uint16_t strike : 1; uint16_t dim : 1; uint16_t mark : 2; - uint16_t next_char_was_wrapped : 1; - uint16_t : 3; + uint16_t : 6; }; uint16_t val; } CellAttrs; @@ -29,7 +35,7 @@ static_assert(sizeof(CellAttrs) == sizeof(uint16_t), "Fix the ordering of CellAt #define WIDTH_MASK (3u) #define DECORATION_MASK (7u) #define NUM_UNDERLINE_STYLES (5u) -#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK, .next_char_was_wrapped=1}).val)) +#define SGR_MASK (~(((CellAttrs){.mark=MARK_MASK}).val)) // Text presentation selector #define VS15 0xfe0e // Emoji presentation selector @@ -42,12 +48,33 @@ typedef struct { } GPUCell; static_assert(sizeof(GPUCell) == 20, "Fix the ordering of GPUCell"); +typedef union MultiCellData { + struct { + char_type scale: 3; + char_type width: 3; + char_type subscale: 2; + char_type vertical_align: 2; + char_type : 21; + char_type msb : 1; + }; + char_type val; +} MultiCellData; +static_assert(sizeof(MultiCellData) == sizeof(char_type), "Fix the ordering of MultiCellData"); + typedef union CPUCell { struct { - bool ch_is_idx: 1; char_type ch_or_idx: sizeof(char_type) * 8 - 1; - hyperlink_id_type hyperlink_id: sizeof(hyperlink_id_type) * 8; - uint16_t : 16; + char_type ch_is_idx: 1; + char_type hyperlink_id: sizeof(hyperlink_id_type) * 8; + char_type x : 8; + char_type y : 3; + char_type is_multicell : 1; + char_type next_char_was_wrapped : 1; + char_type : 3; + }; + struct { + char_type ch_and_idx: sizeof(char_type) * 8; + char_type : sizeof(char_type) * 8; }; uint64_t val; } CPUCell; @@ -80,17 +107,33 @@ typedef struct { Line* alloc_line(TextCache *text_cache); void apply_sgr_to_cells(GPUCell *first_cell, unsigned int cell_count, int *params, unsigned int count, bool is_group); const char* cell_as_sgr(const GPUCell *, const GPUCell *); -static inline bool cell_has_text(const CPUCell *c) { return c->ch_is_idx || c->ch_or_idx; } -static inline void cell_set_char(CPUCell *c, char_type ch) { c->ch_is_idx = false; c->ch_or_idx = ch; } -static inline bool cell_is_char(const CPUCell *c, char_type ch) { return !c->ch_is_idx && c->ch_or_idx == ch; } +static inline bool cell_has_text(const CPUCell *c) { return c->ch_and_idx != 0; } +static inline void cell_set_char(CPUCell *c, char_type ch) { c->ch_and_idx = ch & 0x7fffffff; } +static inline bool cell_is_char(const CPUCell *c, char_type ch) { return c->ch_and_idx == ch; } static inline unsigned num_codepoints_in_cell(const CPUCell *c, const TextCache *tc) { - return c->ch_is_idx ? tc_num_codepoints(tc, c->ch_or_idx) : (c->ch_or_idx ? 1 : 0); + unsigned ans; + if (c->ch_is_idx) { + ans = tc_num_codepoints(tc, c->ch_or_idx); + if (c->is_multicell) ans--; + } else ans = c->ch_or_idx ? 1 : 0; + return ans; } +static inline MultiCellData cell_multicell_data(const CPUCell *c, const TextCache *tc) { + return (MultiCellData){.val = tc_last_char_at_index(tc, c->ch_or_idx)}; +} +static inline unsigned mcd_x_limit(MultiCellData mcd) { return mcd.scale * mcd.width; } static inline void text_in_cell(const CPUCell *c, const TextCache *tc, ListOfChars *ans) { - if (c->ch_is_idx) tc_chars_at_index(tc, c->ch_or_idx, ans); - else { + ans->is_multicell = false; + if (c->ch_is_idx) { + tc_chars_at_index(tc, c->ch_or_idx, ans); + if (c->is_multicell) { + ans->is_multicell = true; + ans->is_topleft = c->x + c->y == 0; + if (ans->count > 0) { ans->count--; } + } + } else { ans->count = 1; ans->chars[0] = c->ch_or_idx; } @@ -98,7 +141,16 @@ text_in_cell(const CPUCell *c, const TextCache *tc, ListOfChars *ans) { static inline bool text_in_cell_without_alloc(const CPUCell *c, const TextCache *tc, ListOfChars *ans) { - if (c->ch_is_idx) return tc_chars_at_index_without_alloc(tc, c->ch_or_idx, ans); + ans->is_multicell = false; + if (c->ch_is_idx) { + if (!tc_chars_at_index_without_alloc(tc, c->ch_or_idx, ans)) return false; + if (c->is_multicell) { + ans->is_multicell = true; + ans->is_topleft = c->x + c->y == 0; + if (ans->count > 0) { ans->count--; } + } + return true; + } ans->count = 1; if (ans->capacity < 1) return false; ans->chars[0] = c->ch_or_idx; @@ -116,15 +168,18 @@ cell_set_chars(CPUCell *c, TextCache *tc, const ListOfChars *lc) { static inline char_type cell_first_char(const CPUCell *c, const TextCache *tc) { - if (c->ch_is_idx) return tc_first_char_at_index(tc, c->ch_or_idx); + if (c->ch_is_idx) { + if (c->is_multicell && (c->x || c->y)) return 0; + return tc_first_char_at_index(tc, c->ch_or_idx); + } return c->ch_or_idx; } static inline CellAttrs -cursor_to_attrs(const Cursor *c, const uint16_t width) { +cursor_to_attrs(const Cursor *c) { CellAttrs ans = { - .width=width, .decoration=c->decoration, .bold=c->bold, .italic=c->italic, .reverse=c->reverse, + .decoration=c->decoration, .bold=c->bold, .italic=c->italic, .reverse=c->reverse, .strike=c->strikethrough, .dim=c->dim}; return ans; } @@ -135,6 +190,6 @@ attrs_to_cursor(const CellAttrs attrs, Cursor *c) { c->reverse = attrs.reverse; c->strikethrough = attrs.strike; c->dim = attrs.dim; } -#define cursor_as_gpu_cell(cursor) {.attrs=cursor_to_attrs(cursor, 0), .fg=(cursor->fg & COL_MASK), .bg=(cursor->bg & COL_MASK), .decoration_fg=cursor->decoration_fg & COL_MASK} +#define cursor_as_gpu_cell(cursor) {.attrs=cursor_to_attrs(cursor), .fg=(cursor->fg & COL_MASK), .bg=(cursor->bg & COL_MASK), .decoration_fg=cursor->decoration_fg & COL_MASK} diff --git a/kitty/lineops.h b/kitty/lineops.h index 514e58d59..04573b3a9 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -16,7 +16,7 @@ static inline bool set_named_attribute_on_line(GPUCell *cells, const char* which, uint16_t val, index_type xnum) { // Set a single attribute on all cells in the line #define s(q) if (strcmp(#q, which) == 0) { set_attribute_on_line(cells, q, val, xnum); return true; } - s(reverse); s(width); s(strike); s(dim); s(mark); s(bold); s(italic); s(decoration); + s(reverse); s(strike); s(dim); s(mark); s(bold); s(italic); s(decoration); return false; #undef s } @@ -32,19 +32,16 @@ static inline void clear_chars_in_line(CPUCell *cpu_cells, GPUCell *gpu_cells, index_type xnum, char_type ch) { // Clear only the char part of each cell, the rest must have been cleared by a memset or similar if (ch) { - static const CellAttrs empty = {.width=1}; - for (index_type i = 0; i < xnum; i++) { cpu_cells[i].val = 0; cpu_cells[i].ch_or_idx = ch; gpu_cells[i].attrs = empty; } + static const CellAttrs empty = {0}; + const CPUCell c = {.ch_or_idx=ch}; + for (index_type i = 0; i < xnum; i++) { cpu_cells[i] = c; gpu_cells[i].attrs = empty; } } } static inline index_type xlimit_for_line(const Line *line) { index_type xlimit = line->xnum; - if (BLANK_CHAR == 0) { - const CPUCell *c; - while (xlimit > 0 && !((c = line->cpu_cells + xlimit - 1)->ch_is_idx) && c->ch_or_idx == BLANK_CHAR) xlimit--; - if (xlimit < line->xnum && line->gpu_cells[xlimit > 0 ? xlimit - 1 : xlimit].attrs.width == 2) xlimit++; - } + while (xlimit > 0 && !line->cpu_cells[xlimit - 1].ch_and_idx) xlimit--; return xlimit; } @@ -60,20 +57,6 @@ line_reset_cells(Line *line, index_type start, index_type num, GPUCell *gpu_cell memcpy(line->cpu_cells + start, cpu_cells + start, sizeof(CPUCell) * num); } -static inline void -left_shift_line(Line *line, index_type at, index_type num) { - for (index_type i = at; i < line->xnum - num; i++) { - COPY_CELL(line, i + num, line, i); - } - const CellAttrs empty = {.width=1}; - const CellAttrs zero = {{0}}; - if (at < line->xnum && line->gpu_cells[at].attrs.width != 1) { - line->cpu_cells[at].val = 0; - line->gpu_cells[at].attrs = BLANK_CHAR ? empty : zero; - clear_sprite_position(line->gpu_cells[at]); - } -} - static inline bool line_is_empty(const Line *line) { for (index_type i = 0; i < line->xnum; i++) { @@ -86,9 +69,7 @@ typedef Line*(get_line_func)(void *, int); void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch); void line_apply_cursor(Line *self, const Cursor *cursor, unsigned int at, unsigned int num, bool clear_char); char_type line_get_char(Line *self, index_type at); -void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, hyperlink_id_type); -void line_right_shift(Line *, unsigned int , unsigned int ); -bool line_add_combining_char(CPUCell *, GPUCell *, TextCache*, ListOfChars*, uint32_t , unsigned int ); +void line_set_char(Line *, unsigned int , uint32_t , Cursor *, hyperlink_id_type); index_type line_url_start_at(Line *self, index_type x); index_type line_url_end_at(Line *self, index_type x, bool, char_type, bool, bool, index_type); bool line_startswith_url_chars(Line*, bool); @@ -101,6 +82,7 @@ PyObject* line_as_unicode(Line *, bool); void linebuf_init_line(LineBuf *, index_type); void linebuf_init_cells(LineBuf *lb, index_type ynum, CPUCell **c, GPUCell **g); +CPUCell* linebuf_cpu_cells_for_line(LineBuf *lb, index_type idx); void linebuf_clear(LineBuf *, char_type ch); void linebuf_clear_lines(LineBuf *self, const Cursor *cursor, index_type start, index_type end); void linebuf_index(LineBuf* self, index_type top, index_type bottom); @@ -114,8 +96,8 @@ void linebuf_mark_line_dirty(LineBuf *self, index_type y); void linebuf_clear_attrs_and_dirty(LineBuf *self, index_type y); void linebuf_mark_line_clean(LineBuf *self, index_type y); void linebuf_set_line_has_image_placeholders(LineBuf *self, index_type y, bool val); -unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y); void linebuf_set_last_char_as_continuation(LineBuf *self, index_type y, bool continued); +CPUCell* linebuf_cpu_cell_at(LineBuf *self, index_type x, index_type y); bool linebuf_line_ends_with_continuation(LineBuf *self, index_type y); void linebuf_refresh_sprite_positions(LineBuf *self); void historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf*); diff --git a/kitty/rewrap.h b/kitty/rewrap.h index f367338e9..10c089f36 100644 --- a/kitty/rewrap.h +++ b/kitty/rewrap.h @@ -38,7 +38,7 @@ #endif #ifndef is_src_line_continued -#define is_src_line_continued() (src->line->gpu_cells[src->xnum-1].attrs.next_char_was_wrapped) +#define is_src_line_continued() (src->line->cpu_cells[src->xnum-1].next_char_was_wrapped) #endif static inline void @@ -69,7 +69,7 @@ rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf // Trim trailing blanks since there is a hard line break at the end of this line while(src_x_limit && src->line->cpu_cells[src_x_limit - 1].ch_or_idx == BLANK_CHAR && !src->line->cpu_cells[src_x_limit - 1].ch_is_idx) src_x_limit--; } else { - src->line->gpu_cells[src->xnum-1].attrs.next_char_was_wrapped = false; + src->line->cpu_cells[src->xnum-1].next_char_was_wrapped = false; } for (TrackCursor *t = track; !t->is_sentinel; t++) { if (t->is_tracked_line && t->x >= src_x_limit) t->x = MAX(1u, src_x_limit) - 1; diff --git a/kitty/screen.c b/kitty/screen.c index 202f3e9a0..6a7522456 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -594,28 +594,193 @@ init_text_loop_line(Screen *self, text_loop_state *s) { s->image_placeholder_marked = false; } +static void +zero_cells(text_loop_state *s, CPUCell *c, GPUCell *g) { *c = s->cc; *g = s->g; } + +typedef Line*(linefunc_t)(Screen*, int); + +static Line* +init_line(Screen *self, index_type y) { + linebuf_init_line(self->linebuf, y); + if (y == 0 && self->linebuf == self->main_linebuf) { + if (history_buf_endswith_wrap(self->historybuf)) self->linebuf->line->attrs.is_continued = true; + } + return self->linebuf->line; +} + +static Line* +visual_line_(Screen *self, int y_) { + index_type y = MAX(0, y_); + if (self->scrolled_by) { + if (y < self->scrolled_by) { + historybuf_init_line(self->historybuf, self->scrolled_by - 1 - y, self->historybuf->line); + return self->historybuf->line; + } + y -= self->scrolled_by; + } + return init_line(self, y); +} + +static Line* +range_line_(Screen *self, int y) { + if (y < 0) { + historybuf_init_line(self->historybuf, -(y + 1), self->historybuf->line); + return self->historybuf->line; + } + return init_line(self, y); +} + +static Line* +checked_range_line(Screen *self, int y) { + if ( + (y < 0 && -(y + 1) >= (int)self->historybuf->count) || y >= (int)self->lines + ) return NULL; + return range_line_(self, y); +} + + +static void +nuke_multicell_char_at(Screen *self, index_type x_, index_type y_, bool replace_with_spaces) { + CPUCell *cp; GPUCell *gp; + linebuf_init_cells(self->linebuf, y_, &cp, &gp); + MultiCellData mcd = cell_multicell_data(cp + x_, self->text_cache); + index_type y_max_limit = MIN(self->lines, y_ + mcd.scale); + while (cp[x_].x && x_ > 0) x_--; + index_type x_limit = MIN(self->columns, x_ + mcd_x_limit(mcd)); + char_type ch = replace_with_spaces ? ' ' : 0; +#define nuke_in_line for (index_type x = x_; x < x_limit; x++) { cell_set_char(cp + x, ch); cp[x].is_multicell = false; clear_sprite_position(gp[x]); } + for (index_type y = y_; y < y_max_limit; y++) { + linebuf_init_cells(self->linebuf, y, &cp, &gp); + nuke_in_line; linebuf_mark_line_dirty(self->linebuf, y); + } + int y_min_limit = -1; + if (self->linebuf == self->main_linebuf) y_min_limit = -(self->historybuf->count + 1); + for (int y = y_ - 1; y > y_min_limit; y--) { + Line *line = range_line_(self, y); cp = line->cpu_cells; gp = line->gpu_cells; + nuke_in_line; if (y > -1) linebuf_mark_line_dirty(self->linebuf, y); + } + self->is_dirty = true; +#undef nuke_in_line +} + +static void +nuke_multiline_char_intersecting_with(Screen *self, index_type x_start, index_type x_limit, index_type y_start, index_type y_limit, bool replace_with_spaces) { + for (index_type y = y_start; y < y_limit; y++) { + CPUCell *cp; GPUCell *gp; + linebuf_init_cells(self->linebuf, y, &cp, &gp); + for (index_type x = x_start; x < x_limit; x++) { + if (cp[x].is_multicell && cell_multicell_data(cp + x, self->text_cache).scale > 1) nuke_multicell_char_at(self, x, y, replace_with_spaces); + } + } +} + +static void +nuke_multicell_char_intersecting_with(Screen *self, index_type x_start, index_type x_limit, index_type y_start, index_type y_limit, bool replace_with_spaces) { + for (index_type y = y_start; y < y_limit; y++) { + CPUCell *cp; GPUCell *gp; + linebuf_init_cells(self->linebuf, y, &cp, &gp); + for (index_type x = x_start; x < x_limit; x++) { + if (cp[x].is_multicell) nuke_multicell_char_at(self, x, y, replace_with_spaces); + } + } +} + + +static void +nuke_split_multicell_char_at_left_boundary(Screen *self, index_type x, index_type y, bool replace_with_spaces) { + CPUCell *cp = linebuf_cpu_cells_for_line(self->linebuf, y); + if (cp[x].is_multicell && cp[x].x) { + nuke_multicell_char_at(self, x, y, replace_with_spaces); // remove split multicell char at left edge + } +} + +static void +nuke_split_multicell_char_at_right_boundary(Screen *self, index_type x, index_type y, bool replace_with_spaces) { + CPUCell *cp = linebuf_cpu_cells_for_line(self->linebuf, y); + CPUCell *c = cp + x; + if (c->is_multicell) { + MultiCellData mcd = cell_multicell_data(c, self->text_cache); + unsigned max_x = mcd_x_limit(mcd) - 1; + if (c->x < max_x) { + nuke_multicell_char_at(self, x, y, replace_with_spaces); + } + } +} + +static void +insert_characters(Screen *self, index_type at, index_type num, index_type y, bool replace_with_spaces) { + // insert num chars at x=at setting them to the value of the num chars at [at, at + num) + // multiline chars at x >= at are deleted and multicell chars split at x=at + // and x=at + num - 1 are deleted + nuke_multiline_char_intersecting_with(self, at, self->columns, y, y + 1, replace_with_spaces); + nuke_split_multicell_char_at_left_boundary(self, at, y, replace_with_spaces); + nuke_split_multicell_char_at_right_boundary(self, at + num - 1, y, replace_with_spaces); + CPUCell *cp; GPUCell *gp; + linebuf_init_cells(self->linebuf, y, &cp, &gp); + // right shift + for(index_type i = self->columns - 1; i >= at + num; i--) { + cp[i] = cp[i - num]; gp[i] = gp[i - num]; + } + nuke_split_multicell_char_at_right_boundary(self, self->columns - 1, y, replace_with_spaces); +} + +static bool +halve_multicell_width(Screen *self, index_type x_, index_type y_) { + CPUCell *cp; GPUCell *gp; + linebuf_init_cells(self->linebuf, y_, &cp, &gp); + MultiCellData mcd = cell_multicell_data(cp + x_, self->text_cache); + int y_min_limit = -1; + if (self->linebuf == self->main_linebuf) y_min_limit = -(self->historybuf->count + 1); + int expected_y_min_limit = ((int)y_) - mcd.scale; + if (expected_y_min_limit < y_min_limit) return false; + y_min_limit = expected_y_min_limit; + MultiCellData new_mcd = mcd; new_mcd.width = mcd.width / 2; + while (cp[x_].x && x_ > 0) x_--; + index_type x_limit = MIN(self->columns, x_ + mcd_x_limit(mcd)); + index_type half_x_limit = x_limit / 2; + int y_max_limit = MIN(self->lines, y_ + mcd.scale); + RAII_ListOfChars(lc); + text_in_cell(cp + x_, self->text_cache, &lc); + lc.chars[lc.count++] = new_mcd.val; + cell_set_chars(cp + x_, self->text_cache, &lc); + char_type idx = cp[x_].ch_or_idx; + for (int y = y_min_limit + 1; y < y_max_limit; y++) { + Line *line = range_line_(self, y); cp = line->cpu_cells; gp = line->gpu_cells; + for (index_type x = 0; x < half_x_limit; x++) cp[x].ch_or_idx = idx; + for (index_type x = half_x_limit; x < x_limit; x++) { + cp[x].val = 0; clear_sprite_position(gp[x]); + } + if (y > -1) linebuf_mark_line_dirty(self->linebuf, y); + } + self->is_dirty = true; + return true; +} static void move_widened_char(Screen *self, text_loop_state *s, CPUCell* cpu_cell, GPUCell *gpu_cell, index_type xpos, index_type ypos) { self->cursor->x = xpos; self->cursor->y = ypos; CPUCell src_cpu = *cpu_cell, *dest_cpu; GPUCell src_gpu = *gpu_cell, *dest_gpu; - *cpu_cell = s->cc; *gpu_cell = s->g; + zero_cells(s, cpu_cell, gpu_cell); + index_type dest_x = xpos, dest_y = ypos; if (self->modes.mDECAWM) { // overflow goes onto next line continue_to_next_line(self); init_text_loop_line(self, s); + dest_y = self->cursor->y; dest_x = self->cursor->x; dest_cpu = s->cp; dest_gpu = s->gp; self->cursor->x = MIN(2u, self->columns); } else { + dest_x--; dest_cpu = cpu_cell - 1; dest_gpu = gpu_cell - 1; self->cursor->x = self->columns; } + if (dest_cpu->is_multicell) nuke_multicell_char_at(self, dest_x, dest_y, false); *dest_cpu = src_cpu; *dest_gpu = src_gpu; - *(dest_cpu + 1) = s->cc; *(dest_gpu + 1) = s->g; - memcpy(dest_gpu + 1, &s->g, sizeof(s->g)); - dest_gpu[1].attrs.width = 0; + CPUCell *second_cpu = dest_cpu + 1; GPUCell *second_gpu = dest_gpu + 1; + *second_cpu = *dest_cpu; *second_gpu = *dest_gpu; + second_cpu->x = 1; } void @@ -633,6 +798,38 @@ static bool is_flag_pair(char_type a, char_type b) { return is_flag_codepoint(a) && is_flag_codepoint(b); } +static bool +add_combining_char(Screen *self, char_type ch, index_type x, index_type y) { + CPUCell *cpu_cells = linebuf_cpu_cells_for_line(self->linebuf, y); + CPUCell *cell = cpu_cells + x; + if (!cell_has_text(cell)) return false; // don't allow adding combining chars to a null cell + if (cell->is_multicell) { + while (cell->x && x) cell = cpu_cells + --x; + if (cell->y) return false; // not the top left cell + text_in_cell(cell, self->text_cache, self->lc); + MultiCellData mcd = {.val=self->lc->chars[self->lc->count]}; + ensure_space_for_chars(self->lc, self->lc->count + 2); + self->lc->chars[self->lc->count++] = ch; + self->lc->chars[self->lc->count++] = mcd.val; + const char_type idx = tc_get_or_insert_chars(self->text_cache, self->lc); + self->lc->count--; + index_type x_limit = MIN(x + mcd_x_limit(mcd), self->columns); + for (index_type v = y; v < y + mcd.scale; v++) { + cpu_cells = linebuf_cpu_cells_for_line(self->linebuf, v); + for (index_type h = x; h < x_limit; h++) cpu_cells[h].ch_or_idx = idx; + linebuf_mark_line_dirty(self->linebuf, v); + } + } else { + text_in_cell(cell, self->text_cache, self->lc); + ensure_space_for_chars(self->lc, self->lc->count + 1); + self->lc->chars[self->lc->count++] = ch; + cell->ch_or_idx = tc_get_or_insert_chars(self->text_cache, self->lc); + cell->ch_is_idx = true; + } + return true; +} + + static bool draw_second_flag_codepoint(Screen *self, char_type ch) { index_type xpos = 0, ypos = 0; @@ -644,21 +841,14 @@ draw_second_flag_codepoint(Screen *self, char_type ch) { xpos = self->columns - 2; } else return false; - CPUCell *cp; GPUCell *gp; - linebuf_init_cells(self->linebuf, ypos, &cp, &gp); + CPUCell *cp = linebuf_cpu_cells_for_line(self->linebuf, ypos); CPUCell *cell = cp + xpos; text_in_cell(cell, self->text_cache, self->lc); if (self->lc->count != 1 || !is_flag_pair(self->lc->chars[0], ch)) return false; - line_add_combining_char(cp, gp, self->text_cache, self->lc, ch, xpos); + add_combining_char(self, ch, xpos, ypos); return true; } -static void -zero_cells(text_loop_state *s, CPUCell *c, GPUCell *g) { - *c = s->cc; - *g = s->g; -} - static void draw_combining_char(Screen *self, text_loop_state *s, char_type ch) { bool has_prev_char = false; @@ -675,29 +865,44 @@ draw_combining_char(Screen *self, text_loop_state *s, char_type ch) { if (has_prev_char) { CPUCell *cp; GPUCell *gp; linebuf_init_cells(self->linebuf, ypos, &cp, &gp); - if (xpos > 0 && gp[xpos].attrs.width == 0 && gp[xpos-1].attrs.width == 2) xpos--; - bool added = line_add_combining_char(cp, gp, self->text_cache, self->lc, ch, xpos); - unsigned base_pos = self->lc->count - (added ? 2 : 1); + while (xpos > 0 && cp[xpos].is_multicell && cp[xpos].x) xpos--; + if (!add_combining_char(self, ch, xpos, ypos)) return; + unsigned base_pos = self->lc->count - 2; if (ch == VS16) { // emoji presentation variation marker makes default text presentation emoji (narrow emoji) into wide emoji CPUCell *cpu_cell = cp + xpos; GPUCell *gpu_cell = gp + xpos; - if (gpu_cell->attrs.width != 2 && added && self->lc->chars[base_pos + 1] == VS16 && is_emoji_presentation_base(self->lc->chars[base_pos])) { - gpu_cell->attrs.width = 2; - if (xpos + 1 < self->columns) { - zero_cells(s, cp + xpos + 1, gp + xpos + 1); - gp[xpos + 1].attrs.width = 0; - self->cursor->x++; - } else move_widened_char(self, s, cpu_cell, gpu_cell, xpos, ypos); + if (self->lc->chars[base_pos + 1] == VS16 && (!cpu_cell->is_multicell || cell_multicell_data(cpu_cell, self->text_cache).width < 2) && is_emoji_presentation_base(self->lc->chars[base_pos])) { + bool already_multicell = cpu_cell->is_multicell; + if (already_multicell) { + MultiCellData mcd = {.val=self->lc->chars[self->lc->count]}; + mcd.width *= 2; + self->lc->chars[self->lc->count++] = mcd.val; + } else { + ensure_space_for_chars(self->lc, self->lc->count + 1); + MultiCellData mcd = {.width=2, .scale=1}; + self->lc->chars[self->lc->count++] = mcd.val; + cpu_cell->is_multicell = true; + } + cpu_cell->ch_or_idx = tc_get_or_insert_chars(self->text_cache, self->lc); + if (!already_multicell) { + if (xpos + 1 < self->columns) { + CPUCell *second = cp + xpos + 1; + if (second->is_multicell) nuke_multicell_char_at(self, xpos, ypos, false); + zero_cells(s, second, gp + xpos + 1); + self->cursor->x++; + second->is_multicell = true; second->ch_or_idx = cpu_cell->ch_or_idx; second->ch_is_idx = true; second->x = 1; + } else move_widened_char(self, s, cpu_cell, gpu_cell, xpos, ypos); + } } } else if (ch == VS15) { - CPUCell *cpu_cell = cp + xpos; - GPUCell *gpu_cell = gp + xpos; - if (gpu_cell->attrs.width == 0 && !cell_has_text(cpu_cell) && xpos > 0) { - cpu_cell--; gpu_cell--; - } - if (gpu_cell->attrs.width == 2 && added && self->lc->chars[base_pos + 1] == VS15 && is_emoji_presentation_base(self->lc->chars[base_pos])) { - gpu_cell->attrs.width = 1; - self->cursor->x--; + const CPUCell *cpu_cell = cp + xpos; + if (self->lc->chars[base_pos + 1] == VS15 && cpu_cell->is_multicell && is_emoji_presentation_base(self->lc->chars[base_pos])) { + MultiCellData mcd = {.val=self->lc->chars[self->lc->count]}; + if (mcd.width == 2) { + if (halve_multicell_width(self, xpos, ypos)) { + self->cursor->x -= (mcd.scale * mcd.width / 2); + } + } } } } @@ -716,15 +921,13 @@ screen_on_input(Screen *self) { } static bool -cursor_on_wide_char_trailer(Screen *self, text_loop_state *s) { - return self->cursor->x > 0 && s->gp[self->cursor->x - 1].attrs.width == 2; +ts_cursor_on_multicell(Screen *self, text_loop_state *s) { + return s->cp[self->cursor->x].is_multicell; } static void -move_cursor_off_wide_char_trailer(Screen *self, text_loop_state *s) { - zero_cells(s, s->cp + self->cursor->x - 1, s->gp + self->cursor->x - 1); - cell_set_char(&s->cp[self->cursor->x-1], ' '); - zero_cells(s, s->cp + self->cursor->x, s->gp + self->cursor->x); +replace_multicell_char_under_cursor_with_spaces(Screen *self) { + nuke_multicell_char_at(self, self->cursor->x, self->cursor->y, true); } static void @@ -765,7 +968,7 @@ static void draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_state *s) { init_text_loop_line(self, s); const uint32_t first_char = map_char(self, chars[0]); - if (cursor_on_wide_char_trailer(self, s) && ' ' <= first_char && first_char != DEL && !is_combining_char(first_char)) move_cursor_off_wide_char_trailer(self, s); + if (ts_cursor_on_multicell(self, s) && ' ' <= first_char && first_char != DEL && !is_combining_char(first_char)) replace_multicell_char_under_cursor_with_spaces(self); for (size_t i = 0; i < num_chars; i++) { uint32_t ch = map_char(self, chars[i]); if (ch < ' ') { @@ -782,7 +985,7 @@ draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_ init_text_loop_line(self, s); } else if (self->columns > 0){ self->cursor->x = self->columns - 1; - if (cursor_on_wide_char_trailer(self, s)) move_cursor_off_wide_char_trailer(self, s); + if (ts_cursor_on_multicell(self, s)) replace_multicell_char_under_cursor_with_spaces(self); screen_tab(self); } } else screen_tab(self); @@ -826,22 +1029,29 @@ draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_ init_text_loop_line(self, s); } else { self->cursor->x = self->columns - char_width; - if (cursor_on_wide_char_trailer(self, s)) move_cursor_off_wide_char_trailer(self, s); + if (ts_cursor_on_multicell(self, s)) replace_multicell_char_under_cursor_with_spaces(self); } } - if (self->modes.mIRM) line_right_shift(self->linebuf->line, self->cursor->x, char_width); + if (self->modes.mIRM) insert_characters(self, self->cursor->x, char_width, self->cursor->y, true); if (UNLIKELY(!s->image_placeholder_marked && ch == IMAGE_PLACEHOLDER_CHAR)) { linebuf_set_line_has_image_placeholders(self->linebuf, self->cursor->y, true); s->image_placeholder_marked = true; } - zero_cells(s, s->cp + self->cursor->x, s->gp + self->cursor->x); - cell_set_char(&s->cp[self->cursor->x], ch); - self->cursor->x++; + CPUCell *fc = s->cp + self->cursor->x; + zero_cells(s, fc, s->gp + self->cursor->x); if (char_width == 2) { - s->gp[self->cursor->x-1].attrs.width = 2; - zero_cells(s, s->cp + self->cursor->x, s->gp + self->cursor->x); - s->gp[self->cursor->x].attrs.width = 0; - self->cursor->x++; + MultiCellData mcd = {.width = 2, .scale = 1}; + RAII_ListOfChars(lc); + lc.chars[lc.count++] = ch; + lc.chars[lc.count++] = mcd.val; + cell_set_chars(fc, self->text_cache, &lc); + fc->x = 0; fc->y = 0; fc->is_multicell = true; + *(fc + 1) = *fc; (fc + 1)->x = 1; + s->gp[self->cursor->x + 1] = s->gp[self->cursor->x]; + self->cursor->x += 2; + } else { + cell_set_char(fc, ch); self->cursor->x++; + fc->is_multicell = false; } } #undef init_line @@ -851,7 +1061,7 @@ static void draw_text(Screen *self, const uint32_t *chars, size_t num_chars) { self->is_dirty = true; const bool force_underline = OPT(underline_hyperlinks) == UNDERLINE_ALWAYS && self->active_hyperlink_id != 0; - CellAttrs attrs = cursor_to_attrs(self->cursor, 1); + CellAttrs attrs = cursor_to_attrs(self->cursor); if (force_underline) attrs.decoration = OPT(url_style); text_loop_state s={ .cc=(CPUCell){.hyperlink_id=self->active_hyperlink_id}, @@ -1432,15 +1642,6 @@ change_pointer_shape(Screen *self, PyObject *args) { Py_RETURN_NONE; } -static unsigned long -screen_current_char_width(Screen *self) { - unsigned long ans = 1; - if (self->cursor->x < self->columns - 1 && self->cursor->y < self->lines) { - ans = linebuf_char_width_at(self->linebuf, self->cursor->x, self->cursor->y); - } - return ans; -} - bool screen_is_cursor_visible(const Screen *self) { return self->paused_rendering.expires_at ? self->paused_rendering.cursor_visible : self->modes.mDECTCEM; @@ -1850,8 +2051,8 @@ screen_fake_move_cursor_to_position(Screen *self, index_type start_x, index_type x_limit = MIN(x_limit, self->columns); bool found_non_empty_cell = false; while (x < x_limit) { - unsigned int w = linebuf_char_width_at(self->linebuf, x, y); - if (w == 0) { + const CPUCell *c = linebuf_cpu_cell_at(self->linebuf, x, y); + if (!cell_has_text(c)) { // we only stop counting the cells in the line at an empty cell // if at least one non-empty cell is found. zsh uses empty cells // between the end of the text ad the right prompt. fish uses empty @@ -1861,7 +2062,10 @@ screen_fake_move_cursor_to_position(Screen *self, index_type start_x, index_type break; } found_non_empty_cell = true; - x += w; + if (c->is_multicell) { + MultiCellData mcd = cell_multicell_data(c, self->text_cache); + x += mcd_x_limit(mcd); + } else x++; count += 1; // zsh requires a single arrow press to move past dualwidth chars } if (!found_non_empty_cell) count++; // blank line @@ -2070,8 +2274,8 @@ screen_insert_characters(Screen *self, unsigned int count) { if (self->cursor->y <= bottom) { unsigned int x = self->cursor->x; unsigned int num = MIN(self->columns - x, count); + insert_characters(self, x, num, self->cursor->y, false); linebuf_init_line(self->linebuf, self->cursor->y); - line_right_shift(self->linebuf->line, x, num); line_apply_cursor(self->linebuf->line, self->cursor, x, num, true); linebuf_mark_line_dirty(self->linebuf, self->cursor->y); self->is_dirty = true; @@ -2090,6 +2294,23 @@ screen_repeat_character(Screen *self, unsigned int count) { } } +static void +remove_characters(Screen *self, index_type at, index_type num, index_type y, bool replace_with_spaces) { + // delete num chars at x=at setting them to the value of the num chars at [at + num, at + num + num) + // multiline chars at x >= at are deleted and multicell chars split at x=at + // and x=at + num - 1 are deleted + nuke_multiline_char_intersecting_with(self, at, self->columns, y, y + 1, replace_with_spaces); + nuke_split_multicell_char_at_left_boundary(self, at, y, replace_with_spaces); + nuke_split_multicell_char_at_right_boundary(self, at + num - 1, y, replace_with_spaces); + CPUCell *cp; GPUCell *gp; + linebuf_init_cells(self->linebuf, y, &cp, &gp); + // left shift + for (index_type i = at; i < self->columns - num; i++) { + cp[i] = cp[i+num]; gp[i] = gp[i+num]; + } + nuke_split_multicell_char_at_right_boundary(self, at + num - 1, y, replace_with_spaces); +} + void screen_delete_characters(Screen *self, unsigned int count) { // Delete characters, later characters are moved left @@ -2098,8 +2319,8 @@ screen_delete_characters(Screen *self, unsigned int count) { if (self->cursor->y <= bottom) { unsigned int x = self->cursor->x; unsigned int num = MIN(self->columns - x, count); + remove_characters(self, x, num, self->cursor->y, false); linebuf_init_line(self->linebuf, self->cursor->y); - left_shift_line(self->linebuf->line, x, num); line_apply_cursor(self->linebuf->line, self->cursor, self->columns - num, num, true); linebuf_mark_line_dirty(self->linebuf, self->cursor->y); self->is_dirty = true; @@ -2113,6 +2334,7 @@ screen_erase_characters(Screen *self, unsigned int count) { if (count == 0) count = 1; unsigned int x = self->cursor->x; unsigned int num = MIN(self->columns - x, count); + nuke_multicell_char_intersecting_with(self, x, x + num, self->cursor->y, self->cursor->y + 1, true); linebuf_init_line(self->linebuf, self->cursor->y); line_apply_cursor(self->linebuf->line, self->cursor, x, num, true); linebuf_mark_line_dirty(self->linebuf, self->cursor->y); @@ -2864,47 +3086,6 @@ num_lines_between_selection_boundaries(const SelectionBoundary *a, const Selecti return before->y - after->y; } -typedef Line*(linefunc_t)(Screen*, int); - -static Line* -init_line(Screen *self, index_type y) { - linebuf_init_line(self->linebuf, y); - if (y == 0 && self->linebuf == self->main_linebuf) { - if (history_buf_endswith_wrap(self->historybuf)) self->linebuf->line->attrs.is_continued = true; - } - return self->linebuf->line; -} - -static Line* -visual_line_(Screen *self, int y_) { - index_type y = MAX(0, y_); - if (self->scrolled_by) { - if (y < self->scrolled_by) { - historybuf_init_line(self->historybuf, self->scrolled_by - 1 - y, self->historybuf->line); - return self->historybuf->line; - } - y -= self->scrolled_by; - } - return init_line(self, y); -} - -static Line* -range_line_(Screen *self, int y) { - if (y < 0) { - historybuf_init_line(self->historybuf, -(y + 1), self->historybuf->line); - return self->historybuf->line; - } - return init_line(self, y); -} - -static Line* -checked_range_line(Screen *self, int y) { - if ( - (y < 0 && -(y + 1) >= (int)self->historybuf->count) || y >= (int)self->lines - ) return NULL; - return range_line_(self, y); -} - static bool selection_is_left_to_right(const Selection *self) { return self->input_start.x < self->input_current.x || (self->input_start.x == self->input_current.x && self->input_start.in_left_half_of_cell); @@ -3126,7 +3307,7 @@ ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st } } if (line_as_ansi(line, &output, &prev_cell, xr.x, x_limit, prefix_char)) has_escape_codes = true; - need_newline = insert_newlines && !line->gpu_cells[line->xnum-1].attrs.next_char_was_wrapped; + need_newline = insert_newlines && !line->cpu_cells[line->xnum-1].next_char_was_wrapped; PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len); if (!t) return NULL; PyTuple_SET_ITEM(ans, i, t); @@ -3231,7 +3412,7 @@ extend_url(Screen *screen, Line *line, index_type *x, index_type *y, char_type s index_type orig_y = *y; while(count++ < 10) { bool in_hostname = last_hostname_char_pos >= line->xnum; - has_newline = !line->gpu_cells[line->xnum-1].attrs.next_char_was_wrapped; + has_newline = !line->cpu_cells[line->xnum-1].next_char_was_wrapped; if (*x != line->xnum - 1 || (!newlines_allowed && has_newline)) break; bool next_line_starts_with_url_chars = false; line = screen_visual_line(screen, *y + 2); @@ -3398,10 +3579,13 @@ screen_draw_overlay_line(Screen *self) { self->cursor->y = self->overlay_line.ynum; self->overlay_line.xnum = 0; if (xstart > 0) { - // When the cursor is on the second cell of a full-width character for whatever reason, - // make sure the first character in the overlay is visible. - GPUCell *g = self->linebuf->line->gpu_cells + (xstart - 1); - if (g->attrs.width > 1) line_set_char(self->linebuf->line, xstart - 1, 0, 0, NULL, 0); + // remove any multicell characters temporarily that intersect the left boundary, + // the characters are not actually removed, just deleted on this line + CPUCell *c = self->linebuf->line->cpu_cells + xstart; + while (c->is_multicell && c->x && c < self->linebuf->line->cpu_cells + self->columns) { + c->is_multicell = false; c->ch_or_idx = ' '; c->ch_is_idx = false; + c++; + } } index_type before; const int kind = PyUnicode_KIND(self->overlay_line.overlay_text); @@ -3420,9 +3604,18 @@ screen_draw_overlay_line(Screen *self) { len = len > columns_exceeded ? len - columns_exceeded : 0; columns_exceeded = 0; if (len > 0) { - // When the last character is full width and only half moved out, make sure the next character is visible. - GPUCell *g = self->linebuf->line->gpu_cells + (len - 1); - if (g->attrs.width > 1) line_set_char(self->linebuf->line, len - 1, 0, 0, NULL, 0); + // When the last character is a split multicell, make sure the next character is visible. + CPUCell *c = self->linebuf->line->cpu_cells + len - 1; + if (c->is_multicell) { + MultiCellData mcd = cell_multicell_data(c, self->text_cache); + if (c->x < mcd_x_limit(mcd) - 1) { + do { + c->is_multicell = false; c->ch_is_idx = false; c->ch_or_idx = ' '; + if (!c->x) break; + c--; + } while(c->is_multicell && c >= self->linebuf->line->cpu_cells); + } + } } } self->cursor->x = len; @@ -4475,7 +4668,15 @@ reload_all_gpu_data(Screen *self, PyObject *a UNUSED) { static PyObject* current_char_width(Screen *self, PyObject *a UNUSED) { #define current_char_width_doc "The width of the character under the cursor" - return PyLong_FromUnsignedLong(screen_current_char_width(self)); + unsigned long ans = 1; + if (self->cursor->x < self->columns && self->cursor->y < self->lines) { + const CPUCell *c = linebuf_cpu_cells_for_line(self->linebuf, self->cursor->y) + self->cursor->x; + if (c->is_multicell) { + if (c->x || c->y) ans = 0; + else ans = cell_multicell_data(c, self->text_cache).width; + } + } + return PyLong_FromUnsignedLong(ans); } static PyObject* @@ -4591,7 +4792,7 @@ scroll_to_next_mark(Screen *self, PyObject *args) { static PyObject* marked_cells(Screen *self, PyObject *o UNUSED) { - PyObject *ans = PyList_New(0); + RAII_PyObject(ans, PyList_New(0)); if (!ans) return ans; for (index_type y = 0; y < self->lines; y++) { linebuf_init_line(self->linebuf, y); @@ -4599,14 +4800,13 @@ marked_cells(Screen *self, PyObject *o UNUSED) { GPUCell *gpu_cell = self->linebuf->line->gpu_cells + x; const unsigned int mark = gpu_cell->attrs.mark; if (mark) { - PyObject *t = Py_BuildValue("III", x, y, mark); - if (!t) { Py_DECREF(ans); return NULL; } - if (PyList_Append(ans, t) != 0) { Py_DECREF(t); Py_DECREF(ans); return NULL; } - Py_DECREF(t); + RAII_PyObject(t, Py_BuildValue("III", x, y, mark)); + if (!t) { return NULL; } + if (PyList_Append(ans, t) != 0) return NULL; } } } - return ans; + return Py_NewRef(ans); } static PyObject* @@ -4671,7 +4871,6 @@ WRAP2(cursor_position, 1, 1) #define COUNT_WRAP(name) WRAP1(name, 1) COUNT_WRAP(insert_lines) COUNT_WRAP(delete_lines) -COUNT_WRAP(insert_characters) COUNT_WRAP(delete_characters) COUNT_WRAP(erase_characters) COUNT_WRAP(cursor_up1) @@ -4679,6 +4878,14 @@ COUNT_WRAP(cursor_down) COUNT_WRAP(cursor_down1) COUNT_WRAP(cursor_forward) +static PyObject* +py_insert_characters(Screen *self, PyObject *count_) { + if (!PyLong_Check(count_)) { PyErr_SetString(PyExc_TypeError, "count must be an integer"); return NULL; } + unsigned long count = PyLong_AsUnsignedLong(count_); + screen_insert_characters(self, count); + Py_RETURN_NONE; +} + static PyObject* screen_is_emoji_presentation_base(PyObject UNUSED *self, PyObject *code_) { unsigned long code = PyLong_AsUnsignedLong(code_); @@ -4852,7 +5059,7 @@ static PyMethodDef methods[] = { METHOD(current_char_width, METH_NOARGS) MND(insert_lines, METH_VARARGS) MND(delete_lines, METH_VARARGS) - MND(insert_characters, METH_VARARGS) + {"insert_characters", (PyCFunction)py_insert_characters, METH_O, ""}, MND(delete_characters, METH_VARARGS) MND(erase_characters, METH_VARARGS) MND(current_pointer_shape, METH_NOARGS) diff --git a/kitty/shaders.c b/kitty/shaders.c index b61f59d33..f474752b5 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -372,8 +372,14 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, c screen->last_rendered.cursor_bg = rd->cursor_bg; } else rd->cursor_x = screen->columns, rd->cursor_y = screen->lines; rd->cursor_w = rd->cursor_x; - if ((rd->cursor_fg_sprite_idx == BLOCK_IDX || rd->cursor_fg_sprite_idx == UNDERLINE_IDX) && line_for_cursor && line_for_cursor->gpu_cells[cursor->x].attrs.width > 1) { - rd->cursor_w += 1; + const CPUCell *cursor_cell; + if ( + (rd->cursor_fg_sprite_idx == BLOCK_IDX || rd->cursor_fg_sprite_idx == UNDERLINE_IDX) && + line_for_cursor && (cursor_cell = line_for_cursor->cpu_cells + cursor->x)->is_multicell && + cursor_cell->x == 0 + ) { + MultiCellData mcd = cell_multicell_data(cursor_cell, screen->text_cache); + rd->cursor_w = mcd.width * mcd.scale; } rd->xnum = screen->columns; rd->ynum = screen->lines; diff --git a/kitty/text-cache.c b/kitty/text-cache.c index f0cbcbd13..6354e0c00 100644 --- a/kitty/text-cache.c +++ b/kitty/text-cache.c @@ -75,6 +75,12 @@ tc_first_char_at_index(const TextCache *self, char_type idx) { return 0; } +char_type +tc_last_char_at_index(const TextCache *self, char_type idx) { + if (self->array.count > idx) return self->array.items[idx].chars[self->array.items[idx].count-1]; + return 0; +} + void tc_chars_at_index(const TextCache *self, char_type idx, ListOfChars *ans) { diff --git a/kitty/text-cache.h b/kitty/text-cache.h index 87098aea1..f4e245299 100644 --- a/kitty/text-cache.h +++ b/kitty/text-cache.h @@ -12,6 +12,7 @@ typedef struct ListOfChars { char_type *chars; size_t count, capacity; + bool is_multicell, is_topleft; } ListOfChars; #define LIST_OF_CHARS_STACK_SIZE 4 @@ -53,5 +54,6 @@ void tc_chars_at_index(const TextCache *self, char_type idx, ListOfChars *ans); unsigned tc_chars_at_index_ansi(const TextCache *self, char_type idx, ANSIBuf *output); char_type tc_get_or_insert_chars(TextCache *self, const ListOfChars *chars); char_type tc_first_char_at_index(const TextCache *self, char_type idx); +char_type tc_last_char_at_index(const TextCache *self, char_type idx); bool tc_chars_at_index_without_alloc(const TextCache *self, char_type idx, ListOfChars *ans); unsigned tc_num_codepoints(const TextCache *self, char_type idx); diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index ac834b771..df4ad22e7 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -240,19 +240,6 @@ class TestDataTypes(BaseTest): l3 = lb.line(0) l3.set_text(t, 0, len(t), C()) self.ae(t, str(l3)) - l3.right_shift(4, 2) - self.ae('0123454567', str(l3)) - l3.set_text(t, 0, len(t), C()) - l3.right_shift(0, 0) - self.ae(t, str(l3)) - l3.right_shift(0, 1) - self.ae(str(l3), '0' + t[:-1]) - l3.set_text(t, 0, len(t), C()) - l3.left_shift(0, 2) - self.ae(str(l3), t[2:] + '89') - l3.set_text(t, 0, len(t), C()) - l3.left_shift(7, 3) - self.ae(str(l3), t) l3.set_text(t, 0, len(t), C()) q = C()