supercell-wx/scwx-qt/source/scwx/qt/manager/font_manager.cpp
2024-11-30 15:42:15 -06:00

551 lines
16 KiB
C++

#include <scwx/qt/manager/font_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/settings/text_settings.hpp>
#include <scwx/util/environment.hpp>
#include <scwx/util/logger.hpp>
#include <filesystem>
#include <fstream>
#include <QFile>
#include <QFileInfo>
#include <QFontDatabase>
#include <QGuiApplication>
#include <QStandardPaths>
#include <boost/container_hash/hash.hpp>
#include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/unordered_flat_set.hpp>
#include <fmt/ranges.h>
#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(FontManager* self) : self_ {self}
{
InitializeEnvironment();
InitializeFontCache();
InitializeFontconfig();
ConnectSignals();
}
~Impl() { FinalizeFontconfig(); }
void ConnectSignals();
void FinalizeFontconfig();
void InitializeEnvironment();
void InitializeFontCache();
void InitializeFontconfig();
void UpdateImGuiFont(types::FontCategory fontCategory);
void UpdateQFont(types::FontCategory fontCategory);
const std::vector<char>& GetRawFontData(const std::string& filename);
static FontRecord MatchFontFile(const std::string& family,
const std::vector<std::string>& styles);
FontManager* self_;
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<char>> rawFontData_ {};
std::mutex rawFontDataMutex_ {};
std::shared_ptr<types::ImGuiFont> defaultFont_ {};
boost::unordered_flat_map<types::FontCategory,
std::shared_ptr<types::ImGuiFont>>
fontCategoryImguiFontMap_ {};
boost::unordered_flat_map<types::FontCategory, QFont>
fontCategoryQFontMap_ {};
std::mutex fontCategoryMutex_ {};
boost::unordered_flat_set<types::FontCategory> dirtyFonts_ {};
std::mutex dirtyFontsMutex_ {};
boost::unordered_flat_map<types::Font, int> fontIds_ {};
};
FontManager::FontManager() : p(std::make_unique<Impl>(this)) {}
FontManager::~FontManager() {};
void FontManager::Impl::ConnectSignals()
{
auto& textSettings = settings::TextSettings::Instance();
for (auto fontCategory : types::FontCategoryIterator())
{
textSettings.font_family(fontCategory)
.RegisterValueChangedCallback(
[this, fontCategory](const auto&)
{
std::unique_lock lock {dirtyFontsMutex_};
dirtyFonts_.insert(fontCategory);
});
textSettings.font_style(fontCategory)
.RegisterValueChangedCallback(
[this, fontCategory](const auto&)
{
std::unique_lock lock {dirtyFontsMutex_};
dirtyFonts_.insert(fontCategory);
});
textSettings.font_point_size(fontCategory)
.RegisterValueChangedCallback(
[this, fontCategory](const auto&)
{
std::unique_lock lock {dirtyFontsMutex_};
dirtyFonts_.insert(fontCategory);
});
}
QObject::connect(
&SettingsManager::Instance(),
&SettingsManager::SettingsSaved,
self_,
[this]()
{
std::scoped_lock lock {dirtyFontsMutex_, fontCategoryMutex_};
for (auto fontCategory : dirtyFonts_)
{
UpdateImGuiFont(fontCategory);
UpdateQFont(fontCategory);
}
dirtyFonts_.clear();
});
}
void FontManager::InitializeFonts()
{
for (auto fontCategory : types::FontCategoryIterator())
{
p->UpdateImGuiFont(fontCategory);
p->UpdateQFont(fontCategory);
}
}
void FontManager::Impl::UpdateImGuiFont(types::FontCategory fontCategory)
{
auto& textSettings = settings::TextSettings::Instance();
auto family = textSettings.font_family(fontCategory).GetValue();
auto styles = textSettings.font_style(fontCategory).GetValue();
units::font_size::points<double> size {
textSettings.font_point_size(fontCategory).GetValue()};
fontCategoryImguiFontMap_.insert_or_assign(
fontCategory, self_->LoadImGuiFont(family, {styles}, size));
}
void FontManager::Impl::UpdateQFont(types::FontCategory fontCategory)
{
auto& textSettings = settings::TextSettings::Instance();
auto family = textSettings.font_family(fontCategory).GetValue();
auto styles = textSettings.font_style(fontCategory).GetValue();
units::font_size::points<double> size {
textSettings.font_point_size(fontCategory).GetValue()};
QFont font = QFontDatabase::font(QString::fromStdString(family),
QString::fromStdString(styles),
static_cast<int>(size.value()));
font.setPointSizeF(size.value());
fontCategoryQFontMap_.insert_or_assign(fontCategory, font);
}
std::shared_mutex& FontManager::imgui_font_atlas_mutex()
{
return p->imguiFontAtlasMutex_;
}
std::uint64_t FontManager::imgui_fonts_build_count() const
{
return p->imguiFontsBuildCount_;
}
int FontManager::GetFontId(types::Font font) const
{
auto it = p->fontIds_.find(font);
if (it != p->fontIds_.cend())
{
return it->second;
}
return -1;
}
std::shared_ptr<types::ImGuiFont>
FontManager::GetImGuiFont(types::FontCategory fontCategory)
{
std::unique_lock lock {p->fontCategoryMutex_};
auto it = p->fontCategoryImguiFontMap_.find(fontCategory);
if (it != p->fontCategoryImguiFontMap_.cend())
{
return it->second;
}
return p->defaultFont_;
}
QFont FontManager::GetQFont(types::FontCategory fontCategory)
{
std::unique_lock lock {p->fontCategoryMutex_};
auto it = p->fontCategoryQFontMap_.find(fontCategory);
if (it != p->fontCategoryQFontMap_.cend())
{
return it->second;
}
return QGuiApplication::font();
}
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;
}
// Define a name for the ImGui font
std::string fontName;
try
{
fontName = fmt::format(
"{}:{}",
std::filesystem::path(fontRecord.filename_).filename().string(),
imFontSize.value());
}
catch (const std::exception& ex)
{
logger_->warn(ex.what());
fontName = fmt::format("{}:{}", fontRecord.filename_, imFontSize.value());
}
// Create an ImGui font
std::shared_ptr<types::ImGuiFont> imguiFont =
std::make_shared<types::ImGuiFont>(fontName, 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<char>&
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<char> 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<char> buffer {};
buffer.reserve(dataSize);
std::copy(std::istreambuf_iterator<char>(ifs),
std::istreambuf_iterator<char>(),
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(types::Font font,
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 the Qt Font Database
int fontId =
QFontDatabase::addApplicationFont(QString::fromStdString(cacheFilename));
p->fontIds_.emplace(font, fontId);
// 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::InitializeEnvironment()
{
#if defined(__linux__)
// Because of the way Fontconfig is built with Conan, FONTCONFIG_PATH must be
// defined on Linux to ensure fonts can be found
static const std::string kFontconfigPathKey {"FONTCONFIG_PATH"};
std::string fontconfigPath = scwx::util::GetEnvironment(kFontconfigPathKey);
if (fontconfigPath.empty())
{
scwx::util::SetEnvironment(kFontconfigPathKey, "/etc/fonts");
}
#endif
}
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