Files
ObsidianDragon/src/ui/material/typography.cpp
T
DanSandClaude Opus 5.5 b50d5ac31f perf(startup): build the font atlas once at the final ui.toml sizes
main() built the font atlas before App::init() loaded ui.toml, so the
first build used the hard-coded fallback sizes (H1 24 px, Body2 12 px).
App::init() then rebuilt for a saved font scale above 1.0, still before
the schema load and so still at the fallback sizes. Only the color-emoji
rebuild requested for the first frame reached the ui.toml sizes, so
color-emoji users built the atlas twice at font scale 1.0 and three
times at 1.5.

Users with chat_emoji_color=false never got that last rebuild. They
built the atlas once or twice and ran the whole session at the fallback
sizes, e.g. Body2 at 12 px instead of 15 and H1 at 24 px instead of 20.

Now:
- App::loadBaseUiSchema(), the ui.toml load moved out of App::init()
  unchanged, runs in main() before both InitImGui paths (DX11 and GL),
  together with the saved font scale and emoji style from the early
  settings read. It is idempotent, so the call left in App::init() is a
  no-op;
- the two rebuild triggers in App::init() are gone;
- Typography records what the atlas was built from (the 15 schema sizes,
  the schema's font scale, the effective emoji style and the schema
  generation), and preFrame() rebuilds once when they differ. While the
  schema generation and the emoji style are unchanged this costs two
  compares per frame.

Measured on the Lite build (color and mono emoji, font scale 1.0 and
1.5, English and Japanese): every run builds the atlas exactly once, at
the ui.toml sizes times the scale (H1 20 px and Body2 15 px at 1.0,
30 px and 22 px at 1.5), and ui.toml is loaded before the window and GL
context are created. The packaged Lite zip and AppImage log one atlas
build instead of two. Color-emoji screenshot sweeps match the previous
build within noise.

Visible changes:
- users with chat_emoji_color=false had been rendering the whole UI at
  the fallback font sizes; they now get the designed ui.toml sizes, so
  every mono-emoji screen differs from before and matches the
  color-emoji layout;
- a user skin whose [fonts] differ from ui.toml now rebuilds the atlas
  once when it is activated or left; on a runtime switch the atlas used
  to keep the old sizes until a restart. With a test skin that sets
  Body2 to 18 px, startup builds twice and ends at 18 px; switching to a
  bundled skin rebuilds once, switching among the 9 bundled skins
  rebuilds nothing (they carry an identical copy of [fonts]), and
  switching back rebuilds once;
- editing ui.toml while running still gives exactly one rebuild.

Not verifiable here: the Windows DX11 path with the schema loaded before
its first FreeType build, macOS, and the interactive font-scale slider,
Alt+scroll and emoji toggle, which were checked by reading the code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:05:07 -05:00

578 lines
25 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <cstring>
#include <cstdio>
#include <cctype>
#include <string>
// 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<int>(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<unsigned char*>(fontData), static_cast<int>(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<unsigned char*>(g_noto_cjk_subset_data),
static_cast<int>(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<unsigned char*>(emojiData),
static_cast<int>(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<unsigned char*>(g_material_icons_data), static_cast<int>(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<unsigned char*>(g_mdi_pickaxe_subset_data), static_cast<int>(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<int>(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<int>(style);
if (index >= 0 && index < kNumStyles) {
return specs[index];
}
// Return Body1 as default
return specs[static_cast<int>(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<char>(std::toupper(static_cast<unsigned char>(*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<char>(std::toupper(static_cast<unsigned char>(*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<char>(std::toupper(static_cast<unsigned char>(*p)));
}
ImGui::TextColored(colorVec, "%s", upper.c_str());
} else {
ImGui::TextColored(colorVec, "%s", text);
}
popFont();
}
} // namespace material
} // namespace ui
} // namespace dragonx