// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "typography.h" #include "../layout.h" #include "../schema/ui_schema.h" #include #include #include #include // Embedded font data (via INCBIN – assembler .incbin directive) #include "../embedded/embedded_fonts.h" // g_ubuntu_*_data/size, g_material_icons_* #include "../embedded/IconsMaterialDesign.h" // Icon codepoint defines #include "../../util/logger.h" #ifdef DRAGONX_HAVE_FREETYPE #include "imgui_internal.h" // ImFontAtlasGetFontLoaderForStbTruetype #include "misc/freetype/imgui_freetype.h" // ImGuiFreeType::GetFontLoader + LoadColor flag #endif namespace dragonx { namespace ui { namespace material { // ============================================================================ // Type Scale Specifications // ============================================================================ // Font sizes are configured in ui/layout.h for easy global adjustment. // Letter spacing, line height and text-transform are loaded from ui.toml // via the UISchema typography section. // Font sizes come from UISchema (JSON-configurable) via Layout:: accessors. const TypeSpec* Typography::getTypeSpecs() { // Memoized on the schema generation. Every input (typography.* and fonts.*) is read via // schema::UI().drawElement, and UISchema bumps generation() on each loadFromFile / loadFromString / // mergeOverlayFromFile (startup, hot-reload, skin/theme switch). ~0u sentinel: generation_ starts at 0, // so the first call always builds (pre-load defaults), then the first load rebuilds. // DPI / font scale / language are NOT inputs (kFont*() are raw schema px). UI thread only, as before. static TypeSpec specs[kNumStyles]; static uint32_t s_builtGen = ~0u; // Helper: read typography entry from UISchema auto& S = schema::UI(); const uint32_t gen = S.generation(); if (gen == s_builtGen) return specs; s_builtGen = gen; auto tspec = [&](const char* key, float defLS, float defLH, bool defUpper) -> TypeSpec { const auto& e = S.drawElement("typography", key); // ref into styleCache_: never let it escape this lambda float ls = e.getFloat("letter-spacing", defLS); float lh = e.getFloat("line-height", defLH); // text-transform stored as extraColors (string map) bool upper = defUpper; auto it = e.extraColors.find("text-transform"); if (it != e.extraColors.end()) { upper = (it->second == "uppercase"); } return TypeSpec{0.0f, ls, lh, upper}; }; // Font size comes from Layout:: accessors; typography from UISchema auto ts0 = tspec("h1", -1.0f, 1.167f, false); auto ts1 = tspec("h2", -0.5f, 1.2f, false); auto ts2 = tspec("h3", 0.0f, 1.167f, false); auto ts3 = tspec("h4", 0.25f, 1.235f, false); auto ts4 = tspec("h5", 0.0f, 1.334f, false); auto ts5 = tspec("h6", 0.15f, 1.6f, false); auto ts6 = tspec("subtitle1", 0.15f, 1.75f, false); auto ts7 = tspec("subtitle2", 0.1f, 1.57f, false); auto ts8 = tspec("body1", 0.5f, 1.5f, false); auto ts9 = tspec("body2", 0.25f, 1.43f, false); auto ts10 = tspec("button", 1.25f, 1.75f, true); auto ts11 = tspec("caption", 0.4f, 1.66f, false); auto ts12 = tspec("overline", 1.5f, 2.66f, true); specs[0] = { Layout::kFontH1(), ts0.letterSpacing, ts0.lineHeight, ts0.uppercase }; specs[1] = { Layout::kFontH2(), ts1.letterSpacing, ts1.lineHeight, ts1.uppercase }; specs[2] = { Layout::kFontH3(), ts2.letterSpacing, ts2.lineHeight, ts2.uppercase }; specs[3] = { Layout::kFontH4(), ts3.letterSpacing, ts3.lineHeight, ts3.uppercase }; specs[4] = { Layout::kFontH5(), ts4.letterSpacing, ts4.lineHeight, ts4.uppercase }; specs[5] = { Layout::kFontH6(), ts5.letterSpacing, ts5.lineHeight, ts5.uppercase }; specs[6] = { Layout::kFontSubtitle1(), ts6.letterSpacing, ts6.lineHeight, ts6.uppercase }; specs[7] = { Layout::kFontSubtitle2(), ts7.letterSpacing, ts7.lineHeight, ts7.uppercase }; specs[8] = { Layout::kFontBody1(), ts8.letterSpacing, ts8.lineHeight, ts8.uppercase }; specs[9] = { Layout::kFontBody2(), ts9.letterSpacing, ts9.lineHeight, ts9.uppercase }; specs[10] = { Layout::kFontButton(), ts10.letterSpacing, ts10.lineHeight, ts10.uppercase }; specs[11] = { Layout::kFontCaption(), ts11.letterSpacing, ts11.lineHeight, ts11.uppercase }; specs[12] = { Layout::kFontOverline(), ts12.letterSpacing, ts12.lineHeight, ts12.uppercase }; specs[13] = { Layout::kFontButtonSm(), ts10.letterSpacing, ts10.lineHeight, ts10.uppercase }; specs[14] = { Layout::kFontButtonLg(), ts10.letterSpacing, ts10.lineHeight, ts10.uppercase }; return specs; } // ============================================================================ // Typography Implementation // ============================================================================ Typography& Typography::instance() { static Typography s_instance; return s_instance; } bool Typography::reload(ImGuiIO& io, float dpiScale) { if (!loaded_) { return load(io, dpiScale); } DEBUG_LOGF("Typography: Reloading fonts for DPI scale %.2f\n", dpiScale); // Clear existing fonts and reload io.Fonts->Clear(); loaded_ = false; for (int i = 0; i < kNumStyles; ++i) fonts_[i] = nullptr; for (int i = 0; i < kNumIconSizes; ++i) iconFonts_[i] = nullptr; for (int i = 0; i < kNumIconSizes; ++i) pickaxeFonts_[i] = nullptr; return load(io, dpiScale); } // The 15 schema font sizes (TypeStyle order) that load() builds from. static void gatherFontSizes(float (&o)[15]) { o[0] = Layout::kFontH1(); o[1] = Layout::kFontH2(); o[2] = Layout::kFontH3(); o[3] = Layout::kFontH4(); o[4] = Layout::kFontH5(); o[5] = Layout::kFontH6(); o[6] = Layout::kFontSubtitle1(); o[7] = Layout::kFontSubtitle2(); o[8] = Layout::kFontBody1(); o[9] = Layout::kFontBody2(); o[10] = Layout::kFontButton(); o[11] = Layout::kFontCaption(); o[12] = Layout::kFontOverline(); o[13] = Layout::kFontButtonSm(); o[14] = Layout::kFontButtonLg(); } bool Typography::effectiveEmoji() const { #ifdef DRAGONX_HAVE_FREETYPE return color_emoji_; #else return false; // no FreeType (osxcross): the flag changes nothing, never rebuild for it #endif } bool Typography::needsRebuild() { if (!loaded_) return false; const uint32_t gen = schema::UI().generation(); if (gen == built_.gen && effectiveEmoji() == built_.emoji) return false; // O(1) per frame in steady state built_.gen = gen; float cur[15]; gatherFontSizes(cur); bool diff = effectiveEmoji() != built_.emoji || Layout::kFontScale() != built_.fontScale; for (int i = 0; i < 15 && !diff; ++i) diff = cur[i] != built_.sizes[i]; return diff; } bool Typography::load(ImGuiIO& io, float dpiScale) { if (loaded_) { DEBUG_LOGF("Typography: Already loaded\n"); return true; } dpiScale_ = dpiScale; // Multiply by dpiScale so fonts are the correct physical size. // On Windows Per-Monitor DPI v2, SDL3 coordinates are physical pixels // and DisplayFramebufferScale is 1.0 (no automatic upscaling). // The window is resized by dpiScale in main.cpp so that fonts at // size*dpiScale fit proportionally (no overflow). // Layout::userFontScale() is the user-chosen accessibility multiplier // (1.0–3.0) persisted in Settings; it makes glyphs physically larger // without any bitmap up-scaling (sharp at every size). float scale = dpiScale * Layout::kFontScale() * Layout::userFontScale(); DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, userFontScale: %.2f, combined: %.2f)\n", dpiScale, Layout::kFontScale(), Layout::userFontScale(), scale); #ifdef DRAGONX_HAVE_FREETYPE // Choose the atlas font loader BEFORE any font is added: FreeType (required to rasterize COLR // color-emoji glyphs) when color emoji is enabled, else the default stb_truetype loader. Toggling // the setting + reload() flips this cleanly. io.Fonts->SetFontLoader(color_emoji_ ? ImGuiFreeType::GetFontLoader() : ImFontAtlasGetFontLoaderForStbTruetype()); DEBUG_LOGF("Typography: font loader = %s\n", color_emoji_ ? "FreeType (color emoji)" : "stb_truetype"); #endif // For ImGui, we need to load fonts at specific pixel sizes. // Font sizes come from Layout:: accessors (backed by UISchema JSON) // 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); } // 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", false); // --- Icon fonts at multiple sizes --- // These are standalone fonts (not merged) so we can PushFont(iconXxx) for icon rendering. iconFonts_[0] = loadIconFont(io, 14.0f * scale, "IconSmall"); iconFonts_[1] = loadIconFont(io, 18.0f * scale, "IconMed"); iconFonts_[2] = loadIconFont(io, 24.0f * scale, "IconLarge"); iconFonts_[3] = loadIconFont(io, 40.0f * scale, "IconXL"); // Load a one-glyph MDI subset for pickaxe. The glyph is remapped onto a // BMP private-use codepoint so it remains renderable with 16-bit ImWchar. pickaxeFonts_[0] = loadPickaxeFont(io, 14.0f * scale, "PickaxeSmall"); pickaxeFonts_[1] = loadPickaxeFont(io, 18.0f * scale, "PickaxeMed"); pickaxeFonts_[2] = loadPickaxeFont(io, 24.0f * scale, "PickaxeLarge"); pickaxeFonts_[3] = loadPickaxeFont(io, 40.0f * scale, "PickaxeXL"); // Verify all fonts loaded bool allLoaded = true; for (int i = 0; i < kNumStyles; ++i) { if (!fonts_[i]) { DEBUG_LOGF("Typography: Warning - Font for style %d not loaded\n", i); fonts_[i] = io.Fonts->Fonts.Size > 0 ? io.Fonts->Fonts[0] : nullptr; allLoaded = false; } } // Set Body1 as the default font so that bare ImGui::Text() calls // (status bar, dialogs, notifications, etc.) render at Body1 size // instead of H1 (the first font loaded). io.FontDefault = fonts_[static_cast(TypeStyle::Body1)]; gatherFontSizes(built_.sizes); built_.fontScale = Layout::kFontScale(); built_.emoji = effectiveEmoji(); built_.gen = schema::UI().generation(); loaded_ = true; Layout::setFontAtlasScale(Layout::userFontScale()); DEBUG_LOGF("Typography: Loaded %d font styles (default=Body1, atlasScale=%.2f)\n", allLoaded ? kNumStyles : -1, Layout::fontAtlasScale()); return allLoaded; } 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; unsigned int fontDataLen = 0; switch (weight) { case kWeightLight: fontData = g_ubuntu_light_data; fontDataLen = g_ubuntu_light_size; break; case kWeightMedium: fontData = g_ubuntu_medium_data; fontDataLen = g_ubuntu_medium_size; break; case kWeightMono: fontData = g_ubuntu_mono_data; fontDataLen = g_ubuntu_mono_size; break; case kWeightRegular: default: fontData = g_ubuntu_regular_data; fontDataLen = g_ubuntu_regular_size; break; } // 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 = false; cfg.OversampleH = 2; cfg.OversampleV = 1; cfg.PixelSnapH = true; // Include default ASCII + Latin, Latin Extended (for Spanish/multilingual), // Cyrillic (for Russian), Greek, plus arrows (⇄ U+21C4), // math (≈ U+2248), general punctuation (— U+2014, … U+2026, etc.) static const ImWchar glyphRanges[] = { 0x0020, 0x00FF, // Basic Latin + Latin-1 Supplement 0x0100, 0x024F, // Latin Extended-A + Latin Extended-B (Spanish, etc.) 0x0370, 0x03FF, // Greek and Coptic 0x0400, 0x04FF, // Cyrillic (Russian, Ukrainian, etc.) 0x0500, 0x052F, // Cyrillic Supplement 0x2000, 0x206F, // General Punctuation (em dash, ellipsis, etc.) 0x2190, 0x21FF, // Arrows (includes ⇄ U+21C4, ↻ U+21BB) 0x2200, 0x22FF, // Mathematical Operators (includes ≈ U+2248) 0x2600, 0x26FF, // Miscellaneous Symbols (includes ⛏ U+26CF) 0, }; cfg.GlyphRanges = glyphRanges; // Create a unique name for this font variant const char* weightName = weight == kWeightLight ? "Light" : weight == kWeightMedium ? "Medium" : weight == kWeightMono ? "Mono" : "Regular"; snprintf(cfg.Name, sizeof(cfg.Name), "Ubuntu %s %.0fpx", weightName, size); 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) { ImFontConfig cjkCfg; cjkCfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob cjkCfg.MergeMode = true; // merge into the font we just loaded cjkCfg.OversampleH = 1; cjkCfg.OversampleV = 1; cjkCfg.PixelSnapH = true; cjkCfg.GlyphMinAdvanceX = 0; // CJK Unified Ideographs + Hiragana + Katakana + Hangul + fullwidth punctuation static const ImWchar cjkRanges[] = { 0x2E80, 0x2FDF, // CJK Radicals 0x3000, 0x30FF, // CJK Symbols, Hiragana, Katakana 0x3100, 0x312F, // Bopomofo 0x31F0, 0x31FF, // Katakana Extensions 0x3400, 0x4DBF, // CJK Extension A 0x4E00, 0x9FFF, // CJK Unified Ideographs 0xAC00, 0xD7AF, // Hangul Syllables 0xFF00, 0xFFEF, // Fullwidth Forms 0, }; cjkCfg.GlyphRanges = cjkRanges; snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size); 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); } else { DEBUG_LOGF("Typography: WARNING — CJK merge FAILED for %s (size=%u)\n", name, g_noto_cjk_subset_size); } } // 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). const unsigned char* emojiData = g_noto_emoji_subset_data; unsigned int emojiSize = g_noto_emoji_subset_size; bool colorGlyphs = false; #ifdef DRAGONX_HAVE_FREETYPE if (color_emoji_ && g_twemoji_color_size > 0) { emojiData = g_twemoji_color_data; emojiSize = g_twemoji_color_size; colorGlyphs = true; } #endif if (wantEmoji && emojiSize > 0) { ImFontConfig emojiCfg; emojiCfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob emojiCfg.MergeMode = true; // merge into the text font just loaded emojiCfg.OversampleH = 1; emojiCfg.OversampleV = 1; emojiCfg.PixelSnapH = true; emojiCfg.GlyphMinAdvanceX = 0; #ifdef DRAGONX_HAVE_FREETYPE if (colorGlyphs) emojiCfg.FontLoaderFlags |= ImGuiFreeTypeLoaderFlags_LoadColor; // render COLR in color #endif // 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), "%s %.0fpx (merge)", colorGlyphs ? "Twemoji" : "NotoEmoji", size); 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, emojiSize); } } } else { DEBUG_LOGF("Typography: Failed to load %s\n", name); } return font; } ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name) { ImFontConfig cfg; cfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob cfg.OversampleH = 2; cfg.OversampleV = 1; cfg.PixelSnapH = true; cfg.MergeMode = false; // standalone icon font cfg.GlyphMinAdvanceX = size; // monospace icons static const ImWchar iconRanges[] = { ICON_MIN_MD, ICON_MAX_16_MD, 0 }; cfg.GlyphRanges = iconRanges; snprintf(cfg.Name, sizeof(cfg.Name), "MDIcons %.0fpx", size); 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); } return font; } ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name) { if (g_mdi_pickaxe_subset_size == 0) { DEBUG_LOGF("Typography: Pickaxe subset font is empty\n"); return nullptr; } ImFontConfig cfg; cfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob cfg.OversampleH = 2; cfg.OversampleV = 1; cfg.PixelSnapH = true; cfg.MergeMode = false; cfg.GlyphMinAdvanceX = size; static const ImWchar pickaxeRange[] = { Typography::kPickaxeCodepoint, Typography::kPickaxeCodepoint, 0, }; cfg.GlyphRanges = pickaxeRange; snprintf(cfg.Name, sizeof(cfg.Name), "MDIPickaxe %.0fpx", size); 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); } return font; } ImFont* Typography::pickaxeFontForSize(float size) const { if (size <= 15.0f) return pickaxeSmall(); if (size <= 20.0f) return pickaxeMed(); if (size <= 32.0f) return pickaxeLarge(); return pickaxeXL(); } ImFont* Typography::getFont(TypeStyle style) const { int index = static_cast(style); if (index >= 0 && index < kNumStyles && fonts_[index]) { return fonts_[index]; } // Return default font if not found return ImGui::GetIO().Fonts->Fonts.Size > 0 ? ImGui::GetIO().Fonts->Fonts[0] : nullptr; } const TypeSpec& Typography::getSpec(TypeStyle style) const { const TypeSpec* specs = getTypeSpecs(); int index = static_cast(style); if (index >= 0 && index < kNumStyles) { return specs[index]; } // Return Body1 as default return specs[static_cast(TypeStyle::Body1)]; } void Typography::pushFont(TypeStyle style) const { ImGui::PushFont(getFont(style)); } void Typography::popFont() const { ImGui::PopFont(); } // ============================================================================ // Text Rendering Helpers // ============================================================================ void Typography::text(TypeStyle style, const char* text) const { const TypeSpec& spec = getSpec(style); pushFont(style); if (spec.uppercase) { // Convert to uppercase std::string upper; upper.reserve(strlen(text)); for (const char* p = text; *p; ++p) { upper += static_cast(std::toupper(static_cast(*p))); } ImGui::TextUnformatted(upper.c_str()); } else { ImGui::TextUnformatted(text); } popFont(); } void Typography::textWrapped(TypeStyle style, const char* text) const { const TypeSpec& spec = getSpec(style); pushFont(style); if (spec.uppercase) { std::string upper; upper.reserve(strlen(text)); for (const char* p = text; *p; ++p) { upper += static_cast(std::toupper(static_cast(*p))); } ImGui::TextWrapped("%s", upper.c_str()); } else { ImGui::TextWrapped("%s", text); } popFont(); } void Typography::textColored(TypeStyle style, ImU32 color, const char* text) const { const TypeSpec& spec = getSpec(style); pushFont(style); ImVec4 colorVec = ImGui::ColorConvertU32ToFloat4(color); if (spec.uppercase) { std::string upper; upper.reserve(strlen(text)); for (const char* p = text; *p; ++p) { upper += static_cast(std::toupper(static_cast(*p))); } ImGui::TextColored(colorVec, "%s", upper.c_str()); } else { ImGui::TextColored(colorVec, "%s", text); } popFont(); } } // namespace material } // namespace ui } // namespace dragonx