Files
ObsidianDragon/src/ui/material/typography.cpp
DanS ee8a08e569 feat(addresses): improve address labeling and view-only handling
- Add expanded address icon picker with search, bottom-aligned actions, and improved modal sizing
- Embed a pickaxe icon font subset and wire it into typography/address icon rendering
- Track view-only shielded addresses and prevent sends from non-spendable z-addresses
- Improve address transfer dialog sizing, max amount handling, and text clipping
- Tune main header layout values in ui.toml
- Update README, codebase overview, and third-party license documentation
2026-04-27 13:54:28 -05:00

501 lines
19 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"
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);
// 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<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 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<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