diff --git a/src/ui/material/typography.cpp b/src/ui/material/typography.cpp index 05222fb..900515d 100644 --- a/src/ui/material/typography.cpp +++ b/src/ui/material/typography.cpp @@ -151,64 +151,40 @@ bool Typography::load(ImGuiIO& io, float dpiScale) // For ImGui, we need to load fonts at specific pixel sizes. // Font sizes come from Layout:: accessors (backed by UISchema JSON) - // Load fonts in order of TypeStyle enum - - // H1: Light - fonts_[0] = loadFont(io, kWeightLight, Layout::kFontH1() * scale, "H1"); - - // H2: Light - fonts_[1] = loadFont(io, kWeightLight, Layout::kFontH2() * scale, "H2"); - - // H3: Regular - fonts_[2] = loadFont(io, kWeightRegular, Layout::kFontH3() * scale, "H3"); - - // H4: Regular - fonts_[3] = loadFont(io, kWeightRegular, Layout::kFontH4() * scale, "H4"); - - // H5: Regular - fonts_[4] = loadFont(io, kWeightRegular, Layout::kFontH5() * scale, "H5"); - - // H6: Medium - fonts_[5] = loadFont(io, kWeightMedium, Layout::kFontH6() * scale, "H6"); - - // Subtitle1: Regular - fonts_[6] = loadFont(io, kWeightRegular, Layout::kFontSubtitle1() * scale, "Subtitle1"); - - // Subtitle2: Medium - fonts_[7] = loadFont(io, kWeightMedium, Layout::kFontSubtitle2() * scale, "Subtitle2"); - - // Body1: Regular (shares with Subtitle1 if same size) - if (Layout::kFontBody1() == Layout::kFontSubtitle1()) { - fonts_[8] = fonts_[6]; // Reuse Subtitle1 font - } else { - fonts_[8] = loadFont(io, kWeightRegular, Layout::kFontBody1() * scale, "Body1"); + // Style table (order == TypeStyle enum). `emoji` = the styles that have always received the emoji merge. + struct StyleDef { int weight; float px; const char* name; bool emoji; }; + const StyleDef defs[kNumStyles] = { + { kWeightLight, Layout::kFontH1(), "H1", false }, + { kWeightLight, Layout::kFontH2(), "H2", false }, + { kWeightRegular, Layout::kFontH3(), "H3", false }, + { kWeightRegular, Layout::kFontH4(), "H4", false }, + { kWeightRegular, Layout::kFontH5(), "H5", false }, + { kWeightMedium, Layout::kFontH6(), "H6", false }, + { kWeightRegular, Layout::kFontSubtitle1(), "Subtitle1", true }, + { kWeightMedium, Layout::kFontSubtitle2(), "Subtitle2", true }, + { kWeightRegular, Layout::kFontBody1(), "Body1", true }, + { kWeightRegular, Layout::kFontBody2(), "Body2", true }, + { kWeightMedium, Layout::kFontButton(), "Button", true }, + { kWeightRegular, Layout::kFontCaption(), "Caption", true }, + { kWeightRegular, Layout::kFontOverline(), "Overline", true }, + { kWeightMedium, Layout::kFontButtonSm(), "ButtonSm", true }, + { kWeightMedium, Layout::kFontButtonLg(), "ButtonLg", false }, + }; + static_assert(sizeof(fonts_) / sizeof(fonts_[0]) == kNumStyles, "style table must cover every TypeStyle"); + // Styles with the same (weight, scaled size, emoji) would build byte-identical ImFonts: load once and share + // the pointer. emoji is part of the key so no style gains or loses the emoji fallback (identical rendering). + // loadFont() adds the base face and its merges back-to-back (MergeMode attaches to Fonts.back()). + for (int i = 0; i < kNumStyles; ++i) { + const float px = defs[i].px * scale; + int first = -1; + for (int j = 0; j < i; ++j) + if (defs[j].weight == defs[i].weight && defs[j].px * scale == px && defs[j].emoji == defs[i].emoji) { first = j; break; } + fonts_[i] = (first >= 0) ? fonts_[first] : loadFont(io, defs[i].weight, px, defs[i].name, defs[i].emoji); } - - // Body2: Regular - fonts_[9] = loadFont(io, kWeightRegular, Layout::kFontBody2() * scale, "Body2"); - - // Button: Medium (shares with Subtitle2 if same size) - if (Layout::kFontButton() == Layout::kFontSubtitle2()) { - fonts_[10] = fonts_[7]; // Reuse Subtitle2 font - } else { - fonts_[10] = loadFont(io, kWeightMedium, Layout::kFontButton() * scale, "Button"); - } - - // Caption: Regular - fonts_[11] = loadFont(io, kWeightRegular, Layout::kFontCaption() * scale, "Caption"); - - // Overline: Regular - fonts_[12] = loadFont(io, kWeightRegular, Layout::kFontOverline() * scale, "Overline"); - - // ButtonSm: Medium - fonts_[13] = loadFont(io, kWeightMedium, Layout::kFontButtonSm() * scale, "ButtonSm"); - - // ButtonLg: Medium - fonts_[14] = loadFont(io, kWeightMedium, Layout::kFontButtonLg() * scale, "ButtonLg"); // Monospace (Ubuntu Mono) at caption size — used by the console so terminal text + // pretty-printed JSON columns align. Same size as Caption to keep line heights matched. - mono_ = loadFont(io, kWeightMono, Layout::kFontCaption() * scale, "Mono"); + mono_ = loadFont(io, kWeightMono, Layout::kFontCaption() * scale, "Mono", false); // --- Icon fonts at multiple sizes --- // These are standalone fonts (not merged) so we can PushFont(iconXxx) for icon rendering. @@ -247,7 +223,7 @@ bool Typography::load(ImGuiIO& io, float dpiScale) return allLoaded; } -ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* name) +ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* name, bool wantEmoji) { // Select font data based on weight const unsigned char* fontData = nullptr; @@ -273,12 +249,10 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na break; } - // ImGui needs to own a copy of the font data - void* fontDataCopy = IM_ALLOC(fontDataLen); - memcpy(fontDataCopy, fontData, fontDataLen); - + // Zero-copy: the INCBIN blob is immutable .rodata that outlives the atlas (ImGui 1.92 requires the data to + // persist for the atlas lifetime); FontDataOwnedByAtlas=false means ImGui never frees or writes it. ImFontConfig cfg; - cfg.FontDataOwnedByAtlas = true; // ImGui will free this + cfg.FontDataOwnedByAtlas = false; cfg.OversampleH = 2; cfg.OversampleV = 1; cfg.PixelSnapH = true; @@ -306,18 +280,15 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na weight == kWeightMono ? "Mono" : "Regular"; snprintf(cfg.Name, sizeof(cfg.Name), "Ubuntu %s %.0fpx", weightName, size); - ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, fontDataLen, size, &cfg); + ImFont* font = io.Fonts->AddFontFromMemoryTTF(const_cast(fontData), static_cast(fontDataLen), size, &cfg); if (font) { DEBUG_LOGF("Typography: Loaded %s (%.0fpx) as '%s'\n", name, size, cfg.Name); // Merge CJK fallback glyphs (Chinese/Japanese/Korean) from subset font if (g_noto_cjk_subset_size > 0) { - void* cjkCopy = IM_ALLOC(g_noto_cjk_subset_size); - memcpy(cjkCopy, g_noto_cjk_subset_data, g_noto_cjk_subset_size); - ImFontConfig cjkCfg; - cjkCfg.FontDataOwnedByAtlas = true; + cjkCfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob cjkCfg.MergeMode = true; // merge into the font we just loaded cjkCfg.OversampleH = 1; cjkCfg.OversampleV = 1; @@ -338,7 +309,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na cjkCfg.GlyphRanges = cjkRanges; snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size); - ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg); + ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(const_cast(g_noto_cjk_subset_data), + static_cast(g_noto_cjk_subset_size), size, &cjkCfg); if (mergeResult) { DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n", g_noto_cjk_subset_size, name); @@ -348,15 +320,9 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na } } - // 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; } + // Emoji (monochrome subset, or COLR color via FreeType) is merged only into the small text styles; load() + // passes `wantEmoji` from its style table (dedup keys on it). ImGui does no shaping (no ZWJ/flags); the + // >0xFFFF ranges need IMGUI_USE_WCHAR32. // Emoji blob: the COLR/CPAL color font (FreeType-rendered) when color emoji is enabled and this // is a FreeType build, else the monochrome subset (default / non-FreeType path). @@ -371,11 +337,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na } #endif if (wantEmoji && emojiSize > 0) { - void* emojiCopy = IM_ALLOC(emojiSize); - memcpy(emojiCopy, emojiData, emojiSize); - ImFontConfig emojiCfg; - emojiCfg.FontDataOwnedByAtlas = true; + emojiCfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob emojiCfg.MergeMode = true; // merge into the text font just loaded emojiCfg.OversampleH = 1; emojiCfg.OversampleV = 1; @@ -396,17 +359,17 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "%s %.0fpx (merge)", colorGlyphs ? "Twemoji" : "NotoEmoji", size); - ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(emojiCopy, emojiSize, size, &emojiCfg); + ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(const_cast(emojiData), + static_cast(emojiSize), size, &emojiCfg); if (emojiMerge) { DEBUG_LOGF("Typography: Merged %s emoji (%u bytes) into %s OK\n", colorGlyphs ? "color" : "mono", emojiSize, name); } else { - DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, size); + DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, emojiSize); } } } else { DEBUG_LOGF("Typography: Failed to load %s\n", name); - IM_FREE(fontDataCopy); } return font; @@ -414,12 +377,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name) { - // ImGui needs to own a copy of the font data - void* fontDataCopy = IM_ALLOC(g_material_icons_size); - memcpy(fontDataCopy, g_material_icons_data, g_material_icons_size); - ImFontConfig cfg; - cfg.FontDataOwnedByAtlas = true; + cfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob cfg.OversampleH = 2; cfg.OversampleV = 1; cfg.PixelSnapH = true; @@ -431,12 +390,11 @@ ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name) snprintf(cfg.Name, sizeof(cfg.Name), "MDIcons %.0fpx", size); - ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_material_icons_size, size, &cfg); + ImFont* font = io.Fonts->AddFontFromMemoryTTF(const_cast(g_material_icons_data), static_cast(g_material_icons_size), size, &cfg); if (font) { DEBUG_LOGF("Typography: Loaded icon font %s (%.0fpx)\n", name, size); } else { DEBUG_LOGF("Typography: Failed to load icon font %s\n", name); - IM_FREE(fontDataCopy); } return font; } @@ -448,11 +406,8 @@ ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name) return nullptr; } - void* fontDataCopy = IM_ALLOC(g_mdi_pickaxe_subset_size); - memcpy(fontDataCopy, g_mdi_pickaxe_subset_data, g_mdi_pickaxe_subset_size); - ImFontConfig cfg; - cfg.FontDataOwnedByAtlas = true; + cfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob cfg.OversampleH = 2; cfg.OversampleV = 1; cfg.PixelSnapH = true; @@ -468,12 +423,11 @@ ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name) snprintf(cfg.Name, sizeof(cfg.Name), "MDIPickaxe %.0fpx", size); - ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_mdi_pickaxe_subset_size, size, &cfg); + ImFont* font = io.Fonts->AddFontFromMemoryTTF(const_cast(g_mdi_pickaxe_subset_data), static_cast(g_mdi_pickaxe_subset_size), size, &cfg); if (font) { DEBUG_LOGF("Typography: Loaded pickaxe font %s (%.0fpx)\n", name, size); } else { DEBUG_LOGF("Typography: Failed to load pickaxe font %s\n", name); - IM_FREE(fontDataCopy); } return font; } diff --git a/src/ui/material/typography.h b/src/ui/material/typography.h index 159d77f..8667080 100644 --- a/src/ui/material/typography.h +++ b/src/ui/material/typography.h @@ -259,7 +259,7 @@ private: Typography& operator=(const Typography&) = delete; // Load fonts at a specific size with a specific weight - ImFont* loadFont(ImGuiIO& io, int weight, float size, const char* name); + ImFont* loadFont(ImGuiIO& io, int weight, float size, const char* name, bool wantEmoji); // Font weight constants static constexpr int kWeightLight = 300;