Files
ObsidianDragon/src/ui/material/typography.cpp
DanS a78b44246e build(freetype): color-emoji rendering via a cross-platform FreeType backend
The chat "Emoji style: Color" option renders the merged emoji in color (COLR/CPAL
Twemoji) instead of the monochrome NotoEmoji subset. This needs FreeType, which the
default stb_truetype rasterizer can't do for color glyphs.

- Vendor imgui_freetype (matches the bundled 1.92 ImFontLoader API) and embed a 1.4 MB
  COLRv0 Twemoji font (no libpng/harfbuzz needed).
- CMake gains an optional FreeType path: native Linux/macOS use the system FreeType via
  find_package; the mingw-w64 cross-compile has none, so build.sh --win-release now
  cross-builds a minimal static FreeType (scripts/build-freetype-mingw.sh) and passes it
  in. Absent FreeType => graceful monochrome fallback, so no build breaks.
- Typography selects the FreeType loader + the color font (LoadColor) when color emoji is
  on, else the stb loader + mono subset; toggling reloads the atlas. Both the DX11 and
  OpenGL backends already support the 1.92 RGBA dynamic atlas, so color glyphs render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:44:19 -05:00

580 lines
23 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()
{
// 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.03.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)
// 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");
// 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");
// --- 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)];
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 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;
}
// 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
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(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);
}
}
// 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 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) {
void* emojiCopy = IM_ALLOC(emojiSize);
memcpy(emojiCopy, emojiData, emojiSize);
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;
#ifdef DRAGONX_HAVE_FREETYPE
if (colorGlyphs) emojiCfg.FontLoaderFlags |= ImGuiFreeTypeLoaderFlags_LoadColor; // render COLR in color
#endif
// The base Ubuntu font already owns U+260026FF 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(emojiCopy, 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);
}
}
} 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<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