From 2f93de3d68878c49b8d45e73be8dc70a81810137 Mon Sep 17 00:00:00 2001 From: kyzabuilds Date: Thu, 25 Jun 2026 20:06:52 -0400 Subject: [PATCH] Fix color render path for non-emoji codepoints on color-capable faces render_group() used has_emoji_presentation() for all codepoints, but that only makes sense for emoji (VS15/VS16 selectors). Non-emoji codepoints now use face_has_color() instead, and *was_colored is set from actual render output rather than preset before rendering. --- kitty/core_text.m | 5 +++++ kitty/fonts.c | 8 +++++++- kitty/fonts.h | 1 + kitty/freetype.c | 18 +++++++++++++----- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/kitty/core_text.m b/kitty/core_text.m index c988a0c0e..afddc6285 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -522,6 +522,11 @@ face_apply_scaling(PyObject *f, const FONTS_DATA_HANDLE fg) { return set_size_for_face(f, 0, false, fg); } +bool +face_has_color(PyObject *f) { + return CTFontSupportsColorGlyphs(((CTFace*)f)->ct_font); +} + static PyObject* set_size(CTFace *self, PyObject *args) { double font_sz_in_pts, dpi_x, dpi_y; diff --git a/kitty/fonts.c b/kitty/fonts.c index ba9aaca7e..b021e4b73 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -1200,7 +1200,13 @@ render_group( is_only_filled_boxes = true; for (unsigned i = 1; i < num_glyphs && is_only_filled_boxes; i++) if (global_glyph_render_scratch.glyphs[i] != box_glyph_id) is_only_filled_boxes = false; } - bool was_colored = !is_only_filled_boxes && has_emoji_presentation(cpu_cells, global_glyph_render_scratch.lc); + const ListOfChars *lc = global_glyph_render_scratch.lc; + // For emoji, honor the text/emoji presentation selectors (VS15/VS16); for a + // non-emoji codepoint on a color-capable face (e.g. an icon font mapped via + // symbol_map), use the color render path. + bool first_is_emoji = lc->count && char_props_for(lc->chars[0]).is_emoji; + bool was_colored = !is_only_filled_boxes && ( + first_is_emoji ? has_emoji_presentation(cpu_cells, lc) : face_has_color(font->face)); GlyphRenderInfo ri = {0}; pixel *canvas = rendering_in_smaller_area && canvas_width != scaled_canvas_width ? scratch : fg->canvas.buf; if (is_only_filled_boxes) { // special case rendering of █ for tests diff --git a/kitty/fonts.h b/kitty/fonts.h index dd5eeaae1..4d9a40055 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -62,6 +62,7 @@ typedef void (*free_extra_data_func)(void*); StringCanvas render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline); StringCanvas render_simple_text(FONTS_DATA_HANDLE fg_, const char *text); bool face_apply_scaling(PyObject*face, const FONTS_DATA_HANDLE fg); +bool face_has_color(PyObject *face); bool add_font_name_record(PyObject *table, uint16_t platform_id, uint16_t encoding_id, uint16_t language_id, uint16_t name_id, const char *string, uint16_t string_len); diff --git a/kitty/freetype.c b/kitty/freetype.c index a1ec1e1b7..c62e1af62 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -246,6 +246,11 @@ face_apply_scaling(PyObject *f, const FONTS_DATA_HANDLE fg) { return false; } +bool +face_has_color(PyObject *f) { + return ((Face*)f)->has_color; +} + static bool init_ft_face(Face *self, PyObject *path, int hinting, int hintstyle, long index, FONTS_DATA_HANDLE fg) { copy_face_metrics(self); @@ -964,24 +969,27 @@ static const ProcessedBitmap EMPTY_PBM = {.factor = 1}; bool render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *info, hb_glyph_position_t *positions, unsigned int num_glyphs, pixel *canvas, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline, bool *was_colored, FONTS_DATA_HANDLE fg, GlyphRenderInfo *ri) { Face *self = (Face*)f; - bool is_emoji = *was_colored; *was_colored = is_emoji && self->has_color; + // *was_colored is an incoming hint; it is set on output to reflect whether + // any glyph was actually rendered as color. + bool want_color = *was_colored && self->has_color; + *was_colored = false; float x = 0.f, y = 0.f; ProcessedBitmap bm; unsigned int canvas_width = cell_width * num_cells; GlyphColorType colored; for (unsigned int i = 0; i < num_glyphs; i++) { bm = EMPTY_PBM; + bool colored_glyph = false; // dont load the space glyph since loading it fails for some fonts/sizes and it is anyway to be rendered as a blank if (info[i].codepoint != self->space_glyph_id) { - if (*was_colored && (colored = glyph_color_type(self, info[i].codepoint)) != NOT_COLORED) { + if (want_color && (colored = glyph_color_type(self, info[i].codepoint)) != NOT_COLORED) { if (!render_color_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, baseline)) { if (PyErr_Occurred()) PyErr_Print(); if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) { free_processed_bitmap(&bm); return false; } - *was_colored = false; - } + } else { colored_glyph = true; *was_colored = true; } } else { if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) { free_processed_bitmap(&bm); @@ -992,7 +1000,7 @@ render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *inf float x_offset = x + (float)positions[i].x_offset / 64.0f; y = (float)positions[i].y_offset / 64.0f; if (debug_placement) printf("%d: x=%f canvas: %u", i, x_offset, canvas_width); - if ((*was_colored || self->face->glyph->metrics.width > 0) && bm.width > 0) { + if ((colored_glyph || self->face->glyph->metrics.width > 0) && bm.width > 0) { place_bitmap_in_canvas(canvas, &bm, canvas_width, cell_height, x_offset, y, baseline, i, 0xffffff, 0, 0); } if (debug_placement) printf(" adv: %f\n", (float)positions[i].x_advance / 64.0f);