diff --git a/libs/imgui/imconfig.h b/libs/imgui/imconfig.h index 322e5b8..20979ab 100644 --- a/libs/imgui/imconfig.h +++ b/libs/imgui/imconfig.h @@ -67,7 +67,8 @@ //#define IMGUI_USE_LEGACY_CRC32_ADLER //---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) -//#define IMGUI_USE_WCHAR32 +//---- Enabled so chat can render emoji (U+1F300+, above the BMP) — see Typography::loadFont emoji merge (Q12). +#define IMGUI_USE_WCHAR32 //---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version // By default the embedded implementations are declared static and not available outside of Dear ImGui sources files. diff --git a/res/fonts/NotoEmoji-Subset.ttf b/res/fonts/NotoEmoji-Subset.ttf new file mode 100644 index 0000000..bb1ed16 Binary files /dev/null and b/res/fonts/NotoEmoji-Subset.ttf differ diff --git a/scripts/build_emoji_subset.py b/scripts/build_emoji_subset.py new file mode 100644 index 0000000..9a102be --- /dev/null +++ b/scripts/build_emoji_subset.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Build a monochrome Noto Emoji subset for the chat/message UI. + +Dear ImGui rasterizes fonts with stb_truetype, which handles only monochrome +(outline `glyf`) fonts — NOT color emoji (CBDT/sbix/COLR). So we use Google's +*monochrome* Noto Emoji (github.com/google/fonts, ofl/notoemoji, OFL-licensed) +and merge it into the text fonts (see Typography::loadFont, the block after the +CJK merge). ImGui renders one glyph per codepoint with no shaping, so ZWJ +sequences / regional-indicator flags won't compose — single-codepoint emoji +(😀 🎉 ❤ 🔥 👍 …) render fine, which covers the overwhelming majority of use. + +Source is the variable font pinned to wght=400 → static, then subset to the +emoji planes plus the higher symbol/star ranges the base UI font doesn't cover. +The base Ubuntu font already owns U+2600–26FF etc.; ImGui's MergeMode gives the +first-loaded glyph precedence, so those stay text-styled and only the codepoints +the base lacks fall through to this font. + +Get the source once (OFL, redistributable): + curl -fsSL -o /tmp/NotoEmoji-VF.ttf \ + 'https://github.com/google/fonts/raw/main/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf' + +Then: python3 scripts/build_emoji_subset.py +Output: res/fonts/NotoEmoji-Subset.ttf (committed; embedded via INCBIN) +""" +import os +from fontTools import ttLib, subset +from fontTools.varLib.instancer import instantiateVariableFont + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SOURCE_VF = '/tmp/NotoEmoji-VF.ttf' +STATIC = '/tmp/NotoEmoji-Static.ttf' +OUTPUT = os.path.join(ROOT, 'res', 'fonts', 'NotoEmoji-Subset.ttf') + +# Emoji codepoints to keep. Ranges are inclusive. +RANGES = [ + (0x1F000, 0x1FAFF), # all the main emoji planes (emoticons, pictographs, transport, supplement, extended) + (0x2600, 0x27BF), # Miscellaneous Symbols + Dingbats + (0x2B00, 0x2BFF), # stars (⭐ 2B50) and misc arrows + (0xFE00, 0xFE0F), # variation selectors (VS16 emoji-style) + (0x2194, 0x21AA), # arrows used as emoji + (0x231A, 0x231B), # ⌚ ⌛ + (0x23E9, 0x23FA), # media-control emoji + (0x25AA, 0x25FE), # small squares +] +SINGLES = [0x200D, 0x2934, 0x2935, 0x3030, 0x303D, 0x3297, 0x3299, + 0x00A9, 0x00AE, 0x2122, 0x2139, 0x24C2] + + +def main(): + if not os.path.exists(SOURCE_VF): + raise SystemExit(f"missing source font {SOURCE_VF} — see the header for the curl command") + + # 1. Pin the weight axis so stb_truetype rasterizes a clean static instance. + f = ttLib.TTFont(SOURCE_VF) + if 'fvar' in f: + instantiateVariableFont(f, {'wght': 400}, inplace=True) + f.save(STATIC) + + unicodes = list(SINGLES) + for lo, hi in RANGES: + unicodes.extend(range(lo, hi + 1)) + + opts = subset.Options() + opts.layout_features = [] # ImGui does no shaping — drop GSUB/GPOS + opts.name_IDs = [] + opts.notdef_outline = True + opts.glyph_names = False + opts.drop_tables = ['GSUB', 'GPOS', 'GDEF', 'morx', 'kern'] + + font = subset.load_font(STATIC, opts) + ss = subset.Subsetter(options=opts) + ss.populate(unicodes=unicodes) + ss.subset(font) + subset.save_font(font, OUTPUT, opts) + + out = ttLib.TTFont(OUTPUT) + cmap = out.getBestCmap() + color = any(t in out.reader.keys() for t in ('CBDT', 'sbix', 'COLR')) + print(f"Output: {OUTPUT}") + print(f"Size: {os.path.getsize(OUTPUT)//1024} KB | glyphs: {len(cmap)} | color tables: {color}") + if color: + raise SystemExit("ERROR: subset has color tables — stb_truetype cannot render it") + + +if __name__ == '__main__': + main() diff --git a/src/embedded/embedded_fonts.cpp.in b/src/embedded/embedded_fonts.cpp.in index 0871f76..8e76f0f 100644 --- a/src/embedded/embedded_fonts.cpp.in +++ b/src/embedded/embedded_fonts.cpp.in @@ -15,3 +15,4 @@ INCBIN(ubuntu_mono, "@CMAKE_SOURCE_DIR@/res/fonts/UbuntuMono-R.ttf"); INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf"); INCBIN(mdi_pickaxe_subset, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf"); INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf"); +INCBIN(noto_emoji_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoEmoji-Subset.ttf"); diff --git a/src/embedded/embedded_fonts.h b/src/embedded/embedded_fonts.h index 6b8b501..5cfc569 100644 --- a/src/embedded/embedded_fonts.h +++ b/src/embedded/embedded_fonts.h @@ -35,4 +35,7 @@ extern "C" { extern const unsigned char g_noto_cjk_subset_data[]; extern const unsigned int g_noto_cjk_subset_size; + + extern const unsigned char g_noto_emoji_subset_data[]; + extern const unsigned int g_noto_emoji_subset_size; } diff --git a/src/ui/material/typography.cpp b/src/ui/material/typography.cpp index 681a682..7c29dc5 100644 --- a/src/ui/material/typography.cpp +++ b/src/ui/material/typography.cpp @@ -325,11 +325,50 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na name, g_noto_cjk_subset_size); } } + + // Merge monochrome emoji for chat + user text (Q12). Only into the small text fonts — baking + // ~1400 emoji at heading sizes would bloat the atlas for glyphs no heading needs. ImGui does no + // shaping, so single-codepoint emoji render but ZWJ sequences / flags won't compose. The + // >0xFFFF ranges require IMGUI_USE_WCHAR32 (imconfig.h). + static const char* const kEmojiFonts[] = { + "Body1", "Body2", "Subtitle1", "Subtitle2", "Caption", "Overline", "Button", "ButtonSm" + }; + bool wantEmoji = false; + for (const char* n : kEmojiFonts) if (strcmp(name, n) == 0) { wantEmoji = true; break; } + if (wantEmoji && g_noto_emoji_subset_size > 0) { + void* emojiCopy = IM_ALLOC(g_noto_emoji_subset_size); + memcpy(emojiCopy, g_noto_emoji_subset_data, g_noto_emoji_subset_size); + + ImFontConfig emojiCfg; + emojiCfg.FontDataOwnedByAtlas = true; + emojiCfg.MergeMode = true; // merge into the text font just loaded + emojiCfg.OversampleH = 1; + emojiCfg.OversampleV = 1; + emojiCfg.PixelSnapH = true; + emojiCfg.GlyphMinAdvanceX = 0; + // The base Ubuntu font already owns U+2600–26FF etc.; MergeMode keeps the first-loaded glyph, + // so its text-style symbols win and only the codepoints it lacks fall through to emoji. + static const ImWchar emojiRanges[] = { + 0x2600, 0x27BF, // Misc Symbols + Dingbats + 0x2B00, 0x2BFF, // stars (⭐) + arrows + 0x1F000, 0x1FAFF, // emoji planes (emoticons, pictographs, transport, supplement, extended) + 0, + }; + emojiCfg.GlyphRanges = emojiRanges; + snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "NotoEmoji %.0fpx (merge)", size); + + ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(emojiCopy, g_noto_emoji_subset_size, size, &emojiCfg); + if (emojiMerge) { + DEBUG_LOGF("Typography: Merged emoji (%u bytes) into %s OK\n", g_noto_emoji_subset_size, name); + } else { + DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, size); + } + } } else { DEBUG_LOGF("Typography: Failed to load %s\n", name); IM_FREE(fontDataCopy); } - + return font; }