/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "FontManager.h" #include "graphics/Font.h" #include "i18n/L10n.h" #include "lib/file/vfs/vfs_path.h" #include "lib/posix/posix.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/CStrInternStatic.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Profiler2.h" #include "ps/strings/StringBuilder.h" #include "renderer/backend/IDeviceCommandContext.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct FontSpec { std::string type; bool bold{false}; bool italic{false}; bool stroke{false}; int size{0}; }; FontSpec ParseFontSpec(const std::string& spec) { // Regex breakdown: // ^([^\\-]+) → capture fontType (one or more non-'-') // (?:-(bold|italic))? → optional "-bold" or "-italic" // (?:-(stroke))? → optional "-stroke" // -([0-9]+)$ → "-" then fontSize digits at end // examples: // "Roboto-italic-stroke-24", // "OpenSans-bold-32", // "Arial-stroke-16", // "Lato-14" static const std::regex pattern{R"(^([^\-]+)(?:-(bold|italic))?(?:-(stroke))?-([0-9]+)$)", std::regex::icase}; std::smatch m; if (!std::regex_match(spec, m, pattern)) { LOGERROR("Invalid font specification: %s", spec.c_str()); return {}; } FontSpec fs; fs.type = m[1].str(); if (m[2].matched) { std::string style = m[2].str(); if (strcasecmp(style.c_str(), "bold") == 0) fs.bold = true; else if (strcasecmp(style.c_str(), "italic") == 0) fs.italic = true; } if (m[3].matched) fs.stroke = true; fs.size = std::stoi(m[4].str()); return fs; } } // namespace CFontManager::CFontManager() : m_GUIScaleHook{std::make_unique(g_ConfigDB.RegisterHookAndCall( "gui.scale", [this]() { m_GUIScale = g_ConfigDB.Get("gui.scale", 1.0f); }))} { FT_Library lib; FT_Error error{FT_Init_FreeType(&lib)}; if (error) throw std::runtime_error{"Failed to initialize FreeType " + std::to_string(error)}; m_FreeType.reset(lib); m_GammaCorrectionLUT = std::make_unique>(); std::generate(m_GammaCorrectionLUT->begin(), m_GammaCorrectionLUT->end(), [i = 0]() mutable { return std::pow((i++) / 255.0f, 1.0f / GAMMA_CORRECTION); }); } CFontManager::~CFontManager() = default; CFont* CFontManager::LoadFont(CStrIntern fontName, CStrIntern locale) { const std::string localeToUse{[&] { if (!locale.empty()) return locale.string(); if (g_L10n.GetCurrentLocale() == icu::Locale::getUS()) return std::string{}; // Use the current locale, but not US English. return g_L10n.GetCurrentLocaleString(); } () }; // fmt::format_to_n is expensive for frequent LoadFont calls, parsing the // format string takes a noticeable amount of time. char buffer[128]; PS::StringBuilder fontNameBuilder{{std::begin(buffer), std::end(buffer)}}; fontNameBuilder.Append(localeToUse); fontNameBuilder.Append(fontName.string()); fontNameBuilder.Append('-'); fontNameBuilder.Append(m_GUIScale); CStrIntern localeFontName{fontNameBuilder.Str()}; FontsMap::iterator it{m_Fonts.find(localeFontName)}; if (it != m_Fonts.end()) return &it->second; // TODO: use hooks or something to hotrealoding default font. const std::string defaultFont{g_ConfigDB.Get("fonts.default", std::string{})}; if (defaultFont.empty()) { LOGERROR("Default font not set in config"); return nullptr; } // FontName contain the format fontType(-fontBold|fontItalic)(-fontStroke)-fontSize. // We are going to split it to get the fontType and fontSize. FontSpec fontSpec{ParseFontSpec(fontName.string())}; if (fontSpec.type.empty()) { LOGERROR("Failed to parse font specification: %s, using default font", fontName.string().c_str()); fontSpec = ParseFontSpec(str_sans_10.string()); } // Check for font configuration or fallback. const std::map fontToSearch{[&] { std::vector candidateFonts; // 3 types * 2 (bold, italic). candidateFonts.reserve(6); // TODO: explicit Locale like RTL or Arabic fonts. // 1. Locale-specific fonts first if (!localeToUse.empty()) { if (fontSpec.bold) candidateFonts.push_back(fmt::format("fonts.{}.{}.bold", localeToUse, fontSpec.type)); if (fontSpec.italic) candidateFonts.push_back(fmt::format("fonts.{}.{}.italic", localeToUse, fontSpec.type)); candidateFonts.push_back(fmt::format("fonts.{}.{}.regular", localeToUse, fontSpec.type)); } // 2. Then global fonts if (fontSpec.bold) candidateFonts.push_back(fmt::format("fonts.{}.bold", fontSpec.type)); if (fontSpec.italic) candidateFonts.push_back(fmt::format("fonts.{}.italic", fontSpec.type)); candidateFonts.push_back(fmt::format("fonts.{}.regular", fontSpec.type)); for (const std::string& key : candidateFonts) { std::map value{g_ConfigDB.GetValuesWithPrefix(CFG_COMMAND, key)}; std::map::iterator item{value.find(key)}; if (item != value.end() && !item->second.empty()) return value; } // Fallback to default. return g_ConfigDB.GetValuesWithPrefix(CFG_COMMAND, defaultFont); }() }; CFont font{this->m_FreeType.get(), *m_GammaCorrectionLUT}; if (!font.SetFontParams(localeFontName.string(), fontSpec.size, fontSpec.stroke ? 1.0f : 0.0f, m_GUIScale)) { LOGERROR("Failed to set font params for %s", localeFontName.string().c_str()); return nullptr; } const VfsPath path(L"fonts/"); for (const std::pair& configPair : fontToSearch) { for (const CStr& fontPath : configPair.second) { const VfsPath fntPath{path / fontPath}; if (!VfsFileExists(fntPath)) { LOGERROR("Font file %s not found", fontPath.c_str()); return nullptr; } if (!font.AddFontFromPath(fntPath)) { LOGERROR("Failed to load font %s", fntPath.string8()); return nullptr; } } } // Preload the common characters for visual quality. // Common characters are: Latin, numbers, punctuation. std::string_view glypshSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,;:!?\"'()[]{}<>-+=_@#$%^&*`~\\|/"; for (const char c : glypshSet) font.GetGlyph(c); return &m_Fonts.insert_or_assign(localeFontName, std::move(font)).first->second; } void CFontManager::UploadAtlasTexturesToGPU(Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { PROFILE2("Loading font textures"); GPU_SCOPED_LABEL(deviceCommandContext, "Loading font textures"); for (auto& [fontName, font] : m_Fonts) font.UploadAtlasTextureToGPU(deviceCommandContext); }