mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 20:40:04 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			386 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			386 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include <scwx/qt/manager/font_manager.hpp>
 | |
| #include <scwx/util/logger.hpp>
 | |
| 
 | |
| #include <filesystem>
 | |
| #include <fstream>
 | |
| 
 | |
| #include <QFile>
 | |
| #include <QFileInfo>
 | |
| #include <QStandardPaths>
 | |
| #include <boost/container_hash/hash.hpp>
 | |
| #include <boost/unordered/unordered_flat_map.hpp>
 | |
| #include <fontconfig/fontconfig.h>
 | |
| 
 | |
| namespace scwx
 | |
| {
 | |
| namespace qt
 | |
| {
 | |
| namespace manager
 | |
| {
 | |
| 
 | |
| static const std::string logPrefix_ = "scwx::qt::manager::font_manager";
 | |
| static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 | |
| 
 | |
| static const std::string kFcTrueType_ {"TrueType"};
 | |
| 
 | |
| struct FontRecord
 | |
| {
 | |
|    std::string family_ {};
 | |
|    std::string style_ {};
 | |
|    std::string filename_ {};
 | |
| };
 | |
| 
 | |
| typedef std::pair<FontRecord, units::font_size::pixels<int>> FontRecordPair;
 | |
| 
 | |
| template<class Key>
 | |
| struct FontRecordHash;
 | |
| 
 | |
| template<>
 | |
| struct FontRecordHash<FontRecordPair>
 | |
| {
 | |
|    size_t operator()(const FontRecordPair& x) const;
 | |
| };
 | |
| 
 | |
| class FontManager::Impl
 | |
| {
 | |
| public:
 | |
|    explicit Impl()
 | |
|    {
 | |
|       InitializeFontCache();
 | |
|       InitializeFontconfig();
 | |
|    }
 | |
|    ~Impl() { FinalizeFontconfig(); }
 | |
| 
 | |
|    void FinalizeFontconfig();
 | |
|    void InitializeFontCache();
 | |
|    void InitializeFontconfig();
 | |
| 
 | |
|    const std::vector<std::uint8_t>& GetRawFontData(const std::string& filename);
 | |
| 
 | |
|    static FontRecord MatchFontFile(const std::string&              family,
 | |
|                                    const std::vector<std::string>& styles);
 | |
| 
 | |
|    std::string fontCachePath_ {};
 | |
| 
 | |
|    std::shared_mutex imguiFontAtlasMutex_ {};
 | |
| 
 | |
|    std::uint64_t imguiFontsBuildCount_ {};
 | |
| 
 | |
|    boost::unordered_flat_map<FontRecordPair,
 | |
|                              std::shared_ptr<types::ImGuiFont>,
 | |
|                              FontRecordHash<FontRecordPair>>
 | |
|                      imguiFonts_ {};
 | |
|    std::shared_mutex imguiFontsMutex_ {};
 | |
| 
 | |
|    boost::unordered_flat_map<std::string, std::vector<std::uint8_t>>
 | |
|               rawFontData_ {};
 | |
|    std::mutex rawFontDataMutex_ {};
 | |
| 
 | |
|    std::shared_ptr<types::ImGuiFont> defaultFont_ {};
 | |
|    boost::unordered_flat_map<types::FontCategory,
 | |
|                              std::shared_ptr<types::ImGuiFont>>
 | |
|               fontCategoryMap_ {};
 | |
|    std::mutex fontCategoryMutex_ {};
 | |
| };
 | |
| 
 | |
| FontManager::FontManager() : p(std::make_unique<Impl>()) {}
 | |
| 
 | |
| FontManager::~FontManager() {};
 | |
| 
 | |
| std::shared_mutex& FontManager::imgui_font_atlas_mutex()
 | |
| {
 | |
|    return p->imguiFontAtlasMutex_;
 | |
| }
 | |
| 
 | |
| std::uint64_t FontManager::imgui_fonts_build_count() const
 | |
| {
 | |
|    return p->imguiFontsBuildCount_;
 | |
| }
 | |
| 
 | |
| std::shared_ptr<types::ImGuiFont>
 | |
| FontManager::GetImGuiFont(types::FontCategory fontCategory)
 | |
| {
 | |
|    std::unique_lock lock {p->fontCategoryMutex_};
 | |
| 
 | |
|    auto it = p->fontCategoryMap_.find(fontCategory);
 | |
|    if (it != p->fontCategoryMap_.cend())
 | |
|    {
 | |
|       return it->second;
 | |
|    }
 | |
| 
 | |
|    return p->defaultFont_;
 | |
| }
 | |
| 
 | |
| std::shared_ptr<types::ImGuiFont>
 | |
| FontManager::LoadImGuiFont(const std::string&               family,
 | |
|                            const std::vector<std::string>&  styles,
 | |
|                            units::font_size::points<double> size,
 | |
|                            bool                             loadIfNotFound)
 | |
| {
 | |
|    const std::string styleString = fmt::format("{}", fmt::join(styles, " "));
 | |
|    const std::string fontString =
 | |
|       fmt::format("{}-{}:{}", family, size.value(), styleString);
 | |
| 
 | |
|    logger_->debug("LoadFontResource: {}", fontString);
 | |
| 
 | |
|    FontRecord fontRecord = Impl::MatchFontFile(family, styles);
 | |
| 
 | |
|    // Only allow whole pixels, and clamp to 6-72 pt
 | |
|    units::font_size::pixels<double> pixels {size};
 | |
|    units::font_size::pixels<int>    imFontSize {
 | |
|       std::clamp(static_cast<int>(pixels.value()), 8, 96)};
 | |
|    auto imguiFontKey = std::make_pair(fontRecord, imFontSize);
 | |
| 
 | |
|    // Search for a loaded ImGui font
 | |
|    {
 | |
|       std::shared_lock imguiFontLock {p->imguiFontsMutex_};
 | |
| 
 | |
|       // Search for the associated ImGui font
 | |
|       auto it = p->imguiFonts_.find(imguiFontKey);
 | |
|       if (it != p->imguiFonts_.end())
 | |
|       {
 | |
|          return it->second;
 | |
|       }
 | |
| 
 | |
|       // No ImGui font was found, we need to create one
 | |
|    }
 | |
| 
 | |
|    // No font was found, return an empty shared pointer if not loading
 | |
|    if (!loadIfNotFound)
 | |
|    {
 | |
|       return nullptr;
 | |
|    }
 | |
| 
 | |
|    // Get raw font data
 | |
|    const auto& rawFontData = p->GetRawFontData(fontRecord.filename_);
 | |
| 
 | |
|    // The font atlas mutex might already be locked within an ImGui render frame.
 | |
|    // Lock the font atlas mutex before the fonts mutex to prevent deadlock.
 | |
|    std::unique_lock imguiFontAtlasLock {p->imguiFontAtlasMutex_};
 | |
|    std::unique_lock imguiFontsLock {p->imguiFontsMutex_};
 | |
| 
 | |
|    // Search for the associated ImGui font again, to prevent loading the same
 | |
|    // font twice
 | |
|    auto it = p->imguiFonts_.find(imguiFontKey);
 | |
|    if (it != p->imguiFonts_.end())
 | |
|    {
 | |
|       return it->second;
 | |
|    }
 | |
| 
 | |
|    // Create an ImGui font
 | |
|    std::shared_ptr<types::ImGuiFont> imguiFont =
 | |
|       std::make_shared<types::ImGuiFont>(
 | |
|          fontRecord.filename_, rawFontData, imFontSize);
 | |
| 
 | |
|    // Store the ImGui font
 | |
|    p->imguiFonts_.insert_or_assign(imguiFontKey, imguiFont);
 | |
| 
 | |
|    // Increment ImGui font build count
 | |
|    ++p->imguiFontsBuildCount_;
 | |
| 
 | |
|    // Return the ImGui font
 | |
|    return imguiFont;
 | |
| }
 | |
| 
 | |
| const std::vector<std::uint8_t>&
 | |
| FontManager::Impl::GetRawFontData(const std::string& filename)
 | |
| {
 | |
|    std::unique_lock rawFontDataLock {rawFontDataMutex_};
 | |
| 
 | |
|    auto it = rawFontData_.find(filename);
 | |
|    if (it != rawFontData_.end())
 | |
|    {
 | |
|       // Raw font data has already been loaded
 | |
|       return it->second;
 | |
|    }
 | |
| 
 | |
|    // Raw font data needs to be loaded
 | |
|    std::basic_ifstream<std::uint8_t> ifs {filename, std::ios::binary};
 | |
|    ifs.seekg(0, std::ios_base::end);
 | |
|    std::size_t dataSize = ifs.tellg();
 | |
|    ifs.seekg(0, std::ios_base::beg);
 | |
| 
 | |
|    // Store the font data in a buffer
 | |
|    std::vector<std::uint8_t> buffer {};
 | |
|    buffer.reserve(dataSize);
 | |
|    std::copy(std::istreambuf_iterator<std::uint8_t>(ifs),
 | |
|              std::istreambuf_iterator<std::uint8_t>(),
 | |
|              std::back_inserter(buffer));
 | |
| 
 | |
|    // Place the buffer in the cache
 | |
|    auto result = rawFontData_.emplace(filename, std::move(buffer));
 | |
| 
 | |
|    // Return the cached buffer
 | |
|    return result.first->second;
 | |
| }
 | |
| 
 | |
| void FontManager::LoadApplicationFont(const std::string& filename)
 | |
| {
 | |
|    // If the font cache failed to create, don't attempt to cache any fonts
 | |
|    if (p->fontCachePath_.empty())
 | |
|    {
 | |
|       return;
 | |
|    }
 | |
| 
 | |
|    // Make a copy of the font in the cache (if it doesn't exist)
 | |
|    QFile     fontFile(QString::fromStdString(filename));
 | |
|    QFileInfo fontFileInfo(fontFile);
 | |
| 
 | |
|    QFile       cacheFile(QString::fromStdString(p->fontCachePath_) +
 | |
|                    fontFileInfo.fileName());
 | |
|    QFileInfo   cacheFileInfo(cacheFile);
 | |
|    std::string cacheFilename = cacheFile.fileName().toStdString();
 | |
| 
 | |
|    if (fontFile.exists())
 | |
|    {
 | |
|       // If the file has not been cached, or the font file size has changed
 | |
|       if (!cacheFile.exists() || fontFileInfo.size() != cacheFileInfo.size())
 | |
|       {
 | |
|          logger_->info("Caching font: {}", filename);
 | |
|          if (!fontFile.copy(cacheFile.fileName()))
 | |
|          {
 | |
|             logger_->error("Could not cache font: {}", filename);
 | |
|             return;
 | |
|          }
 | |
|       }
 | |
|    }
 | |
|    else
 | |
|    {
 | |
|       logger_->error("Font does not exist: {}", filename);
 | |
|       return;
 | |
|    }
 | |
| 
 | |
|    // Load the file into fontconfig
 | |
|    FcBool result = FcConfigAppFontAddFile(
 | |
|       nullptr, reinterpret_cast<const FcChar8*>(cacheFilename.c_str()));
 | |
|    if (!result)
 | |
|    {
 | |
|       logger_->error("Could not load font into fontconfig database", filename);
 | |
|    }
 | |
| }
 | |
| 
 | |
| void FontManager::Impl::InitializeFontCache()
 | |
| {
 | |
|    std::string cachePath {
 | |
|       QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
 | |
|          .toStdString() +
 | |
|       "/fonts"};
 | |
| 
 | |
|    fontCachePath_ = cachePath + "/";
 | |
| 
 | |
|    if (!std::filesystem::exists(cachePath))
 | |
|    {
 | |
|       std::error_code error;
 | |
|       if (!std::filesystem::create_directories(cachePath, error))
 | |
|       {
 | |
|          logger_->error("Unable to create font cache directory: \"{}\" ({})",
 | |
|                         cachePath,
 | |
|                         error.message());
 | |
|          fontCachePath_.clear();
 | |
|       }
 | |
|    }
 | |
| }
 | |
| 
 | |
| void FontManager::Impl::InitializeFontconfig()
 | |
| {
 | |
|    FcConfig* fcConfig = FcInitLoadConfigAndFonts();
 | |
|    FcConfigSetCurrent(fcConfig);
 | |
| }
 | |
| 
 | |
| void FontManager::Impl::FinalizeFontconfig()
 | |
| {
 | |
|    FcFini();
 | |
| }
 | |
| 
 | |
| FontRecord
 | |
| FontManager::Impl::MatchFontFile(const std::string&              family,
 | |
|                                  const std::vector<std::string>& styles)
 | |
| {
 | |
|    const std::string styleString = fmt::format("{}", fmt::join(styles, " "));
 | |
|    const std::string fontString  = fmt::format("{}:{}", family, styleString);
 | |
| 
 | |
|    // Build fontconfig pattern
 | |
|    FcPattern* pattern = FcPatternCreate();
 | |
| 
 | |
|    FcPatternAddString(
 | |
|       pattern, FC_FAMILY, reinterpret_cast<const FcChar8*>(family.c_str()));
 | |
|    FcPatternAddString(pattern,
 | |
|                       FC_FONTFORMAT,
 | |
|                       reinterpret_cast<const FcChar8*>(kFcTrueType_.c_str()));
 | |
| 
 | |
|    if (!styles.empty())
 | |
|    {
 | |
|       FcPatternAddString(pattern,
 | |
|                          FC_STYLE,
 | |
|                          reinterpret_cast<const FcChar8*>(styleString.c_str()));
 | |
|    }
 | |
| 
 | |
|    // Perform font pattern match substitution
 | |
|    FcConfigSubstitute(nullptr, pattern, FcMatchPattern);
 | |
|    FcDefaultSubstitute(pattern);
 | |
| 
 | |
|    // Find matching font
 | |
|    FcResult   result;
 | |
|    FcPattern* match = FcFontMatch(nullptr, pattern, &result);
 | |
|    FontRecord record {};
 | |
| 
 | |
|    if (match != nullptr)
 | |
|    {
 | |
|       FcChar8* fcFamily;
 | |
|       FcChar8* fcStyle;
 | |
|       FcChar8* fcFile;
 | |
| 
 | |
|       // Match was found, get properties
 | |
|       if (FcPatternGetString(match, FC_FAMILY, 0, &fcFamily) == FcResultMatch &&
 | |
|           FcPatternGetString(match, FC_STYLE, 0, &fcStyle) == FcResultMatch &&
 | |
|           FcPatternGetString(match, FC_FILE, 0, &fcFile) == FcResultMatch)
 | |
|       {
 | |
|          record.family_   = reinterpret_cast<char*>(fcFamily);
 | |
|          record.style_    = reinterpret_cast<char*>(fcStyle);
 | |
|          record.filename_ = reinterpret_cast<char*>(fcFile);
 | |
| 
 | |
|          logger_->debug("Found matching font: {}:{} ({})",
 | |
|                         record.family_,
 | |
|                         record.style_,
 | |
|                         record.filename_);
 | |
|       }
 | |
|    }
 | |
| 
 | |
|    if (record.filename_.empty())
 | |
|    {
 | |
|       logger_->warn("Could not find matching font: {}", fontString);
 | |
|    }
 | |
| 
 | |
|    // Cleanup
 | |
|    FcPatternDestroy(match);
 | |
|    FcPatternDestroy(pattern);
 | |
| 
 | |
|    return record;
 | |
| }
 | |
| 
 | |
| FontManager& FontManager::Instance()
 | |
| {
 | |
|    static FontManager instance_ {};
 | |
|    return instance_;
 | |
| }
 | |
| 
 | |
| size_t FontRecordHash<FontRecordPair>::operator()(const FontRecordPair& x) const
 | |
| {
 | |
|    size_t seed = 0;
 | |
|    boost::hash_combine(seed, x.first.family_);
 | |
|    boost::hash_combine(seed, x.first.style_);
 | |
|    boost::hash_combine(seed, x.first.filename_);
 | |
|    boost::hash_combine(seed, x.second.value());
 | |
|    return seed;
 | |
| }
 | |
| 
 | |
| bool operator==(const FontRecord& lhs, const FontRecord& rhs)
 | |
| {
 | |
|    return lhs.family_ == rhs.family_ && //
 | |
|           lhs.style_ == rhs.style_ &&   //
 | |
|           lhs.filename_ == rhs.filename_;
 | |
| }
 | |
| 
 | |
| } // namespace manager
 | |
| } // namespace qt
 | |
| } // namespace scwx
 | 
