Files
ObsidianDragon/src/ui/material/typography.h
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

316 lines
12 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "imgui.h"
#include <string>
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Type Scale
// ============================================================================
// Based on https://m2.material.io/design/typography/the-type-system.html
//
// The type scale is a combination of 13 styles that are supported by the
// type system. It contains reusable categories of text, each with an
// intended application and meaning.
/**
* @brief Material Design typography style identifiers
*/
enum class TypeStyle {
H1, // 96sp Light - Large display headers
H2, // 60sp Light - Display headers
H3, // 48sp Regular - Section titles
H4, // 34sp Regular - Section titles
H5, // 24sp Regular - Card titles, dialog titles
H6, // 20sp Medium - Section headers, subtitles
Subtitle1, // 16sp Regular - Secondary information
Subtitle2, // 14sp Medium - Secondary information
Body1, // 16sp Regular - Primary body text
Body2, // 14sp Regular - Secondary body text
Button, // 14sp Medium - Button labels (uppercase)
Caption, // 12sp Regular - Timestamps, labels
Overline, // 10sp Regular - Overline labels (uppercase)
ButtonSm, // Small button labels
ButtonLg // Large button labels
};
/**
* @brief Typography specification for a type style
*/
struct TypeSpec {
float size; // Font size in sp (scaled pixels)
float letterSpacing; // Letter spacing in px (applied after scaling)
float lineHeight; // Line height multiplier
bool uppercase; // Whether text should be uppercase
// Font weight is handled by which font is used (Light/Regular/Medium)
};
/**
* @brief Typography system managing Material Design fonts
*
* Loads Ubuntu fonts in Light, Regular, and Medium weights at multiple
* sizes to implement the Material Design type scale.
*
* Usage:
* // Initialize during startup (after ImGui context)
* dragonx::ui::material::Typography::instance().load(io);
*
* // Use fonts throughout the application
* ImGui::PushFont(Typography::instance().getFont(TypeStyle::H5));
* ImGui::Text("Card Title");
* ImGui::PopFont();
*
* // Or use convenience functions
* Typography::instance().pushFont(TypeStyle::Body1);
* ImGui::TextWrapped("Body text here...");
* Typography::instance().popFont();
*/
class Typography {
public:
/**
* @brief Get the singleton instance
*/
static Typography& instance();
/**
* @brief Load all fonts for the type scale
*
* Must be called after ImGui context is created but before first frame.
*
* @param io ImGui IO reference
* @param dpiScale DPI scale factor (default 1.0)
* @return true if fonts loaded successfully
*/
bool load(ImGuiIO& io, float dpiScale = 1.0f);
/**
* @brief Reload fonts at a new DPI scale
*
* Call when the display scale changes (e.g., window moved to a different
* DPI monitor). Clears the existing font atlas and reloads all fonts.
*
* @param io ImGui IO reference
* @param dpiScale New DPI scale factor
* @return true if fonts reloaded successfully
*/
bool reload(ImGuiIO& io, float dpiScale);
/**
* @brief Check if typography system is loaded
*/
bool isLoaded() const { return loaded_; }
/**
* @brief Get the current DPI scale
*/
float getDpiScale() const { return dpiScale_; }
/**
* @brief Select color vs monochrome emoji for the next (re)load. Color needs a FreeType-enabled
* build (DRAGONX_HAVE_FREETYPE); otherwise this is inert and monochrome is always used.
* Set before load()/reload() (App does this from the chat_emoji_color setting).
*/
void setColorEmoji(bool enabled) { color_emoji_ = enabled; }
bool colorEmoji() const { return color_emoji_; }
/**
* @brief Get font for a type style
*
* @param style The Material Design type style
* @return ImFont pointer (never null, returns default if not loaded)
*/
ImFont* getFont(TypeStyle style) const;
/**
* @brief Get the spec for a type style
*
* @param style The Material Design type style
* @return TypeSpec with size, spacing, and line height
*/
const TypeSpec& getSpec(TypeStyle style) const;
/**
* @brief Push font for a type style
*
* Convenience wrapper around ImGui::PushFont(getFont(style))
*/
void pushFont(TypeStyle style) const;
/**
* @brief Pop the current font
*
* Convenience wrapper around ImGui::PopFont()
*/
void popFont() const;
// ========================================================================
// Font Accessors (for common cases)
// ========================================================================
// Headers
ImFont* h1() const { return getFont(TypeStyle::H1); }
ImFont* h2() const { return getFont(TypeStyle::H2); }
ImFont* h3() const { return getFont(TypeStyle::H3); }
ImFont* h4() const { return getFont(TypeStyle::H4); }
ImFont* h5() const { return getFont(TypeStyle::H5); }
ImFont* h6() const { return getFont(TypeStyle::H6); }
// Body text
ImFont* subtitle1() const { return getFont(TypeStyle::Subtitle1); }
ImFont* subtitle2() const { return getFont(TypeStyle::Subtitle2); }
ImFont* body1() const { return getFont(TypeStyle::Body1); }
ImFont* body2() const { return getFont(TypeStyle::Body2); }
// UI elements
ImFont* button() const { return getFont(TypeStyle::Button); }
ImFont* buttonSm() const { return getFont(TypeStyle::ButtonSm); }
ImFont* buttonLg() const { return getFont(TypeStyle::ButtonLg); }
ImFont* caption() const { return getFont(TypeStyle::Caption); }
ImFont* overline() const { return getFont(TypeStyle::Overline); }
// Monospace (Ubuntu Mono) at caption size — for the console, so JSON/columns align
// like a real terminal. Falls back to the proportional caption font if not loaded.
ImFont* mono() const { return mono_ ? mono_ : caption(); }
// Icon fonts — Material Design Icons merged at specific pixel sizes
// Use these when you need to render an icon at a specific size via AddText/ImGui::Text.
// Common sizes: iconSmall (14px), iconMed (18px), iconLarge (24px).
ImFont* iconSmall() const { return iconFonts_[0] ? iconFonts_[0] : getFont(TypeStyle::Body2); }
ImFont* iconMed() const { return iconFonts_[1] ? iconFonts_[1] : getFont(TypeStyle::Body1); }
ImFont* iconLarge() const { return iconFonts_[2] ? iconFonts_[2] : getFont(TypeStyle::H5); }
ImFont* iconXL() const { return iconFonts_[3] ? iconFonts_[3] : getFont(TypeStyle::H3); }
ImFont* pickaxeSmall() const { return pickaxeFonts_[0] ? pickaxeFonts_[0] : iconSmall(); }
ImFont* pickaxeMed() const { return pickaxeFonts_[1] ? pickaxeFonts_[1] : iconMed(); }
ImFont* pickaxeLarge() const { return pickaxeFonts_[2] ? pickaxeFonts_[2] : iconLarge(); }
ImFont* pickaxeXL() const { return pickaxeFonts_[3] ? pickaxeFonts_[3] : iconXL(); }
ImFont* pickaxeFontForSize(float size) const;
static constexpr ImWchar kPickaxeCodepoint = 0xE001;
/**
* @brief Resolve a font name string to ImFont*
* @param name Font name like "button", "button-sm", "button-lg", "h4", "body1"
* @return ImFont* pointer, or nullptr if name is empty/unknown
*/
ImFont* resolveByName(const std::string& name) const {
if (name.empty()) return nullptr;
if (name == "h1") return h1();
if (name == "h2") return h2();
if (name == "h3") return h3();
if (name == "h4") return h4();
if (name == "h5") return h5();
if (name == "h6") return h6();
if (name == "subtitle1") return subtitle1();
if (name == "subtitle2") return subtitle2();
if (name == "body1") return body1();
if (name == "body2") return body2();
if (name == "button") return button();
if (name == "button-sm") return buttonSm();
if (name == "button-lg") return buttonLg();
if (name == "caption") return caption();
if (name == "overline") return overline();
return nullptr;
}
// ========================================================================
// Text Rendering Helpers
// ========================================================================
/**
* @brief Render text with a specific type style
*
* Handles font push/pop and optional uppercase transformation.
*
* @param style The type style to use
* @param text The text to render
*/
void text(TypeStyle style, const char* text) const;
/**
* @brief Render wrapped text with a specific type style
*
* @param style The type style to use
* @param text The text to render
*/
void textWrapped(TypeStyle style, const char* text) const;
/**
* @brief Render colored text with a specific type style
*
* @param style The type style to use
* @param color Text color
* @param text The text to render
*/
void textColored(TypeStyle style, ImU32 color, const char* text) const;
private:
Typography() = default;
~Typography() = default;
Typography(const Typography&) = delete;
Typography& operator=(const Typography&) = delete;
// Load fonts at a specific size with a specific weight
ImFont* loadFont(ImGuiIO& io, int weight, float size, const char* name);
// Font weight constants
static constexpr int kWeightLight = 300;
static constexpr int kWeightRegular = 400;
static constexpr int kWeightMedium = 500;
static constexpr int kWeightMono = 401; // selects Ubuntu Mono (monospace, regular)
bool loaded_ = false;
float dpiScale_ = 1.0f;
bool color_emoji_ = false; // when true + FreeType present, merge the COLR color-emoji font
// Fonts for each type style
ImFont* fonts_[15] = {};
// Monospace font (Ubuntu Mono) at caption size — console/terminal text.
ImFont* mono_ = nullptr;
// Icon fonts at different sizes: [0]=small(14), [1]=med(18), [2]=large(24), [3]=xl(40)
ImFont* iconFonts_[4] = {};
ImFont* pickaxeFonts_[4] = {};
static constexpr int kNumIconSizes = 4;
// Load an icon-only font at a specific pixel size
ImFont* loadIconFont(ImGuiIO& io, float size, const char* name);
ImFont* loadPickaxeFont(ImGuiIO& io, float size, const char* name);
// Type specifications
static const TypeSpec* getTypeSpecs();
static constexpr int kNumStyles = 15;
};
// ============================================================================
// Convenience Macros (Optional)
// ============================================================================
// Scoped font push/pop using RAII
class ScopedFont {
public:
explicit ScopedFont(TypeStyle style) {
Typography::instance().pushFont(style);
}
~ScopedFont() {
Typography::instance().popFont();
}
};
// Usage: MATERIAL_FONT(H5) { ImGui::Text("Title"); }
#define MATERIAL_FONT(style) \
if (dragonx::ui::material::ScopedFont _font{dragonx::ui::material::TypeStyle::style}; true)
} // namespace material
} // namespace ui
} // namespace dragonx