From 383e1f8f57985d783ea1251a3683b1f48adb41ef Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 22 Nov 2024 12:36:11 +0530 Subject: [PATCH] Work on scaled rendering for box drawing chars --- gen/apc_parsers.py | 1 + kitty/fonts.c | 204 ++++++++++++++++++++++---------- kitty/glyph-cache.c | 6 +- kitty/glyph-cache.h | 2 +- kitty/line.h | 8 +- kitty/parse-multicell-command.h | 9 +- kitty/screen.c | 2 +- kitty_tests/__init__.py | 7 +- kitty_tests/fonts.py | 26 +++- kitty_tests/multicell.py | 10 +- 10 files changed, 196 insertions(+), 79 deletions(-) diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 3ddf5fb19..327fd08c3 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -317,6 +317,7 @@ def parsers() -> None: 'w': ('width', 'uint'), 's': ('scale', 'uint'), 'f': ('subscale', 'uint'), + 'v': ('vertical_align', 'uint'), } text = generate( 'parse_multicell_code', 'screen_handle_multicell_command', 'multicell_command', keymap, 'MultiCellCommand', diff --git a/kitty/fonts.c b/kitty/fonts.c index 217b15e4d..f03be38f6 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -69,7 +69,8 @@ typedef struct RunFont { typedef struct Canvas { pixel *buf; - unsigned current_cells, alloced_cells; + unsigned current_cells, alloced_cells, alloced_scale, current_scale; + size_t size_in_bytes; } Canvas; #define NAME fallback_font_map_t @@ -98,15 +99,21 @@ static id_type font_group_id_counter = 0; static void initialize_font_group(FontGroup *fg); static void -ensure_canvas_can_fit(FontGroup *fg, unsigned cells) { - if (fg->canvas.alloced_cells < cells) { +ensure_canvas_can_fit(FontGroup *fg, unsigned cells, unsigned scale) { +#define cs(cells, scale) (sizeof(fg->canvas.buf[0]) * 3u * cells * fg->cell_width * fg->cell_height * scale * scale) + size_t size_in_bytes = cs(cells, scale); + if (size_in_bytes > fg->canvas.size_in_bytes) { free(fg->canvas.buf); - fg->canvas.alloced_cells = cells + 4; - fg->canvas.buf = malloc(sizeof(fg->canvas.buf[0]) * 3u * fg->canvas.alloced_cells * fg->cell_width * fg->cell_height); + fg->canvas.alloced_cells = MAX(8u, cells + 4u); + fg->canvas.alloced_scale = MAX(scale, 4u); + fg->canvas.size_in_bytes = cs(fg->canvas.alloced_cells, fg->canvas.alloced_scale); + fg->canvas.buf = malloc(fg->canvas.size_in_bytes); if (!fg->canvas.buf) fatal("Out of memory allocating canvas"); } fg->canvas.current_cells = cells; - if (fg->canvas.buf) memset(fg->canvas.buf, 0, sizeof(fg->canvas.buf[0]) * fg->canvas.current_cells * 3u * fg->cell_width * fg->cell_height); + fg->canvas.current_scale = scale; + if (fg->canvas.buf) memset(fg->canvas.buf, 0, cs(fg->canvas.current_cells, fg->canvas.alloced_scale)); +#undef cs } @@ -246,9 +253,12 @@ do_increment(FontGroup *fg, int *error) { static SpritePosition* -sprite_position_for(FontGroup *fg, Font *font, glyph_index *glyphs, unsigned glyph_count, uint8_t ligature_index, unsigned cell_count, int *error) { +sprite_position_for(FontGroup *fg, RunFont rf, glyph_index *glyphs, unsigned glyph_count, uint8_t ligature_index, unsigned cell_count, int *error) { bool created; - SpritePosition *s = find_or_create_sprite_position(font->sprite_position_hash_table, glyphs, glyph_count, ligature_index, cell_count, &created); + Font *font = fg->fonts + rf.font_idx; + SpritePosition *s = find_or_create_sprite_position( + font->sprite_position_hash_table, glyphs, glyph_count, ligature_index, cell_count, + rf.scale, rf.subscale, rf.multicell_y, rf.vertical_align, &created); if (!s) { *error = 1; return NULL; } if (created) { s->x = fg->sprite_tracker.x; s->y = fg->sprite_tracker.y; s->z = fg->sprite_tracker.z; @@ -462,7 +472,7 @@ calc_cell_metrics(FontGroup *fg) { sprite_tracker_set_layout(&fg->sprite_tracker, cell_width, cell_height); fg->cell_width = cell_width; fg->cell_height = cell_height; fg->baseline = baseline; fg->underline_position = underline_position; fg->underline_thickness = underline_thickness, fg->strikethrough_position = strikethrough_position, fg->strikethrough_thickness = strikethrough_thickness; - ensure_canvas_can_fit(fg, 8); + ensure_canvas_can_fit(fg, 8, 1); } static bool @@ -692,29 +702,118 @@ render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Regi } } +typedef struct GlyphRenderScratch { + SpritePosition* *sprite_positions; + glyph_index *glyphs; + size_t sz; + ListOfChars *lc; +} GlyphRenderScratch; +static GlyphRenderScratch global_glyph_render_scratch = {0}; + static void -render_box_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell, const TextCache *tc) { - int error = 0; - char_type ch = cell_first_char(cpu_cell, tc); - glyph_index glyph = box_glyph_id(ch); - SpritePosition *sp = sprite_position_for(fg, &fg->fonts[BOX_FONT], &glyph, 1, 0, 1, &error); - if (sp == NULL) { - sprite_map_set_error(error); PyErr_Print(); - set_sprite(gpu_cell, 0, 0, 0); - return; +ensure_glyph_render_scratch_space(size_t sz) { +#define a global_glyph_render_scratch + sz += 16; + if (a.sz < sz) { + free(a.glyphs); a.glyphs = malloc(sz * sizeof(a.glyphs[0])); if (!a.glyphs) fatal("Out of memory"); + free(a.sprite_positions); a.sprite_positions = malloc(sz * sizeof(SpritePosition*)); if (!a.sprite_positions) fatal("Out of memory"); + a.sz = sz; + if (!a.lc) { + a.lc = alloc_list_of_chars(); + if (!a.lc) fatal("Out of memory"); + } } - set_sprite(gpu_cell, sp->x, sp->y, sp->z); - if (sp->rendered) return; - sp->rendered = true; - sp->colored = false; - PyObject *ret = PyObject_CallFunction(box_drawing_function, "IIId", (unsigned int)ch, fg->cell_width, fg->cell_height, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0); +#undef a +} + +static void +scaled_cell_dimensions(RunFont rf, unsigned *width, unsigned *height) { + *width *= rf.scale; + *height *= rf.scale; + if (rf.subscale) { + double frac = 1. / (rf.subscale + 1); + *width = (unsigned)ceil(frac * *width); + *height = (unsigned)ceil(frac * *height); + } +} + +static pixel* +extract_cell_from_canvas(FontGroup *fg, unsigned int i, unsigned int num_cells) { + pixel *ans = fg->canvas.buf + (fg->canvas.size_in_bytes / sizeof(fg->canvas.buf[0]) - fg->cell_width * fg->cell_height); + pixel *dest = ans, *src = fg->canvas.buf + (i * fg->cell_width); + unsigned int stride = fg->cell_width * num_cells; + for (unsigned int r = 0; r < fg->cell_height; r++, dest += fg->cell_width, src += stride) memcpy(dest, src, fg->cell_width * sizeof(fg->canvas.buf[0])); + return ans; +} + +static void +calculate_regions_for_line(RunFont rf, unsigned cell_height, Region *src, Region *dest) { + unsigned src_height = src->bottom; + Region src_in_full_coords = *src; unsigned full_dest_height = cell_height * rf.scale; + if (rf.subscale) { + switch(rf.vertical_align) { + case 0: break; // top aligned no change + case 1: // bottom aligned + src_in_full_coords.top = full_dest_height - src_height; + src_in_full_coords.bottom = full_dest_height; + break; + case 2: // centered + src_in_full_coords.top = (full_dest_height - src_height) / 2; + src_in_full_coords.bottom = src_in_full_coords.top + src_height; + break; + } + } + Region dest_in_full_coords = {.top = rf.multicell_y * cell_height, .bottom = (rf.multicell_y + 1) * cell_height}; + unsigned intersetion_top = MAX(src_in_full_coords.top, dest_in_full_coords.top); + unsigned intersetion_bottom = MIN(src_in_full_coords.bottom, dest_in_full_coords.bottom); + unsigned src_top_delta = intersetion_top - src_in_full_coords.top, src_bottom_delta = src_in_full_coords.bottom - intersetion_bottom; + src->top += src_top_delta; src->bottom = src->bottom > src_bottom_delta ? src->bottom - src_bottom_delta : 0; + unsigned dest_top_delta = intersetion_top - dest_in_full_coords.top, dest_bottom_delta = dest_in_full_coords.bottom - intersetion_bottom; + dest->top = dest_top_delta; dest->bottom = cell_height > dest_bottom_delta ? cell_height - dest_bottom_delta : 0; +} + +static void +render_box_cell(FontGroup *fg, RunFont rf, CPUCell *cpu_cell, GPUCell *gpu_cell, const TextCache *tc) { + int error = 0; + // We need to render multicell chars for multicell_y > 0 cell_first_char() returns 0 for such cells + char_type ch = cpu_cell->is_multicell ? tc_first_char_at_index(tc, cpu_cell->ch_or_idx) : cell_first_char(cpu_cell, tc); + glyph_index glyph = box_glyph_id(ch); + ensure_glyph_render_scratch_space(rf.scale); + bool all_rendered = true; +#define sp global_glyph_render_scratch.sprite_positions + for (unsigned ligature_index = 0; ligature_index < rf.scale; ligature_index++) { + sp[ligature_index] = sprite_position_for(fg, rf, &glyph, 1, ligature_index, rf.scale, &error); + if (sp[ligature_index] == NULL) { + sprite_map_set_error(error); PyErr_Print(); + set_sprite(gpu_cell + ligature_index, 0, 0, 0); + return; + } + set_sprite(gpu_cell + ligature_index, sp[ligature_index]->x, sp[ligature_index]->y, sp[ligature_index]->z); + sp[ligature_index]->colored = false; + if (!sp[ligature_index]->rendered) { + all_rendered = false; sp[ligature_index]->rendered = true; + } + } + if (all_rendered) return; + unsigned width = fg->cell_width, height = fg->cell_height; + scaled_cell_dimensions(rf, &width, &height); + RAII_PyObject(ret, PyObject_CallFunction(box_drawing_function, "IIId", (unsigned int)ch, width, height, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0)); if (ret == NULL) { PyErr_Print(); return; } uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0)); - ensure_canvas_can_fit(fg, 1); - Region r = { .right = fg->cell_width, .bottom = fg->cell_height }; - render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width, 0xffffff); - current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp->x, sp->y, sp->z, fg->canvas.buf); - Py_DECREF(ret); + ensure_canvas_can_fit(fg, 2, rf.scale); + Region src = { .right = width, .bottom = height }, dest = src; + unsigned dest_stride = rf.scale * fg->cell_width, src_stride = width; + calculate_regions_for_line(rf, fg->cell_height, &src, &dest); + render_alpha_mask(alpha_mask, fg->canvas.buf, &src, &dest, src_stride, dest_stride, 0xffffff); + if (rf.scale == 1) { + current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp[0]->x, sp[0]->y, sp[0]->z, fg->canvas.buf); + } else { + for (unsigned i = 0; i < rf.scale; i++) { + pixel *b = extract_cell_from_canvas(fg, i, rf.scale); + current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp[i]->x, sp[i]->y, sp[i]->z, b); + } + } +#undef sp } static void @@ -743,22 +842,6 @@ set_cell_sprite(GPUCell *cell, const SpritePosition *sp) { if (sp->colored) cell->sprite_z |= 0x4000; } -static pixel* -extract_cell_from_canvas(FontGroup *fg, unsigned int i, unsigned int num_cells) { - pixel *ans = fg->canvas.buf + (fg->cell_width * fg->cell_height * (fg->canvas.current_cells - 1)), *dest = ans, *src = fg->canvas.buf + (i * fg->cell_width); - unsigned int stride = fg->cell_width * num_cells; - for (unsigned int r = 0; r < fg->cell_height; r++, dest += fg->cell_width, src += stride) memcpy(dest, src, fg->cell_width * sizeof(fg->canvas.buf[0])); - return ans; -} - -typedef struct GlyphRenderScratch { - SpritePosition* *sprite_positions; - glyph_index *glyphs; - size_t sz; - ListOfChars *lc; -} GlyphRenderScratch; -static GlyphRenderScratch global_glyph_render_scratch = {0}; - static void render_group(FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPUCell *cpu_cells, GPUCell *gpu_cells, hb_glyph_info_t *info, hb_glyph_position_t *positions, RunFont rf, glyph_index *glyphs, unsigned glyph_count, bool center_glyph, const TextCache *tc) { #define sp global_glyph_render_scratch.sprite_positions @@ -771,7 +854,7 @@ render_group(FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPU if (is_repeat_glyph) { sp[i] = sp[i-1]; } else { - sp[i] = sprite_position_for(fg, font, glyphs, glyph_count, ligature_index++, num_cells, &error); + sp[i] = sprite_position_for(fg, rf, glyphs, glyph_count, ligature_index++, num_cells, &error); } if (error != 0) { sprite_map_set_error(error); PyErr_Print(); return; } if (!sp[i]->rendered) all_rendered = false; @@ -781,7 +864,7 @@ render_group(FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPU return; } - ensure_canvas_can_fit(fg, num_cells + 1); + ensure_canvas_can_fit(fg, num_cells + 1, rf.scale); 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); @@ -1243,7 +1326,6 @@ split_run_at_offset(index_type cursor_offset, index_type *left, index_type *righ } } - static void render_groups(FontGroup *fg, RunFont rf, bool center_glyph, const TextCache *tc) { unsigned idx = 0; @@ -1253,14 +1335,7 @@ render_groups(FontGroup *fg, RunFont rf, bool center_glyph, const TextCache *tc) /* printf("Group: idx: %u num_cells: %u num_glyphs: %u first_glyph_idx: %u first_cell_idx: %u total_num_glyphs: %zu\n", */ /* idx, group->num_cells, group->num_glyphs, group->first_glyph_idx, group->first_cell_idx, group_state.num_glyphs); */ if (group->num_glyphs) { - size_t sz = MAX(group->num_glyphs, group->num_cells) + 16; - if (global_glyph_render_scratch.sz < sz) { -#define a(what) free(global_glyph_render_scratch.what); global_glyph_render_scratch.what = malloc(sz * sizeof(global_glyph_render_scratch.what[0])); if (!global_glyph_render_scratch.what) fatal("Out of memory"); - 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(); - } + ensure_glyph_render_scratch_space(MAX(group->num_glyphs, group->num_cells)); 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, rf, global_glyph_render_scratch.glyphs, group->num_glyphs, center_glyph, tc); } @@ -1346,7 +1421,12 @@ render_run(FontGroup *fg, CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, inde while(num_cells--) { set_sprite(first_gpu_cell, 0, 0, 0); first_cpu_cell++; first_gpu_cell++; } break; case BOX_FONT: - while(num_cells--) { render_box_cell(fg, first_cpu_cell, first_gpu_cell, tc); first_cpu_cell++; first_gpu_cell++; } + while(num_cells) { + render_box_cell(fg, rf, first_cpu_cell, first_gpu_cell, tc); + num_cells -= rf.scale; + first_cpu_cell += rf.scale; + first_gpu_cell += rf.scale; + } break; case MISSING_FONT: while(num_cells--) { set_sprite(first_gpu_cell, MISSING_GLYPH, 0, 0); first_cpu_cell++; first_gpu_cell++; } @@ -1397,11 +1477,10 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, for (i=0, first_cell_in_run=0; i < line->xnum; i++) { cell_font = basic_font; CPUCell *cpu_cell = line->cpu_cells + i; - GPUCell *gpu_cell = line->gpu_cells + i; if (cpu_cell->is_multicell) { mcd = cell_multicell_data(cpu_cell, line->text_cache); if (cpu_cell->x) { - i += mcd_x_limit(mcd); + i += mcd_x_limit(mcd) - cpu_cell->x - 1; continue; } cell_font.scale = mcd.scale; cell_font.subscale = mcd.subscale; cell_font.vertical_align = mcd.vertical_align; @@ -1409,9 +1488,9 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, } text_in_cell(cpu_cell, line->text_cache, lc); bool is_main_font, is_emoji_presentation; + GPUCell *gpu_cell = line->gpu_cells + i; cell_font.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]; - if ( cell_font.font_idx != MISSING_FONT && ((!is_main_font && !is_emoji_presentation && is_symbol(first_ch)) || (cell_font.font_idx != BOX_FONT && (is_private_use(first_ch))) || is_non_emoji_dingbat(first_ch)) @@ -1519,7 +1598,7 @@ send_prerendered_sprites(FontGroup *fg) { int error = 0; sprite_index x = 0, y = 0, z = 0; // blank cell - ensure_canvas_can_fit(fg, 1); + ensure_canvas_can_fit(fg, 1, 1); current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas.buf); do_increment(fg, &error); if (error != 0) { sprite_map_set_error(error); PyErr_Print(); fatal("Failed"); } @@ -1532,7 +1611,7 @@ send_prerendered_sprites(FontGroup *fg) { do_increment(fg, &error); if (error != 0) { sprite_map_set_error(error); PyErr_Print(); fatal("Failed"); } uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(cell_addresses, i)); - ensure_canvas_can_fit(fg, 1); // clear canvas + ensure_canvas_can_fit(fg, 1, 1); // clear canvas Region r = { .right = fg->cell_width, .bottom = fg->cell_height }; render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width, 0xffffff); current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas.buf); @@ -1642,7 +1721,8 @@ test_sprite_position_for(PyObject UNUSED *self, PyObject *args) { } FontGroup *fg = font_groups; if (!num_font_groups) { PyErr_SetString(PyExc_RuntimeError, "must create font group first"); return NULL; } - SpritePosition *pos = sprite_position_for(fg, &fg->fonts[fg->medium_font_idx], glyphs, PyTuple_GET_SIZE(args), 0, 1, &error); + RunFont rf = {.scale = 1, .font_idx=fg->medium_font_idx}; + SpritePosition *pos = sprite_position_for(fg, rf, glyphs, PyTuple_GET_SIZE(args), 0, 1, &error); if (pos == NULL) { sprite_map_set_error(error); return NULL; } return Py_BuildValue("HHH", pos->x, pos->y, pos->z); } diff --git a/kitty/glyph-cache.c b/kitty/glyph-cache.c index 761f20961..8d557d208 100644 --- a/kitty/glyph-cache.c +++ b/kitty/glyph-cache.c @@ -56,7 +56,10 @@ create_sprite_position_hash_table(void) { } SpritePosition* -find_or_create_sprite_position(SPRITE_POSITION_MAP_HANDLE map_, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, glyph_index cell_count, bool *created) { +find_or_create_sprite_position( + SPRITE_POSITION_MAP_HANDLE map_, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, glyph_index cell_count, + uint8_t scale, uint8_t subscale, uint8_t multicell_y, uint8_t vertical_align, bool *created +) { sprite_pos_map *map = (sprite_pos_map*)map_; const size_t keysz_in_bytes = count * sizeof(glyph_index); if (!scratch || keysz_in_bytes > scratch_key_capacity) { @@ -68,6 +71,7 @@ find_or_create_sprite_position(SPRITE_POSITION_MAP_HANDLE map_, glyph_index *gly } scratch->keysz_in_bytes = keysz_in_bytes; scratch->count = count; scratch->ligature_index = ligature_index; scratch->cell_count = cell_count; + scratch->scale = scale; scratch->subscale = subscale; scratch->multicell_y = multicell_y; scratch->vertical_align = vertical_align; memcpy(scratch->key, glyphs, keysz_in_bytes); sprite_pos_map_itr n = vt_get(map, scratch); if (!vt_is_end(n)) { *created = false; return n.data->val; } diff --git a/kitty/glyph-cache.h b/kitty/glyph-cache.h index 7b70438cb..a8c4e461b 100644 --- a/kitty/glyph-cache.h +++ b/kitty/glyph-cache.h @@ -22,7 +22,7 @@ create_sprite_position_hash_table(void); void free_sprite_position_hash_table(SPRITE_POSITION_MAP_HANDLE *handle); SpritePosition* -find_or_create_sprite_position(SPRITE_POSITION_MAP_HANDLE map, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, glyph_index cell_count, bool *created); +find_or_create_sprite_position(SPRITE_POSITION_MAP_HANDLE map, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, glyph_index cell_count, uint8_t scale, uint8_t subscale, uint8_t multicell_y, uint8_t vertical_align, bool *created); typedef union GlyphProperties { diff --git a/kitty/line.h b/kitty/line.h index c6d8ad24d..8cf2cd936 100644 --- a/kitty/line.h +++ b/kitty/line.h @@ -20,6 +20,8 @@ // TODO: Handle rewrap of multiline chars // TODO: Handle rewrap when a character is too wide/tall to fit on resized screen // TODO: Test serialization to ansi only using escape code for explicitly set multicells +// TODO: Test rendering of box drawing at various scales and subscales and alignments +// TODO: Implement baseline align for box drawing typedef union CellAttrs { struct { @@ -57,9 +59,9 @@ typedef union MultiCellData { char_type scale: 3; char_type width: 3; char_type subscale: 2; - char_type vertical_align: 2; + char_type vertical_align: 3; char_type explicitly_set: 1; - char_type : 20; + char_type : 19; char_type msb : 1; }; char_type val; @@ -110,7 +112,7 @@ typedef struct { } Line; typedef struct MultiCellCommand { - unsigned int width, scale, subscale; + unsigned int width, scale, subscale, vertical_align; size_t payload_sz; } MultiCellCommand; diff --git a/kitty/parse-multicell-command.h b/kitty/parse-multicell-command.h index bb4aacf57..56eb9cc46 100644 --- a/kitty/parse-multicell-command.h +++ b/kitty/parse-multicell-command.h @@ -18,7 +18,7 @@ static inline void parse_multicell_code(PS *self, uint8_t *parser_buf, (void)is_negative; size_t sz; - enum KEYS { width = 'w', scale = 's', subscale = 'f' }; + enum KEYS { width = 'w', scale = 's', subscale = 'f', vertical_align = 'v' }; enum KEYS key = 'a'; if (parser_buf[pos] == ';') @@ -39,6 +39,9 @@ static inline void parse_multicell_code(PS *self, uint8_t *parser_buf, case subscale: value_state = UINT; break; + case vertical_align: + value_state = UINT; + break; default: REPORT_ERROR("Malformed MultiCellCommand control block, invalid key " "character: 0x%x", @@ -117,6 +120,7 @@ static inline void parse_multicell_code(PS *self, uint8_t *parser_buf, U(width); U(scale); U(subscale); + U(vertical_align); default: break; } @@ -168,11 +172,12 @@ static inline void parse_multicell_code(PS *self, uint8_t *parser_buf, break; } - REPORT_VA_COMMAND("K s { sI sI sI sI} y#", self->window_id, + REPORT_VA_COMMAND("K s { sI sI sI sI sI} y#", self->window_id, "multicell_command", "width", (unsigned int)g.width, "scale", (unsigned int)g.scale, "subscale", (unsigned int)g.subscale, + "vertical_align", (unsigned int)g.vertical_align, "payload_sz", g.payload_sz, parser_buf, g.payload_sz); diff --git a/kitty/screen.c b/kitty/screen.c index 671a7d8d9..d2bad8638 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1157,7 +1157,7 @@ screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const if (!width) return; MultiCellData mcd = { .width=MIN(width, 15), .scale=MAX(1, MIN(cmd->scale, 15)), .subscale=MIN(cmd->subscale, 3), - .explicitly_set=1, .msb=1 + .explicitly_set=1, .msb=1, .vertical_align=MIN(cmd->vertical_align, 7u) }; self->lc->chars[self->lc->count++] = mcd.val; width = mcd.width * mcd.scale; diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py index 103c231b1..4e3290eec 100644 --- a/kitty_tests/__init__.py +++ b/kitty_tests/__init__.py @@ -19,7 +19,7 @@ from typing import Optional from unittest import TestCase from kitty.config import finalize_keys, finalize_mouse_mappings -from kitty.fast_data_types import Cursor, HistoryBuf, LineBuf, Screen, get_options, monotonic, set_options +from kitty.fast_data_types import TEXT_SIZE_CODE, Cursor, HistoryBuf, LineBuf, Screen, get_options, monotonic, set_options from kitty.options.parse import merge_result_dicts from kitty.options.types import Options, defaults from kitty.rgb import to_color @@ -37,6 +37,11 @@ def parse_bytes(screen, data, dump_callback=None): screen.test_parse_written_data(dump_callback) +def draw_multicell(screen: Screen, text: str, width: int = 0, scale: int = 1, subscale: int = 0, vertical_align: int = 0) -> None: + cmd = f'\x1b]{TEXT_SIZE_CODE};w={width}:s={scale}:f={subscale}:v={vertical_align};{text}\a' + parse_bytes(screen, cmd.encode()) + + class Callbacks: def __init__(self, pty=None) -> None: diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index e00acbf87..19e944c9e 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -23,7 +23,7 @@ from kitty.fonts.common import FontSpec, all_fonts_map, face_from_descriptor, ge from kitty.fonts.render import coalesce_symbol_maps, render_string, setup_for_testing, shape_string from kitty.options.types import Options -from . import BaseTest +from . import BaseTest, draw_multicell def parse_font_spec(spec): @@ -207,6 +207,30 @@ class Rendering(BaseTest): test_render_line(line) self.assertEqual(len(self.sprites) - prerendered, len(box_chars)) + def test_scaled_box_drawing(self): + full_block = b'\xff' * len(next(iter(self.sprites.values()))) + empty_block = b'\0' * len(full_block) + upper_half_block = (b'\xff' * (len(full_block) // 2)) + (b'\0' * (len(full_block) // 2)) + lower_half_block = (b'\0' * (len(full_block) // 2)) + (b'\xff' * (len(full_block) // 2)) + s = self.create_screen(cols=8, lines=8, scrollback=0) + + def block_test(a=empty_block, b=empty_block, c=empty_block, d=empty_block, scale=2, subscale=1, vertical_align=0): + s.reset() + before = len(self.sprites) + draw_multicell(s, '█', scale=scale, subscale=subscale, vertical_align=vertical_align) + test_render_line(s.line(0)) + self.ae(len(self.sprites), before + 2) + test_render_line(s.line(1)) + self.ae(len(self.sprites), before + 4) + blocks = tuple(self.sprites)[before:] + for expected, actual in zip((a, b, c, d), blocks): + self.ae(self.sprites[actual], expected) + + block_test(full_block, full_block, full_block, full_block, subscale=0) + block_test(a=full_block) + block_test(c=full_block, vertical_align=1) + block_test(a=lower_half_block, c=upper_half_block, vertical_align=2) + def test_font_rendering(self): render_string('ab\u0347\u0305你好|\U0001F601|\U0001F64f|\U0001F63a|') text = 'He\u0347\u0305llo\u0341, w\u0302or\u0306l\u0354d!' diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index 5372b881b..391e78c56 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -2,9 +2,10 @@ # License: GPLv3 Copyright: 2024, Kovid Goyal -from kitty.fast_data_types import TEXT_SIZE_CODE, Screen +from kitty.fast_data_types import TEXT_SIZE_CODE -from . import BaseTest, parse_bytes +from . import BaseTest +from . import draw_multicell as multicell class TestMulticell(BaseTest): @@ -13,11 +14,6 @@ class TestMulticell(BaseTest): test_multicell(self) -def multicell(screen: Screen, text: str, width: int = 0, scale: int = 1, subscale: int = 0) -> None: - cmd = f'\x1b]{TEXT_SIZE_CODE};w={width}:s={scale}:f={subscale};{text}\a' - parse_bytes(screen, cmd.encode()) - - def test_multicell(self: TestMulticell) -> None: def ac(x_, y_, **assertions): # assert cell