diff --git a/docs/changelog.rst b/docs/changelog.rst index 43e821b88..5896932e1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -68,6 +68,10 @@ To update |kitty|, :doc:`follow the instructions `. some reason puts empty glyphs after the ligature glyph rather than before it (:iss:`3313`) +- Improve handling of infinite length ligatures in newer versions of FiraCode + and CascadiaCode. Now such ligatures are detected based on glyph naming + convention, this removes the gap in the ligatures at cell boundaries (:iss:`2695`) + 0.19.3 [2020-12-19] ------------------- diff --git a/kitty/fonts.c b/kitty/fonts.c index 702cbeb18..934ee9d24 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -23,6 +23,9 @@ extern PyTypeObject Line_Type; typedef struct SpecialGlyphCache SpecialGlyphCache; enum {NO_FONT=-3, MISSING_FONT=-2, BLANK_FONT=-1, BOX_FONT=0}; +typedef enum { + LIGATURE_UNKNOWN, INFINITE_LIGATURE_START, INFINITE_LIGATURE_MIDDLE, INFINITE_LIGATURE_END +} LigatureType; typedef struct { @@ -72,6 +75,8 @@ typedef struct { static SymbolMap *symbol_maps = NULL; static size_t num_symbol_maps = 0; +typedef enum { SPACER_STARTEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER } SpacerStrategy; + typedef struct { PyObject *face; // Map glyphs to sprite map co-ords @@ -80,6 +85,7 @@ typedef struct { size_t num_ffs_hb_features; SpecialGlyphCache special_glyph_cache[SPECIAL_GLYPH_CACHE_SIZE]; bool bold, italic, emoji_presentation; + SpacerStrategy spacer_strategy; } Font; typedef struct { @@ -767,7 +773,7 @@ typedef struct { typedef struct { unsigned int first_glyph_idx, first_cell_idx, num_glyphs, num_cells; - bool has_special_glyph, is_space_ligature, started_with_empty_glyph; + bool has_special_glyph, is_space_ligature, started_with_infinite_ligature; } Group; typedef struct { @@ -887,10 +893,37 @@ check_cell_consumed(CellData *cell_data, CPUCell *last_cpu_cell) { return 0; } +static inline LigatureType +ligature_type_from_glyph_name(const char *glyph_name) { + const char *p = strrchr(glyph_name, '_'); + if (!p) return LIGATURE_UNKNOWN; + if (strcmp(p, "_start.seq") == 0) return INFINITE_LIGATURE_START; + if (strcmp(p, "_middle.seq") == 0) return INFINITE_LIGATURE_MIDDLE; + if (strcmp(p, "_end.seq") == 0) return INFINITE_LIGATURE_END; + return LIGATURE_UNKNOWN; +} + +#define G(x) (group_state.x) + +static inline void +detect_spacer_strategy(hb_font_t *hbf, Font *font) { + CPUCell cpu_cells[3] = {{.ch = '='}, {.ch = '='}, {.ch = '='}}; + GPUCell gpu_cells[3] = {{.attrs = 1}, {.attrs = 1}, {.attrs = 1}}; + shape(cpu_cells, gpu_cells, arraysz(cpu_cells), hbf, font, false); + font->spacer_strategy = SPACERS_BEFORE; + if (G(num_glyphs) > 1) { + glyph_index glyph_id = G(info)[G(num_glyphs) - 1].codepoint; + bool is_special = is_special_glyph(glyph_id, font, &G(current_cell_data)); + bool is_empty = is_special && is_empty_glyph(glyph_id, font); + if (is_empty) font->spacer_strategy = SPACERS_AFTER; + } +} static inline void shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, Font *font, bool disable_ligature) { - shape(first_cpu_cell, first_gpu_cell, num_cells, harfbuzz_font_for_face(font->face), font, disable_ligature); + hb_font_t *hbf = harfbuzz_font_for_face(font->face); + if (font->spacer_strategy == SPACER_STARTEGY_UNKNOWN) detect_spacer_strategy(hbf, font); + shape(first_cpu_cell, first_gpu_cell, num_cells, hbf, font, disable_ligature); #if 0 static char dbuf[1024]; // You can also generate this easily using hb-shape --show-extents --cluster-level=1 --shapers=ot /path/to/font/file text @@ -906,17 +939,24 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells * 1. ABC becomes EMPTY, EMPTY, WIDE GLYPH this means we have to render N glyphs in N cells (example Fira Code) * 2. ABC becomes WIDE GLYPH this means we have to render one glyph in N cells (example Operator Mono Lig) * 3. ABC becomes WIDE GLYPH, EMPTY, EMPTY this means we have to render N glyphs in N cells (example Cascadia Code) + * 4. Variable length ligatures are identified by a glyph naming convention of _start.seq, _middle.seq and _end.seq + * with EMPTY glyphs in the middle or after (both Fira Code and Cascadia Code) * * We rely on the cluster numbers from harfbuzz to tell us how many unicode codepoints a glyph corresponds to. - * Then we check if the glyph is a ligature glyph (is_special_glyph) and if it is an empty glyph. These three - * datapoints give us enough information to satisfy the constraint above, for a wide variety of fonts. + * Then we check if the glyph is a ligature glyph (is_special_glyph) and if it is an empty glyph. + * We detect if the font uses EMPTY glyphs before or after ligature glyphs (1. or 3. above) by checking what it does for ===. + * Finally we look at the glyph name. These five datapoints give us enough information to satisfy the constraint above, + * for a wide variety of fonts. */ uint32_t cluster, next_cluster; bool add_to_current_group; -#define G(x) (group_state.x) + char glyph_name[128]; glyph_name[arraysz(glyph_name)-1] = 0; + bool prev_glyph_was_inifinte_ligature_end = false; #define MAX_GLYPHS_IN_GROUP (MAX_NUM_EXTRA_GLYPHS + 1u) while (G(glyph_idx) < G(num_glyphs) && G(cell_idx) < G(num_cells)) { glyph_index glyph_id = G(info)[G(glyph_idx)].codepoint; + hb_font_glyph_to_string(hbf, glyph_id, glyph_name, arraysz(glyph_name)-1); + LigatureType ligature_type = ligature_type_from_glyph_name(glyph_name); cluster = G(info)[G(glyph_idx)].cluster; bool is_special = is_special_glyph(glyph_id, font, &G(current_cell_data)); bool is_empty = is_special && is_empty_glyph(glyph_id, font); @@ -932,9 +972,12 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells } if (!current_group->num_glyphs) { add_to_current_group = true; + } else if (current_group->started_with_infinite_ligature) { + if (prev_glyph_was_inifinte_ligature_end) add_to_current_group = is_empty && font->spacer_strategy == SPACERS_AFTER; + else add_to_current_group = ligature_type == INFINITE_LIGATURE_MIDDLE || ligature_type == INFINITE_LIGATURE_END || is_empty; } else { if (is_special) { - if (current_group->started_with_empty_glyph) add_to_current_group = G(prev_was_empty); + if (font->spacer_strategy == SPACERS_BEFORE) add_to_current_group = G(prev_was_empty); else add_to_current_group = is_empty; } else { add_to_current_group = !G(prev_was_special); @@ -943,8 +986,8 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells if (current_group->num_glyphs >= MAX_GLYPHS_IN_GROUP || current_group->num_cells >= MAX_GLYPHS_IN_GROUP) add_to_current_group = false; if (!add_to_current_group) { G(group_idx)++; current_group = G(groups) + G(group_idx); } - if (is_empty && !current_group->num_glyphs) current_group->started_with_empty_glyph = true; if (!current_group->num_glyphs++) { + if (ligature_type == INFINITE_LIGATURE_START || ligature_type == INFINITE_LIGATURE_MIDDLE) current_group->started_with_infinite_ligature = true; current_group->first_glyph_idx = G(glyph_idx); current_group->first_cell_idx = G(cell_idx); } @@ -999,6 +1042,7 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells G(prev_was_special) = is_special; G(prev_was_empty) = is_empty; G(previous_cluster) = cluster; + prev_glyph_was_inifinte_ligature_end = ligature_type == INFINITE_LIGATURE_END; G(glyph_idx)++; } #undef MOVE_GLYPH_TO_NEXT_GROUP diff --git a/kitty_tests/FiraCode-Medium.otf b/kitty_tests/FiraCode-Medium.otf index 89f0e33ae..2cad0184a 100644 Binary files a/kitty_tests/FiraCode-Medium.otf and b/kitty_tests/FiraCode-Medium.otf differ diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index 2052d011c..c55d385cc 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -79,18 +79,19 @@ class Rendering(BaseTest): def groups(text, font=None): return [x[:2] for x in ss(text, font)] - self.ae(groups('abcd'), [(1, 1) for i in range(4)]) - self.ae(groups('A=>>B!=C', font='FiraCode-Medium.otf'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)]) - self.ae(groups('==!=<>==<><><>', font='FiraCode-Medium.otf'), [(2, 2), (2, 2), (2, 2), (2, 2), (2, 2), (2, 2), (2, 2)]) - for font in ('FiraCode-Medium.otf', 'CascadiaCode-Regular.otf'): g = partial(groups, font=font) + self.ae(g('abcd'), [(1, 1) for i in range(4)]) + self.ae(g('----'), [(4, 4)]) self.ae(g('A===B!=C'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)]) self.ae(g('F--a--'), [(1, 1), (2, 2), (1, 1), (2, 2)]) self.ae(g('===--<>=='), [(3, 3), (2, 2), (2, 2), (2, 2)]) + self.ae(g('==!=<>==<><><>'), [(4, 4), (2, 2), (2, 2), (2, 2), (2, 2), (2, 2)]) + self.ae(g('A=>>B!=C'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)]) + self.ae(g('-' * 18), [(9, 9), (9, 9)]) colon_glyph = ss('9:30', font='FiraCode-Medium.otf')[1][2] self.assertNotEqual(colon_glyph, ss(':', font='FiraCode-Medium.otf')[0][2]) - self.ae(colon_glyph, 998) + self.ae(colon_glyph, 1031) self.ae(groups('9:30', font='FiraCode-Medium.otf'), [(1, 1), (1, 1), (1, 1), (1, 1)]) self.ae(groups('|\U0001F601|\U0001F64f|\U0001F63a|'), [(1, 1), (2, 1), (1, 1), (2, 1), (1, 1), (2, 1), (1, 1)])