diff --git a/gen-wcwidth.py b/gen-wcwidth.py index 701f2242e..3acf3960f 100755 --- a/gen-wcwidth.py +++ b/gen-wcwidth.py @@ -71,6 +71,7 @@ def split_two(line): all_emoji = set() emoji_categories = {} +emoji_presentation_bases = set() def parse_emoji(): @@ -79,6 +80,13 @@ def parse_emoji(): s = emoji_categories.setdefault(rest, set()) s.update(chars) all_emoji.update(chars) + for line in get_data('emoji-variation-sequences.txt', 'emoji'): + base, var, *rest = line.split() + if base.startswith('#'): + continue + base = int(base, 16) + if var.upper() == 'FE0F': + emoji_presentation_bases.add(base) doublewidth, ambiguous = set(), set() @@ -231,6 +239,15 @@ def add(p, comment, chars_, ret): p('\t}') p('\treturn 1;\n}') + p('static bool\nis_emoji_presentation_base(uint32_t code) {') + p('\tswitch(code) {') + for spec in get_ranges(list(emoji_presentation_bases)): + write_case(spec, p) + p('\t\t\treturn true;') + p('\t\tdefault: return false;') + p('\t}') + p('\treturn 1;\n}') + parse_ucd() parse_emoji() diff --git a/kitty/screen.c b/kitty/screen.c index 38e32918b..c6ce96074 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -274,10 +274,31 @@ screen_designate_charset(Screen *self, uint32_t which, uint32_t as) { } } +static inline void +move_widened_char(Screen *self, Cell* cell, index_type xpos, index_type ypos) { + self->cursor->x = xpos; self->cursor->y = ypos; + Cell src = *cell, *dest; + line_clear_text(self->linebuf->line, xpos, 1, BLANK_CHAR); + + if (self->modes.mDECAWM) { // overflow goes onto next line + screen_carriage_return(self); + screen_linefeed(self); + self->linebuf->line_attrs[self->cursor->y] |= CONTINUED_MASK; + linebuf_init_line(self->linebuf, self->cursor->y); + dest = self->linebuf->line->cells; + self->cursor->x = MIN(2, self->columns); + linebuf_mark_line_dirty(self->linebuf, self->cursor->y); + } else { + dest = cell - 1; + self->cursor->x = self->columns; + } + *dest = src; +} + static inline void draw_combining_char(Screen *self, char_type ch) { bool has_prev_char = false; - index_type xpos, ypos; + index_type xpos = 0, ypos = 0; if (self->cursor->x > 0) { ypos = self->cursor->y; linebuf_init_line(self->linebuf, ypos); @@ -293,6 +314,14 @@ draw_combining_char(Screen *self, char_type ch) { line_add_combining_char(self->linebuf->line, ch, xpos); self->is_dirty = true; linebuf_mark_line_dirty(self->linebuf, ypos); + if (ch == 0xfe0f) { // emoji presentation variation marker makes default text presentation emoji (narrow emoji) into wide emoji + Cell *cell = self->linebuf->line->cells + xpos; + if ((cell->attrs & WIDTH_MASK) != 2 && cell->cc_idx[0] == mark_for_codepoint(0xfe0f) && is_emoji_presentation_base(cell->ch)) { + cell->attrs = (cell->attrs & !WIDTH_MASK) | 2; + if (xpos == self->columns - 1) move_widened_char(self, cell, xpos, ypos); + else self->cursor->x++; + } + } } } diff --git a/kitty/wcwidth-std.h b/kitty/wcwidth-std.h index 24b3e0a54..5ce0829ec 100644 --- a/kitty/wcwidth-std.h +++ b/kitty/wcwidth-std.h @@ -2700,5 +2700,368 @@ wcwidth_std(int32_t code) { } return 1; } +static bool +is_emoji_presentation_base(uint32_t code) { + switch(code) { + case 0x23: + return true; + case 0x2a: + return true; + case 0x30 ... 0x39: + return true; + case 0xa9: + return true; + case 0xae: + return true; + case 0x203c: + return true; + case 0x2049: + return true; + case 0x2122: + return true; + case 0x2139: + return true; + case 0x2194 ... 0x2199: + return true; + case 0x21a9 ... 0x21aa: + return true; + case 0x231a ... 0x231b: + return true; + case 0x2328: + return true; + case 0x23cf: + return true; + case 0x23e9 ... 0x23ea: + return true; + case 0x23ed ... 0x23ef: + return true; + case 0x23f1 ... 0x23f3: + return true; + case 0x23f8 ... 0x23fa: + return true; + case 0x24c2: + return true; + case 0x25aa ... 0x25ab: + return true; + case 0x25b6: + return true; + case 0x25c0: + return true; + case 0x25fb ... 0x25fe: + return true; + case 0x2600 ... 0x2604: + return true; + case 0x260e: + return true; + case 0x2611: + return true; + case 0x2614 ... 0x2615: + return true; + case 0x2618: + return true; + case 0x261d: + return true; + case 0x2620: + return true; + case 0x2622 ... 0x2623: + return true; + case 0x2626: + return true; + case 0x262a: + return true; + case 0x262e ... 0x262f: + return true; + case 0x2638 ... 0x263a: + return true; + case 0x2640: + return true; + case 0x2642: + return true; + case 0x2648 ... 0x2653: + return true; + case 0x2660: + return true; + case 0x2663: + return true; + case 0x2665 ... 0x2666: + return true; + case 0x2668: + return true; + case 0x267b: + return true; + case 0x267f: + return true; + case 0x2692 ... 0x2697: + return true; + case 0x2699: + return true; + case 0x269b ... 0x269c: + return true; + case 0x26a0 ... 0x26a1: + return true; + case 0x26aa ... 0x26ab: + return true; + case 0x26b0 ... 0x26b1: + return true; + case 0x26bd ... 0x26be: + return true; + case 0x26c4 ... 0x26c5: + return true; + case 0x26c8: + return true; + case 0x26cf: + return true; + case 0x26d1: + return true; + case 0x26d3 ... 0x26d4: + return true; + case 0x26e9 ... 0x26ea: + return true; + case 0x26f0 ... 0x26f5: + return true; + case 0x26f7 ... 0x26fa: + return true; + case 0x26fd: + return true; + case 0x2702: + return true; + case 0x2708 ... 0x2709: + return true; + case 0x270c ... 0x270d: + return true; + case 0x270f: + return true; + case 0x2712: + return true; + case 0x2714: + return true; + case 0x2716: + return true; + case 0x271d: + return true; + case 0x2721: + return true; + case 0x2733 ... 0x2734: + return true; + case 0x2744: + return true; + case 0x2747: + return true; + case 0x2753: + return true; + case 0x2757: + return true; + case 0x2763 ... 0x2764: + return true; + case 0x27a1: + return true; + case 0x2934 ... 0x2935: + return true; + case 0x2b05 ... 0x2b07: + return true; + case 0x2b1b ... 0x2b1c: + return true; + case 0x2b50: + return true; + case 0x2b55: + return true; + case 0x3030: + return true; + case 0x303d: + return true; + case 0x3297: + return true; + case 0x3299: + return true; + case 0x1f004: + return true; + case 0x1f170 ... 0x1f171: + return true; + case 0x1f17e ... 0x1f17f: + return true; + case 0x1f202: + return true; + case 0x1f21a: + return true; + case 0x1f22f: + return true; + case 0x1f237: + return true; + case 0x1f30d ... 0x1f30f: + return true; + case 0x1f315: + return true; + case 0x1f31c: + return true; + case 0x1f321: + return true; + case 0x1f324 ... 0x1f32c: + return true; + case 0x1f336: + return true; + case 0x1f378: + return true; + case 0x1f37d: + return true; + case 0x1f393: + return true; + case 0x1f396 ... 0x1f397: + return true; + case 0x1f399 ... 0x1f39b: + return true; + case 0x1f39e ... 0x1f39f: + return true; + case 0x1f3a7: + return true; + case 0x1f3ac ... 0x1f3ae: + return true; + case 0x1f3c2: + return true; + case 0x1f3c4: + return true; + case 0x1f3c6: + return true; + case 0x1f3ca ... 0x1f3ce: + return true; + case 0x1f3d4 ... 0x1f3e0: + return true; + case 0x1f3ed: + return true; + case 0x1f3f3: + return true; + case 0x1f3f5: + return true; + case 0x1f3f7: + return true; + case 0x1f408: + return true; + case 0x1f415: + return true; + case 0x1f41f: + return true; + case 0x1f426: + return true; + case 0x1f43f: + return true; + case 0x1f441 ... 0x1f442: + return true; + case 0x1f446 ... 0x1f449: + return true; + case 0x1f44d ... 0x1f44e: + return true; + case 0x1f453: + return true; + case 0x1f46a: + return true; + case 0x1f47d: + return true; + case 0x1f4a3: + return true; + case 0x1f4b0: + return true; + case 0x1f4b3: + return true; + case 0x1f4bb: + return true; + case 0x1f4bf: + return true; + case 0x1f4cb: + return true; + case 0x1f4da: + return true; + case 0x1f4df: + return true; + case 0x1f4e4 ... 0x1f4e6: + return true; + case 0x1f4ea ... 0x1f4ed: + return true; + case 0x1f4f7: + return true; + case 0x1f4f9 ... 0x1f4fb: + return true; + case 0x1f4fd: + return true; + case 0x1f508: + return true; + case 0x1f50d: + return true; + case 0x1f512 ... 0x1f513: + return true; + case 0x1f549 ... 0x1f54a: + return true; + case 0x1f550 ... 0x1f567: + return true; + case 0x1f56f ... 0x1f570: + return true; + case 0x1f573 ... 0x1f579: + return true; + case 0x1f587: + return true; + case 0x1f58a ... 0x1f58d: + return true; + case 0x1f590: + return true; + case 0x1f5a5: + return true; + case 0x1f5a8: + return true; + case 0x1f5b1 ... 0x1f5b2: + return true; + case 0x1f5bc: + return true; + case 0x1f5c2 ... 0x1f5c4: + return true; + case 0x1f5d1 ... 0x1f5d3: + return true; + case 0x1f5dc ... 0x1f5de: + return true; + case 0x1f5e1: + return true; + case 0x1f5e3: + return true; + case 0x1f5e8: + return true; + case 0x1f5ef: + return true; + case 0x1f5f3: + return true; + case 0x1f5fa: + return true; + case 0x1f610: + return true; + case 0x1f687: + return true; + case 0x1f68d: + return true; + case 0x1f691: + return true; + case 0x1f694: + return true; + case 0x1f698: + return true; + case 0x1f6ad: + return true; + case 0x1f6b2: + return true; + case 0x1f6b9 ... 0x1f6ba: + return true; + case 0x1f6bc: + return true; + case 0x1f6cb: + return true; + case 0x1f6cd ... 0x1f6cf: + return true; + case 0x1f6e0 ... 0x1f6e5: + return true; + case 0x1f6e9: + return true; + case 0x1f6f0: + return true; + case 0x1f6f3: + return true; + default: return false; + } + return 1; +} END_ALLOW_CASE_RANGE diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index 3d27b4e9d..ba308a94d 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -6,9 +6,8 @@ from kitty.constants import is_macos from kitty.fast_data_types import ( - set_logical_dpi, set_send_sprite_to_gpu, - sprite_map_set_layout, sprite_map_set_limits, test_render_line, - test_sprite_position_for, wcwidth + DECAWM, set_logical_dpi, set_send_sprite_to_gpu, sprite_map_set_layout, + sprite_map_set_limits, test_render_line, test_sprite_position_for, wcwidth ) from kitty.fonts.box_drawing import box_chars from kitty.fonts.render import ( @@ -87,3 +86,29 @@ def groups(text, path=None): self.ae(groups('|\U0001F601|\U0001F64f|\U0001F63a|'), [(1, 1), (2, 1), (1, 1), (2, 1), (1, 1), (2, 1), (1, 1)]) self.ae(groups('He\u0347\u0305llo\u0337,', path='kitty_tests/LiberationMono-Regular.ttf'), [(1, 1), (1, 3), (1, 1), (1, 1), (1, 2), (1, 1)]) + + def test_emoji_presentation(self): + s = self.create_screen() + s.draw('\u2716\u2716\ufe0f') + self.ae((s.cursor.x, s.cursor.y), (3, 0)) + s.draw('\u2716\u2716') + self.ae((s.cursor.x, s.cursor.y), (5, 0)) + s.draw('\ufe0f') + self.ae((s.cursor.x, s.cursor.y), (2, 1)) + self.ae(str(s.line(0)), '\u2716\u2716\ufe0f\u2716') + self.ae(str(s.line(1)), '\u2716\ufe0f') + s.draw('\u2716' * 3) + self.ae((s.cursor.x, s.cursor.y), (5, 1)) + self.ae(str(s.line(1)), '\u2716\ufe0f\u2716\u2716\u2716') + self.ae((s.cursor.x, s.cursor.y), (5, 1)) + s.reset_mode(DECAWM) + s.draw('\ufe0f') + s.set_mode(DECAWM) + self.ae((s.cursor.x, s.cursor.y), (5, 1)) + self.ae(str(s.line(1)), '\u2716\ufe0f\u2716\u2716\ufe0f') + s.cursor.y = s.lines - 1 + s.draw('\u2716' * s.columns) + self.ae((s.cursor.x, s.cursor.y), (5, 4)) + s.draw('\ufe0f') + self.ae((s.cursor.x, s.cursor.y), (2, 4)) + self.ae(str(s.line(s.cursor.y)), '\u2716\ufe0f')