perf(typography): use font blobs in place and share identical faces
Typography copied every font blob into a fresh heap buffer for each face it added (IM_ALLOC + memcpy, FontDataOwnedByAtlas = true): the Ubuntu weight, the CJK subset merge, the emoji merge, and the icon and pickaxe fonts. The blobs are INCBIN symbols in read-only memory that live for the whole process, so the copies (16 faces, each with its own CJK copy and, for 8 of them, an emoji copy) only cost memory. Styles with the same weight and size also built separate, identical ImFonts. Now: - every font is added straight from the embedded data with FontDataOwnedByAtlas = false. ImGui 1.92 frees only data it owns, so the blobs survive atlas clears and DPI or skin reloads; - load() walks a style table and shares the ImFont between styles with the same (weight, scaled size, emoji) key. Emoji is part of the key, so no style gains or loses the emoji fallback. With the current ui.toml sizes H3/H4, H6/ButtonLg and Body1/Overline share a face: 13 ImFonts instead of 16 (12 text faces plus Mono), CJK merged 13 times instead of 16 and emoji 7 times instead of 8. Measured on the Lite build, RssAnon 15 s after start, median of 3 runs: - color emoji: 108.6 -> 78.8 MiB (-29.8 MiB); - monochrome emoji: 102.8 -> 78.6 MiB (-24.2 MiB). RssFile is also 2.4-5.1 MiB lower, because the blobs are no longer read end to end for copying. Full screenshot sweeps (610 screens per run; color and mono emoji at 1280x720 in English and Japanese, color at 2560x1440) match the previous build within run-to-run noise. The only content differences are live values such as the mining tab's RAM readout (233 MB before, 201 MB after). Also fixed: when AddFontFromMemoryTTF failed, ImGui had already freed the owned copy and the caller freed it again, a latent double free; those IM_FREE calls are gone. The emoji-merge failure log now prints the blob size instead of passing the float font size to %u. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -151,64 +151,40 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
|||||||
// For ImGui, we need to load fonts at specific pixel sizes.
|
// For ImGui, we need to load fonts at specific pixel sizes.
|
||||||
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
||||||
|
|
||||||
// Load fonts in order of TypeStyle enum
|
// 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; };
|
||||||
// H1: Light
|
const StyleDef defs[kNumStyles] = {
|
||||||
fonts_[0] = loadFont(io, kWeightLight, Layout::kFontH1() * scale, "H1");
|
{ kWeightLight, Layout::kFontH1(), "H1", false },
|
||||||
|
{ kWeightLight, Layout::kFontH2(), "H2", false },
|
||||||
// H2: Light
|
{ kWeightRegular, Layout::kFontH3(), "H3", false },
|
||||||
fonts_[1] = loadFont(io, kWeightLight, Layout::kFontH2() * scale, "H2");
|
{ kWeightRegular, Layout::kFontH4(), "H4", false },
|
||||||
|
{ kWeightRegular, Layout::kFontH5(), "H5", false },
|
||||||
// H3: Regular
|
{ kWeightMedium, Layout::kFontH6(), "H6", false },
|
||||||
fonts_[2] = loadFont(io, kWeightRegular, Layout::kFontH3() * scale, "H3");
|
{ kWeightRegular, Layout::kFontSubtitle1(), "Subtitle1", true },
|
||||||
|
{ kWeightMedium, Layout::kFontSubtitle2(), "Subtitle2", true },
|
||||||
// H4: Regular
|
{ kWeightRegular, Layout::kFontBody1(), "Body1", true },
|
||||||
fonts_[3] = loadFont(io, kWeightRegular, Layout::kFontH4() * scale, "H4");
|
{ kWeightRegular, Layout::kFontBody2(), "Body2", true },
|
||||||
|
{ kWeightMedium, Layout::kFontButton(), "Button", true },
|
||||||
// H5: Regular
|
{ kWeightRegular, Layout::kFontCaption(), "Caption", true },
|
||||||
fonts_[4] = loadFont(io, kWeightRegular, Layout::kFontH5() * scale, "H5");
|
{ kWeightRegular, Layout::kFontOverline(), "Overline", true },
|
||||||
|
{ kWeightMedium, Layout::kFontButtonSm(), "ButtonSm", true },
|
||||||
// H6: Medium
|
{ kWeightMedium, Layout::kFontButtonLg(), "ButtonLg", false },
|
||||||
fonts_[5] = loadFont(io, kWeightMedium, Layout::kFontH6() * scale, "H6");
|
};
|
||||||
|
static_assert(sizeof(fonts_) / sizeof(fonts_[0]) == kNumStyles, "style table must cover every TypeStyle");
|
||||||
// Subtitle1: Regular
|
// Styles with the same (weight, scaled size, emoji) would build byte-identical ImFonts: load once and share
|
||||||
fonts_[6] = loadFont(io, kWeightRegular, Layout::kFontSubtitle1() * scale, "Subtitle1");
|
// 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()).
|
||||||
// Subtitle2: Medium
|
for (int i = 0; i < kNumStyles; ++i) {
|
||||||
fonts_[7] = loadFont(io, kWeightMedium, Layout::kFontSubtitle2() * scale, "Subtitle2");
|
const float px = defs[i].px * scale;
|
||||||
|
int first = -1;
|
||||||
// Body1: Regular (shares with Subtitle1 if same size)
|
for (int j = 0; j < i; ++j)
|
||||||
if (Layout::kFontBody1() == Layout::kFontSubtitle1()) {
|
if (defs[j].weight == defs[i].weight && defs[j].px * scale == px && defs[j].emoji == defs[i].emoji) { first = j; break; }
|
||||||
fonts_[8] = fonts_[6]; // Reuse Subtitle1 font
|
fonts_[i] = (first >= 0) ? fonts_[first] : loadFont(io, defs[i].weight, px, defs[i].name, defs[i].emoji);
|
||||||
} 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 +
|
// 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.
|
// pretty-printed JSON columns align. Same size as Caption to keep line heights matched.
|
||||||
mono_ = loadFont(io, kWeightMono, Layout::kFontCaption() * scale, "Mono");
|
mono_ = loadFont(io, kWeightMono, Layout::kFontCaption() * scale, "Mono", false);
|
||||||
|
|
||||||
// --- Icon fonts at multiple sizes ---
|
// --- Icon fonts at multiple sizes ---
|
||||||
// These are standalone fonts (not merged) so we can PushFont(iconXxx) for icon rendering.
|
// These are standalone fonts (not merged) so we can PushFont(iconXxx) for icon rendering.
|
||||||
@@ -247,7 +223,7 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
|||||||
return allLoaded;
|
return allLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* name)
|
ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* name, bool wantEmoji)
|
||||||
{
|
{
|
||||||
// Select font data based on weight
|
// Select font data based on weight
|
||||||
const unsigned char* fontData = nullptr;
|
const unsigned char* fontData = nullptr;
|
||||||
@@ -273,12 +249,10 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImGui needs to own a copy of the font data
|
// Zero-copy: the INCBIN blob is immutable .rodata that outlives the atlas (ImGui 1.92 requires the data to
|
||||||
void* fontDataCopy = IM_ALLOC(fontDataLen);
|
// persist for the atlas lifetime); FontDataOwnedByAtlas=false means ImGui never frees or writes it.
|
||||||
memcpy(fontDataCopy, fontData, fontDataLen);
|
|
||||||
|
|
||||||
ImFontConfig cfg;
|
ImFontConfig cfg;
|
||||||
cfg.FontDataOwnedByAtlas = true; // ImGui will free this
|
cfg.FontDataOwnedByAtlas = false;
|
||||||
cfg.OversampleH = 2;
|
cfg.OversampleH = 2;
|
||||||
cfg.OversampleV = 1;
|
cfg.OversampleV = 1;
|
||||||
cfg.PixelSnapH = true;
|
cfg.PixelSnapH = true;
|
||||||
@@ -306,18 +280,15 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
|||||||
weight == kWeightMono ? "Mono" : "Regular";
|
weight == kWeightMono ? "Mono" : "Regular";
|
||||||
snprintf(cfg.Name, sizeof(cfg.Name), "Ubuntu %s %.0fpx", weightName, size);
|
snprintf(cfg.Name, sizeof(cfg.Name), "Ubuntu %s %.0fpx", weightName, size);
|
||||||
|
|
||||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, fontDataLen, size, &cfg);
|
ImFont* font = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(fontData), static_cast<int>(fontDataLen), size, &cfg);
|
||||||
|
|
||||||
if (font) {
|
if (font) {
|
||||||
DEBUG_LOGF("Typography: Loaded %s (%.0fpx) as '%s'\n", name, size, cfg.Name);
|
DEBUG_LOGF("Typography: Loaded %s (%.0fpx) as '%s'\n", name, size, cfg.Name);
|
||||||
|
|
||||||
// Merge CJK fallback glyphs (Chinese/Japanese/Korean) from subset font
|
// Merge CJK fallback glyphs (Chinese/Japanese/Korean) from subset font
|
||||||
if (g_noto_cjk_subset_size > 0) {
|
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;
|
ImFontConfig cjkCfg;
|
||||||
cjkCfg.FontDataOwnedByAtlas = true;
|
cjkCfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob
|
||||||
cjkCfg.MergeMode = true; // merge into the font we just loaded
|
cjkCfg.MergeMode = true; // merge into the font we just loaded
|
||||||
cjkCfg.OversampleH = 1;
|
cjkCfg.OversampleH = 1;
|
||||||
cjkCfg.OversampleV = 1;
|
cjkCfg.OversampleV = 1;
|
||||||
@@ -338,7 +309,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
|||||||
cjkCfg.GlyphRanges = cjkRanges;
|
cjkCfg.GlyphRanges = cjkRanges;
|
||||||
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
|
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
|
||||||
|
|
||||||
ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
|
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) {
|
if (mergeResult) {
|
||||||
DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n",
|
DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n",
|
||||||
g_noto_cjk_subset_size, name);
|
g_noto_cjk_subset_size, name);
|
||||||
@@ -348,15 +320,9 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge monochrome emoji for chat + user text (Q12). Only into the small text fonts — baking
|
// Emoji (monochrome subset, or COLR color via FreeType) is merged only into the small text styles; load()
|
||||||
// ~1400 emoji at heading sizes would bloat the atlas for glyphs no heading needs. ImGui does no
|
// passes `wantEmoji` from its style table (dedup keys on it). ImGui does no shaping (no ZWJ/flags); the
|
||||||
// shaping, so single-codepoint emoji render but ZWJ sequences / flags won't compose. The
|
// >0xFFFF ranges need IMGUI_USE_WCHAR32.
|
||||||
// >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
|
// 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).
|
// is a FreeType build, else the monochrome subset (default / non-FreeType path).
|
||||||
@@ -371,11 +337,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (wantEmoji && emojiSize > 0) {
|
if (wantEmoji && emojiSize > 0) {
|
||||||
void* emojiCopy = IM_ALLOC(emojiSize);
|
|
||||||
memcpy(emojiCopy, emojiData, emojiSize);
|
|
||||||
|
|
||||||
ImFontConfig emojiCfg;
|
ImFontConfig emojiCfg;
|
||||||
emojiCfg.FontDataOwnedByAtlas = true;
|
emojiCfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob
|
||||||
emojiCfg.MergeMode = true; // merge into the text font just loaded
|
emojiCfg.MergeMode = true; // merge into the text font just loaded
|
||||||
emojiCfg.OversampleH = 1;
|
emojiCfg.OversampleH = 1;
|
||||||
emojiCfg.OversampleV = 1;
|
emojiCfg.OversampleV = 1;
|
||||||
@@ -396,17 +359,17 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
|||||||
snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "%s %.0fpx (merge)",
|
snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "%s %.0fpx (merge)",
|
||||||
colorGlyphs ? "Twemoji" : "NotoEmoji", size);
|
colorGlyphs ? "Twemoji" : "NotoEmoji", size);
|
||||||
|
|
||||||
ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(emojiCopy, emojiSize, size, &emojiCfg);
|
ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(emojiData),
|
||||||
|
static_cast<int>(emojiSize), size, &emojiCfg);
|
||||||
if (emojiMerge) {
|
if (emojiMerge) {
|
||||||
DEBUG_LOGF("Typography: Merged %s emoji (%u bytes) into %s OK\n",
|
DEBUG_LOGF("Typography: Merged %s emoji (%u bytes) into %s OK\n",
|
||||||
colorGlyphs ? "color" : "mono", emojiSize, name);
|
colorGlyphs ? "color" : "mono", emojiSize, name);
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, size);
|
DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, emojiSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOGF("Typography: Failed to load %s\n", name);
|
DEBUG_LOGF("Typography: Failed to load %s\n", name);
|
||||||
IM_FREE(fontDataCopy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return font;
|
return font;
|
||||||
@@ -414,12 +377,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
|||||||
|
|
||||||
ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name)
|
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;
|
ImFontConfig cfg;
|
||||||
cfg.FontDataOwnedByAtlas = true;
|
cfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob
|
||||||
cfg.OversampleH = 2;
|
cfg.OversampleH = 2;
|
||||||
cfg.OversampleV = 1;
|
cfg.OversampleV = 1;
|
||||||
cfg.PixelSnapH = true;
|
cfg.PixelSnapH = true;
|
||||||
@@ -431,12 +390,11 @@ ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name)
|
|||||||
|
|
||||||
snprintf(cfg.Name, sizeof(cfg.Name), "MDIcons %.0fpx", size);
|
snprintf(cfg.Name, sizeof(cfg.Name), "MDIcons %.0fpx", size);
|
||||||
|
|
||||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_material_icons_size, size, &cfg);
|
ImFont* font = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(g_material_icons_data), static_cast<int>(g_material_icons_size), size, &cfg);
|
||||||
if (font) {
|
if (font) {
|
||||||
DEBUG_LOGF("Typography: Loaded icon font %s (%.0fpx)\n", name, size);
|
DEBUG_LOGF("Typography: Loaded icon font %s (%.0fpx)\n", name, size);
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOGF("Typography: Failed to load icon font %s\n", name);
|
DEBUG_LOGF("Typography: Failed to load icon font %s\n", name);
|
||||||
IM_FREE(fontDataCopy);
|
|
||||||
}
|
}
|
||||||
return font;
|
return font;
|
||||||
}
|
}
|
||||||
@@ -448,11 +406,8 @@ ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name)
|
|||||||
return nullptr;
|
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;
|
ImFontConfig cfg;
|
||||||
cfg.FontDataOwnedByAtlas = true;
|
cfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob
|
||||||
cfg.OversampleH = 2;
|
cfg.OversampleH = 2;
|
||||||
cfg.OversampleV = 1;
|
cfg.OversampleV = 1;
|
||||||
cfg.PixelSnapH = true;
|
cfg.PixelSnapH = true;
|
||||||
@@ -468,12 +423,11 @@ ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name)
|
|||||||
|
|
||||||
snprintf(cfg.Name, sizeof(cfg.Name), "MDIPickaxe %.0fpx", size);
|
snprintf(cfg.Name, sizeof(cfg.Name), "MDIPickaxe %.0fpx", size);
|
||||||
|
|
||||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_mdi_pickaxe_subset_size, size, &cfg);
|
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) {
|
if (font) {
|
||||||
DEBUG_LOGF("Typography: Loaded pickaxe font %s (%.0fpx)\n", name, size);
|
DEBUG_LOGF("Typography: Loaded pickaxe font %s (%.0fpx)\n", name, size);
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOGF("Typography: Failed to load pickaxe font %s\n", name);
|
DEBUG_LOGF("Typography: Failed to load pickaxe font %s\n", name);
|
||||||
IM_FREE(fontDataCopy);
|
|
||||||
}
|
}
|
||||||
return font;
|
return font;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ private:
|
|||||||
Typography& operator=(const Typography&) = delete;
|
Typography& operator=(const Typography&) = delete;
|
||||||
|
|
||||||
// Load fonts at a specific size with a specific weight
|
// Load fonts at a specific size with a specific weight
|
||||||
ImFont* loadFont(ImGuiIO& io, int weight, float size, const char* name);
|
ImFont* loadFont(ImGuiIO& io, int weight, float size, const char* name, bool wantEmoji);
|
||||||
|
|
||||||
// Font weight constants
|
// Font weight constants
|
||||||
static constexpr int kWeightLight = 300;
|
static constexpr int kWeightLight = 300;
|
||||||
|
|||||||
Reference in New Issue
Block a user