// 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" 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() { // Rebuild each call to pick up hot-reloaded values static TypeSpec specs[kNumStyles]; // Helper: read typography entry from UISchema auto& S = schema::UI(); auto tspec = [&](const char* key, float defLS, float defLH, bool defUpper) -> TypeSpec { auto e = S.drawElement("typography", key); 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); } 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); // 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"); } // 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"); // --- 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)]; 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) { // 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 kWeightRegular: default: fontData = g_ubuntu_regular_data; fontDataLen = g_ubuntu_regular_size; break; } // ImGui needs to own a copy of the font data void* fontDataCopy = IM_ALLOC(fontDataLen); memcpy(fontDataCopy, fontData, fontDataLen); ImFontConfig cfg; cfg.FontDataOwnedByAtlas = true; // ImGui will free this 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 snprintf(cfg.Name, sizeof(cfg.Name), "Ubuntu %s %.0fpx", weight == kWeightLight ? "Light" : weight == kWeightMedium ? "Medium" : "Regular", size); ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, 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.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(cjkCopy, 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); } } } else { DEBUG_LOGF("Typography: Failed to load %s\n", name); IM_FREE(fontDataCopy); } return font; } 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.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(fontDataCopy, 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; } 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; } 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.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(fontDataCopy, 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; } 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