diff --git a/docs/changelog.rst b/docs/changelog.rst index 85932395d..cb02b077d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -61,6 +61,8 @@ Detailed list of changes - macOS: Fix returning from full screen via the button when the titlebar is hidden not hiding the buttons (:iss:`6883`) +- Font fallback: Fix the font used to render a character sometimes dependent on the order in which characters appear on screen (:iss:`6865`) + 0.31.0 [2023-11-08] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/core_text.m b/kitty/core_text.m index 8e11a9b4f..36b53be74 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -240,7 +240,7 @@ find_substitute_face(CFStringRef str, CTFontRef old_font, CPUCell *cpu_cell) { new_font = manually_search_fallback_fonts(old_font, cpu_cell); if (new_font) return new_font; } - PyErr_SetString(PyExc_ValueError, "Failed to find fallback CTFont other than the LastResort font"); + PyErr_Format(PyExc_ValueError, "Failed to find fallback CTFont other than the LastResort font for: %s", [(NSString *)str UTF8String]); return NULL; } return new_font; @@ -270,7 +270,22 @@ create_fallback_face(PyObject *base_face, CPUCell* cell, bool UNUSED bold, bool } else { search_for_fallback(); } if (new_font == NULL) return NULL; - return (PyObject*)ct_face(new_font, fg); + NSURL *url = (NSURL*)CTFontCopyAttribute(new_font, kCTFontURLAttribute); + const char *font_path = [[url path] UTF8String]; + ssize_t idx = -1; + PyObject *q, *ans = NULL; + while ((q = iter_fallback_faces(fg, &idx))) { + CTFace *qf = (CTFace*)q; + const char *qpath; + if (qf->path && (qpath = PyUnicode_AsUTF8(qf->path)) && strcmp(qpath, font_path) == 0) { + ans = PyLong_FromSsize_t(idx); + break; + } + } + [url release]; + if (ans == NULL) return (PyObject*)ct_face(new_font, fg); + CFRelease(new_font); + return ans; } unsigned int diff --git a/kitty/fontconfig.c b/kitty/fontconfig.c index 9710f2109..27f144707 100644 --- a/kitty/fontconfig.c +++ b/kitty/fontconfig.c @@ -450,7 +450,15 @@ create_fallback_face(PyObject UNUSED *base_face, CPUCell* cell, bool bold, bool size_t num = cell_as_unicode_for_fallback(cell, char_buf); add_charset(pat, num); PyObject *d = _fc_match(pat); - if (d) { ans = face_from_descriptor(d, fg); Py_CLEAR(d); } + if (d) { + ssize_t idx = -1; + PyObject *q; + while ((q = iter_fallback_faces(fg, &idx))) { + if (face_equals_descriptor(q, d)) { ans = PyLong_FromSsize_t(idx); Py_CLEAR(d); goto end; } + } + ans = face_from_descriptor(d, fg); + Py_CLEAR(d); + } end: if (pat != NULL) FcPatternDestroy(pat); return ans; diff --git a/kitty/fonts.c b/kitty/fonts.c index a1e4c256e..9b5104f0b 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -12,6 +12,7 @@ #include "unicode-data.h" #include "charsets.h" #include "glyph-cache.h" +#include "kitty-uthash.h" #define MISSING_GLYPH (NUM_UNDERLINE_STYLES + 2) #define MAX_NUM_EXTRA_GLYPHS_PUA 4u @@ -71,6 +72,12 @@ typedef struct Canvas { unsigned current_cells, alloced_cells; } Canvas; +typedef struct fallback_font_map { + const char *cell_text; + size_t font_idx; + UT_hash_handle hh; +} fallback_font_map_t; + typedef struct { FONTS_DATA_HEAD id_type id; @@ -80,6 +87,7 @@ typedef struct { Font *fonts; Canvas canvas; GPUSpriteTracker sprite_tracker; + fallback_font_map_t *fallback_font_map; } FontGroup; static FontGroup* font_groups = NULL; @@ -152,6 +160,15 @@ static void del_font_group(FontGroup *fg) { free(fg->canvas.buf); fg->canvas.buf = NULL; fg->canvas = (Canvas){0}; fg->sprite_map = free_sprite_map(fg->sprite_map); + if (fg->fallback_font_map) { + fallback_font_map_t *current, *tmp; + HASH_ITER(hh, fg->fallback_font_map, current, tmp) { + free((void*)current->cell_text); + HASH_DEL(fg->fallback_font_map, current); + free(current); + } + fg->fallback_font_map = NULL; + } for (size_t i = 0; i < fg->fonts_count; i++) del_font(fg->fonts + i); free(fg->fonts); fg->fonts = NULL; } @@ -451,6 +468,16 @@ output_cell_fallback_data(CPUCell *cell, bool bold, bool italic, bool emoji_pres printf("\n"); } +PyObject* +iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx) { + FontGroup *fg = (FontGroup*)fgh; + if (*idx + 1 < (ssize_t)fg->fallback_fonts_count) { + *idx += 1; + return fg->fonts[fg->first_fallback_font_idx + *idx].face; + } + return NULL; +} + static ssize_t load_fallback_font(FontGroup *fg, CPUCell *cell, bool bold, bool italic, bool emoji_presentation) { if (fg->fallback_fonts_count > 100) { log_error("Too many fallback fonts"); return MISSING_FONT; } @@ -464,6 +491,7 @@ load_fallback_font(FontGroup *fg, CPUCell *cell, bool bold, bool italic, bool em if (face == NULL) { PyErr_Print(); return MISSING_FONT; } if (face == Py_None) { Py_DECREF(face); return MISSING_FONT; } if (global_state.debug_font_fallback) output_cell_fallback_data(cell, bold, italic, emoji_presentation, face, true); + if (PyLong_Check(face)) { ssize_t ans = fg->first_fallback_font_idx + PyLong_AsSsize_t(face); Py_DECREF(face); return ans; } set_size_for_face(face, fg->cell_height, true, (FONTS_DATA_HANDLE)fg); ensure_space_for(fg, fonts, Font, fg->fonts_count + 1, fonts_capacity, 5, true); @@ -495,17 +523,24 @@ fallback_font(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell) { bool bold = gpu_cell->attrs.bold; bool italic = gpu_cell->attrs.italic; bool emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell); - - // Check if one of the existing fallback fonts has this text - for (size_t i = 0, j = fg->first_fallback_font_idx; i < fg->fallback_fonts_count; i++, j++) { - Font *ff = fg->fonts +j; - if (ff->bold == bold && ff->italic == italic && ff->emoji_presentation == emoji_presentation && has_cell_text(ff, cpu_cell)) { - if (global_state.debug_font_fallback) output_cell_fallback_data(cpu_cell, bold, italic, emoji_presentation, ff->face, false); - return j; + char cell_text[6 + arraysz(cpu_cell->cc_idx) * 4]; + const size_t cell_text_len = cell_as_utf8(cpu_cell, true, cell_text, ' '); + if (fg->fallback_font_map) { + fallback_font_map_t *s; + HASH_FIND_STR(fg->fallback_font_map, cell_text, s); + /* printf("cache %s\n", (s ? "hit" : "miss")); */ + if (s) return s->font_idx; + } + ssize_t idx = load_fallback_font(fg, cpu_cell, bold, italic, emoji_presentation); + fallback_font_map_t *ffm = calloc(1, sizeof(fallback_font_map_t)); + if (ffm) { + ffm->font_idx = idx; + ffm->cell_text = strndup(cell_text, cell_text_len); + if (ffm->cell_text) { + HASH_ADD_KEYPTR(hh, fg->fallback_font_map, ffm->cell_text, cell_text_len, ffm); } } - - return load_fallback_font(fg, cpu_cell, bold, italic, emoji_presentation); + return idx; } static ssize_t diff --git a/kitty/fonts.h b/kitty/fonts.h index d8bca7a60..071442242 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -30,6 +30,8 @@ PyObject* create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bo PyObject* specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE); PyObject* face_from_path(const char *path, int index, FONTS_DATA_HANDLE); PyObject* face_from_descriptor(PyObject*, FONTS_DATA_HANDLE); +PyObject* iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx); +bool face_equals_descriptor(PyObject *face_, PyObject *descriptor); const char* postscript_name_for_face(const PyObject*); void sprite_tracker_current_layout(FONTS_DATA_HANDLE data, unsigned int *x, unsigned int *y, unsigned int *z); diff --git a/kitty/freetype.c b/kitty/freetype.c index bd028202c..5f1aa2119 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -218,6 +218,17 @@ set_load_error(const char *path, int error) { return NULL; } +bool +face_equals_descriptor(PyObject *face_, PyObject *descriptor) { + Face *face = (Face*)face_; + PyObject *t = PyDict_GetItemString(descriptor, "path"); + if (!t) return false; + if (PyObject_RichCompareBool(face->path, t, Py_EQ) != 1) return false; + t = PyDict_GetItemString(descriptor, "index"); + if (t && PyLong_AsLong(t) != face->index) return false; + return true; +} + PyObject* face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) { #define D(key, conv, missing_ok) { \ @@ -285,7 +296,6 @@ repr(Face *self) { ); } - const char* postscript_name_for_face(const PyObject *face_) { const Face *self = (const Face*)face_; @@ -723,7 +733,6 @@ static PyMethodDef methods[] = { {NULL} /* Sentinel */ }; - PyTypeObject Face_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "fast_data_types.Face",