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>
578 lines
25 KiB
C++
578 lines
25 KiB
C++
// 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
|