mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2025-01-23 17:15:38 +00:00
0c552dd8fb
Also added support for specifying a separate format as compared to internalFormat when creating textures, although the shader should be used primarily as this is not really supported by the OpenGL standard.
749 lines
24 KiB
C++
749 lines
24 KiB
C++
// SPDX-License-Identifier: MIT
|
|
//
|
|
// EmulationStation Desktop Edition
|
|
// Font.h
|
|
//
|
|
// Loading, unloading, caching and rendering of fonts.
|
|
// Also functions for word wrapping and similar.
|
|
//
|
|
|
|
#include "resources/Font.h"
|
|
|
|
#include "Log.h"
|
|
#include "renderers/Renderer.h"
|
|
#include "utils/FileSystemUtil.h"
|
|
#include "utils/StringUtil.h"
|
|
|
|
FT_Library Font::sLibrary = nullptr;
|
|
|
|
std::map<std::pair<std::string, int>, std::weak_ptr<Font>> Font::sFontMap;
|
|
|
|
Font::FontFace::FontFace(ResourceData&& d, int size)
|
|
: data(d)
|
|
{
|
|
int err =
|
|
FT_New_Memory_Face(sLibrary, data.ptr.get(), static_cast<FT_Long>(data.length), 0, &face);
|
|
assert(!err);
|
|
|
|
if (!err)
|
|
FT_Set_Pixel_Sizes(face, 0, size);
|
|
}
|
|
|
|
Font::FontFace::~FontFace()
|
|
{
|
|
if (face)
|
|
FT_Done_Face(face);
|
|
}
|
|
|
|
void Font::initLibrary()
|
|
{
|
|
assert(sLibrary == nullptr);
|
|
|
|
if (FT_Init_FreeType(&sLibrary)) {
|
|
sLibrary = nullptr;
|
|
LOG(LogError) << "Couldn't initialize FreeType";
|
|
}
|
|
}
|
|
|
|
size_t Font::getMemUsage() const
|
|
{
|
|
size_t memUsage = 0;
|
|
for (auto it = mTextures.cbegin(); it != mTextures.cend(); ++it)
|
|
memUsage += it->textureSize.x * it->textureSize.y * 4;
|
|
|
|
for (auto it = mFaceCache.cbegin(); it != mFaceCache.cend(); ++it)
|
|
memUsage += it->second->data.length;
|
|
|
|
return memUsage;
|
|
}
|
|
|
|
size_t Font::getTotalMemUsage()
|
|
{
|
|
size_t total = 0;
|
|
|
|
auto it = sFontMap.cbegin();
|
|
while (it != sFontMap.cend()) {
|
|
if (it->second.expired()) {
|
|
it = sFontMap.erase(it);
|
|
continue;
|
|
}
|
|
|
|
total += it->second.lock()->getMemUsage();
|
|
++it;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
Font::Font(int size, const std::string& path)
|
|
: mSize(size)
|
|
, mPath(path)
|
|
{
|
|
if (mSize < 9) {
|
|
mSize = 9;
|
|
LOG(LogWarning) << "Requested font size too small, changing to minimum supported size";
|
|
}
|
|
else if (mSize > Renderer::getScreenHeight()) {
|
|
mSize = Renderer::getScreenHeight();
|
|
LOG(LogWarning) << "Requested font size too large, changing to maximum supported size";
|
|
}
|
|
|
|
mMaxGlyphHeight = 0;
|
|
|
|
if (!sLibrary)
|
|
initLibrary();
|
|
|
|
// Always initialize ASCII characters.
|
|
for (unsigned int i = 32; i < 128; ++i)
|
|
getGlyph(i);
|
|
|
|
clearFaceCache();
|
|
}
|
|
|
|
Font::~Font()
|
|
{
|
|
unload(ResourceManager::getInstance());
|
|
|
|
auto fontEntry = sFontMap.find(std::pair<std::string, int>(mPath, mSize));
|
|
|
|
if (fontEntry != sFontMap.cend())
|
|
sFontMap.erase(fontEntry);
|
|
|
|
if (sFontMap.empty() && sLibrary) {
|
|
FT_Done_FreeType(sLibrary);
|
|
sLibrary = nullptr;
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Font> Font::get(int size, const std::string& path)
|
|
{
|
|
const std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path);
|
|
std::pair<std::string, int> def(canonicalPath.empty() ? getDefaultPath() : canonicalPath, size);
|
|
|
|
auto foundFont = sFontMap.find(def);
|
|
if (foundFont != sFontMap.cend()) {
|
|
if (!foundFont->second.expired())
|
|
return foundFont->second.lock();
|
|
}
|
|
|
|
std::shared_ptr<Font> font = std::shared_ptr<Font>(new Font(def.second, def.first));
|
|
sFontMap[def] = std::weak_ptr<Font>(font);
|
|
ResourceManager::getInstance().addReloadable(font);
|
|
return font;
|
|
}
|
|
|
|
void Font::unloadTextures()
|
|
{
|
|
for (auto it = mTextures.begin(); it != mTextures.end(); ++it)
|
|
it->deinitTexture();
|
|
}
|
|
|
|
Font::FontTexture::FontTexture(const int mSize)
|
|
{
|
|
textureId = 0;
|
|
|
|
// This is a hack to add some extra texture size when running at very low resolutions. If not
|
|
// doing this, the use of fallback fonts (such as Japanese characters) could result in the
|
|
// texture not fitting the glyphs which would crash the application.
|
|
int extraTextureSize{0};
|
|
const float screenSizeModifier =
|
|
std::min(Renderer::getScreenWidthModifier(), Renderer::getScreenHeightModifier());
|
|
|
|
if (screenSizeModifier < 0.2f)
|
|
extraTextureSize += 6;
|
|
if (screenSizeModifier < 0.45f)
|
|
extraTextureSize += 4;
|
|
|
|
// It's not entirely clear if the 18 and 6 constants are correct, but they seem to provide
|
|
// a texture buffer large enough to hold the fonts (otherwise the application would crash).
|
|
// This logic is obviously a hack though and needs to be properly reviewed and improved.
|
|
textureSize = glm::ivec2{mSize * (18 + extraTextureSize), mSize * (6 + extraTextureSize / 2)};
|
|
writePos = glm::ivec2{};
|
|
rowHeight = 0;
|
|
}
|
|
|
|
Font::FontTexture::~FontTexture()
|
|
{
|
|
// Deinit the texture when destroyed.
|
|
deinitTexture();
|
|
}
|
|
|
|
bool Font::FontTexture::findEmpty(const glm::ivec2& size, glm::ivec2& cursor_out)
|
|
{
|
|
if (size.x >= textureSize.x || size.y >= textureSize.y)
|
|
return false;
|
|
|
|
if (writePos.x + size.x >= textureSize.x &&
|
|
writePos.y + rowHeight + size.y + 1 < textureSize.y) {
|
|
// Row full, but it should fit on the next row so move the cursor there.
|
|
// Leave 1px of space between glyphs.
|
|
writePos = glm::ivec2{0, writePos.y + rowHeight + 1};
|
|
rowHeight = 0;
|
|
}
|
|
|
|
if (writePos.x + size.x >= textureSize.x || writePos.y + size.y >= textureSize.y) {
|
|
// Nope, still won't fit.
|
|
return false;
|
|
}
|
|
|
|
cursor_out = writePos;
|
|
// Leave 1px of space between glyphs.
|
|
writePos.x += size.x + 1;
|
|
|
|
if (size.y > rowHeight)
|
|
rowHeight = size.y;
|
|
|
|
return true;
|
|
}
|
|
|
|
void Font::FontTexture::initTexture()
|
|
{
|
|
assert(textureId == 0);
|
|
textureId = Renderer::createTexture(Renderer::Texture::ALPHA, Renderer::Texture::ALPHA, false,
|
|
false, false, textureSize.x, textureSize.y, nullptr);
|
|
}
|
|
|
|
void Font::FontTexture::deinitTexture()
|
|
{
|
|
if (textureId != 0) {
|
|
Renderer::destroyTexture(textureId);
|
|
textureId = 0;
|
|
}
|
|
}
|
|
|
|
void Font::getTextureForNewGlyph(const glm::ivec2& glyphSize,
|
|
FontTexture*& tex_out,
|
|
glm::ivec2& cursor_out)
|
|
{
|
|
if (mTextures.size()) {
|
|
// Check if the most recent texture has space.
|
|
tex_out = &mTextures.back();
|
|
|
|
// Will this one work?
|
|
if (tex_out->findEmpty(glyphSize, cursor_out))
|
|
return; // Yes.
|
|
}
|
|
|
|
// Current textures are full, make a new one.
|
|
mTextures.push_back(FontTexture(mSize));
|
|
tex_out = &mTextures.back();
|
|
tex_out->initTexture();
|
|
|
|
bool ok = tex_out->findEmpty(glyphSize, cursor_out);
|
|
if (!ok) {
|
|
LOG(LogError) << "Glyph too big to fit on a new texture (glyph size > "
|
|
<< tex_out->textureSize.x << ", " << tex_out->textureSize.y << ")";
|
|
tex_out = nullptr;
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> getFallbackFontPaths()
|
|
{
|
|
std::vector<std::string> fontPaths;
|
|
|
|
// Standard fonts, let's include them here for exception handling purposes even though that's
|
|
// not really the correct location. (The application will crash if they are missing.)
|
|
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-Regular.ttf");
|
|
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-SemiBold.ttf");
|
|
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-Bold.ttf");
|
|
|
|
// Vera sans Unicode.
|
|
fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/DejaVuSans.ttf"));
|
|
// GNU FreeFont monospaced.
|
|
fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/FreeMono.ttf"));
|
|
// Various languages, such as Japanese and Chinese.
|
|
fontPaths.push_back(
|
|
ResourceManager::getInstance().getResourcePath(":/fonts/DroidSansFallbackFull.ttf"));
|
|
// Korean.
|
|
fontPaths.push_back(
|
|
ResourceManager::getInstance().getResourcePath(":/fonts/NanumMyeongjo.ttf"));
|
|
// Font Awesome icon glyphs, used for various special symbols like stars, folders etc.
|
|
fontPaths.push_back(
|
|
ResourceManager::getInstance().getResourcePath(":/fonts/fontawesome-webfont.ttf"));
|
|
// This is only needed for some really rare special characters.
|
|
fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/Ubuntu-C.ttf"));
|
|
|
|
fontPaths.shrink_to_fit();
|
|
return fontPaths;
|
|
}
|
|
|
|
FT_Face Font::getFaceForChar(unsigned int id)
|
|
{
|
|
static const std::vector<std::string> fallbackFonts = getFallbackFontPaths();
|
|
|
|
// Look through our current font + fallback fonts to see if any have the
|
|
// glyph we're looking for.
|
|
for (unsigned int i = 0; i < fallbackFonts.size() + 1; ++i) {
|
|
auto fit = mFaceCache.find(i);
|
|
|
|
// Doesn't exist yet.
|
|
if (fit == mFaceCache.cend()) {
|
|
// i == 0 -> mPath
|
|
// Otherwise, take from fallbackFonts.
|
|
const std::string& path = (i == 0 ? mPath : fallbackFonts.at(i - 1));
|
|
ResourceData data = ResourceManager::getInstance().getFileData(path);
|
|
mFaceCache[i] = std::unique_ptr<FontFace>(new FontFace(std::move(data), mSize));
|
|
fit = mFaceCache.find(i);
|
|
}
|
|
|
|
if (FT_Get_Char_Index(fit->second->face, id) != 0)
|
|
return fit->second->face;
|
|
}
|
|
|
|
// Nothing has a valid glyph - return the "real" face so we get a "missing" character.
|
|
return mFaceCache.cbegin()->second->face;
|
|
}
|
|
|
|
Font::Glyph* Font::getGlyph(unsigned int id)
|
|
{
|
|
// Is it already loaded?
|
|
auto it = mGlyphMap.find(id);
|
|
if (it != mGlyphMap.cend())
|
|
return &it->second;
|
|
|
|
// Nope, need to make a glyph.
|
|
FT_Face face = getFaceForChar(id);
|
|
if (!face) {
|
|
LOG(LogError) << "Couldn't find appropriate font face for character " << id << " for font "
|
|
<< mPath;
|
|
return nullptr;
|
|
}
|
|
|
|
FT_GlyphSlot g = face->glyph;
|
|
|
|
if (FT_Load_Char(face, id, FT_LOAD_RENDER)) {
|
|
LOG(LogError) << "Couldn't find glyph for character " << id << " for font " << mPath
|
|
<< ", size " << mSize;
|
|
return nullptr;
|
|
}
|
|
|
|
glm::ivec2 glyphSize{g->bitmap.width, g->bitmap.rows};
|
|
|
|
FontTexture* tex = nullptr;
|
|
glm::ivec2 cursor;
|
|
getTextureForNewGlyph(glyphSize, tex, cursor);
|
|
|
|
// getTextureForNewGlyph can fail if the glyph is bigger than the max texture
|
|
// size (absurdly large font size).
|
|
if (tex == nullptr) {
|
|
LOG(LogError) << "Couldn't create glyph for character " << id << " for font " << mPath
|
|
<< ", size " << mSize << " (no suitable texture found)";
|
|
return nullptr;
|
|
}
|
|
|
|
// Create glyph.
|
|
Glyph& glyph = mGlyphMap[id];
|
|
|
|
glyph.texture = tex;
|
|
glyph.texPos = glm::vec2{cursor.x / static_cast<float>(tex->textureSize.x),
|
|
cursor.y / static_cast<float>(tex->textureSize.y)};
|
|
glyph.texSize = glm::vec2{glyphSize.x / static_cast<float>(tex->textureSize.x),
|
|
glyphSize.y / static_cast<float>(tex->textureSize.y)};
|
|
|
|
glyph.advance = glm::vec2{static_cast<float>(g->metrics.horiAdvance) / 64.0f,
|
|
static_cast<float>(g->metrics.vertAdvance) / 64.0f};
|
|
glyph.bearing = glm::vec2{static_cast<float>(g->metrics.horiBearingX) / 64.0f,
|
|
static_cast<float>(g->metrics.horiBearingY) / 64.0f};
|
|
|
|
// Upload glyph bitmap to texture.
|
|
Renderer::updateTexture(tex->textureId, Renderer::Texture::ALPHA, cursor.x, cursor.y,
|
|
glyphSize.x, glyphSize.y, g->bitmap.buffer);
|
|
|
|
// Update max glyph height.
|
|
if (glyphSize.y > mMaxGlyphHeight)
|
|
mMaxGlyphHeight = glyphSize.y;
|
|
|
|
// Done.
|
|
return &glyph;
|
|
}
|
|
|
|
void Font::rebuildTextures()
|
|
{
|
|
// Recreate OpenGL textures.
|
|
for (auto it = mTextures.begin(); it != mTextures.end(); ++it)
|
|
it->initTexture();
|
|
|
|
// Re-upload the texture data.
|
|
for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); ++it) {
|
|
FT_Face face = getFaceForChar(it->first);
|
|
FT_GlyphSlot glyphSlot = face->glyph;
|
|
|
|
// Load the glyph bitmap through FT.
|
|
FT_Load_Char(face, it->first, FT_LOAD_RENDER);
|
|
|
|
FontTexture* tex = it->second.texture;
|
|
|
|
// Find the position/size.
|
|
glm::ivec2 cursor{static_cast<int>(it->second.texPos.x * tex->textureSize.x),
|
|
static_cast<int>(it->second.texPos.y * tex->textureSize.y)};
|
|
glm::ivec2 glyphSize{static_cast<int>(it->second.texSize.x * tex->textureSize.x),
|
|
static_cast<int>(it->second.texSize.y * tex->textureSize.y)};
|
|
|
|
// Upload to texture.
|
|
Renderer::updateTexture(tex->textureId, Renderer::Texture::ALPHA, cursor.x, cursor.y,
|
|
glyphSize.x, glyphSize.y, glyphSlot->bitmap.buffer);
|
|
}
|
|
}
|
|
|
|
void Font::renderTextCache(TextCache* cache)
|
|
{
|
|
if (cache == nullptr) {
|
|
LOG(LogError) << "Attempted to draw nullptr TextCache";
|
|
return;
|
|
}
|
|
|
|
for (auto it = cache->vertexLists.cbegin(); it != cache->vertexLists.cend(); ++it) {
|
|
assert(*it->textureIdPtr != 0);
|
|
|
|
auto vertexList = *it;
|
|
|
|
Renderer::bindTexture(*it->textureIdPtr);
|
|
Renderer::drawTriangleStrips(&it->verts[0],
|
|
static_cast<const unsigned int>(it->verts.size()));
|
|
}
|
|
}
|
|
|
|
glm::vec2 Font::sizeText(std::string text, float lineSpacing)
|
|
{
|
|
float lineWidth = 0.0f;
|
|
float highestWidth = 0.0f;
|
|
|
|
const float lineHeight = getHeight(lineSpacing);
|
|
|
|
float y = lineHeight;
|
|
|
|
size_t i = 0;
|
|
while (i < text.length()) {
|
|
unsigned int character = Utils::String::chars2Unicode(text, i); // Advances i.
|
|
|
|
if (character == '\n') {
|
|
if (lineWidth > highestWidth)
|
|
highestWidth = lineWidth;
|
|
|
|
lineWidth = 0.0f;
|
|
y += lineHeight;
|
|
}
|
|
|
|
Glyph* glyph = getGlyph(character);
|
|
if (glyph)
|
|
lineWidth += glyph->advance.x;
|
|
}
|
|
|
|
if (lineWidth > highestWidth)
|
|
highestWidth = lineWidth;
|
|
|
|
return glm::vec2{highestWidth, y};
|
|
}
|
|
|
|
std::string Font::getTextMaxWidth(std::string text, float maxWidth)
|
|
{
|
|
float width = sizeText(text).x;
|
|
while (width > maxWidth) {
|
|
text.pop_back();
|
|
width = sizeText(text).x;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
float Font::getHeight(float lineSpacing) const
|
|
{
|
|
// Return overall height including line spacing.
|
|
return mMaxGlyphHeight * lineSpacing;
|
|
}
|
|
|
|
float Font::getLetterHeight()
|
|
{
|
|
Glyph* glyph = getGlyph('S');
|
|
assert(glyph);
|
|
return glyph->texSize.y * glyph->texture->textureSize.y;
|
|
}
|
|
|
|
std::string Font::wrapText(std::string text, float xLen)
|
|
{
|
|
std::string out;
|
|
std::string line;
|
|
std::string word;
|
|
std::string abbreviatedWord;
|
|
std::string temp;
|
|
|
|
size_t space;
|
|
glm::vec2 textSize;
|
|
float dotsSize = sizeText("...").x;
|
|
|
|
// While there's text or we still have text to render.
|
|
while (text.length() > 0) {
|
|
space = text.find_first_of(" \t\n");
|
|
if (space == std::string::npos)
|
|
space = text.length() - 1;
|
|
|
|
word = text.substr(0, space + 1);
|
|
text.erase(0, space + 1);
|
|
|
|
temp = line + word;
|
|
|
|
textSize = sizeText(temp);
|
|
|
|
// If the word will fit on the line, add it to our line, and continue.
|
|
if (textSize.x <= xLen) {
|
|
line = temp;
|
|
continue;
|
|
}
|
|
else {
|
|
// The next word won't fit, so break here.
|
|
|
|
// If the word is too long to fit within xLen, then abbreviate it.
|
|
if (xLen > 0 && sizeText(word).x > xLen) {
|
|
float length = xLen - dotsSize;
|
|
if (length < 0)
|
|
length = 0;
|
|
abbreviatedWord = getTextMaxWidth(word, length);
|
|
abbreviatedWord += "...";
|
|
word = abbreviatedWord;
|
|
out += line;
|
|
}
|
|
else {
|
|
out += line + '\n';
|
|
}
|
|
line = word;
|
|
}
|
|
}
|
|
|
|
// Whatever's left should fit.
|
|
out += line;
|
|
|
|
return out;
|
|
}
|
|
|
|
glm::vec2 Font::sizeWrappedText(std::string text, float xLen, float lineSpacing)
|
|
{
|
|
text = wrapText(text, xLen);
|
|
return sizeText(text, lineSpacing);
|
|
}
|
|
|
|
glm::vec2 Font::getWrappedTextCursorOffset(std::string text,
|
|
float xLen,
|
|
size_t stop,
|
|
float lineSpacing)
|
|
{
|
|
std::string wrappedText = wrapText(text, xLen);
|
|
|
|
float lineWidth = 0.0f;
|
|
float y = 0.0f;
|
|
|
|
size_t wrapCursor = 0;
|
|
size_t cursor = 0;
|
|
while (cursor < stop) {
|
|
unsigned int wrappedCharacter = Utils::String::chars2Unicode(wrappedText, wrapCursor);
|
|
unsigned int character = Utils::String::chars2Unicode(text, cursor);
|
|
|
|
if (wrappedCharacter == '\n' && character != '\n') {
|
|
// This is where the wordwrap inserted a newline
|
|
// Reset lineWidth and increment y, but don't consume .a cursor character.
|
|
lineWidth = 0.0f;
|
|
y += getHeight(lineSpacing);
|
|
|
|
cursor = Utils::String::prevCursor(text, cursor); // Unconsume.
|
|
continue;
|
|
}
|
|
|
|
if (character == '\n') {
|
|
lineWidth = 0.0f;
|
|
y += getHeight(lineSpacing);
|
|
continue;
|
|
}
|
|
|
|
Glyph* glyph = getGlyph(character);
|
|
if (glyph)
|
|
lineWidth += glyph->advance.x;
|
|
}
|
|
|
|
return glm::vec2{lineWidth, y};
|
|
}
|
|
|
|
//
|
|
// TextCache.
|
|
//
|
|
|
|
float Font::getNewlineStartOffset(const std::string& text,
|
|
const unsigned int& charStart,
|
|
const float& xLen,
|
|
const Alignment& alignment)
|
|
{
|
|
switch (alignment) {
|
|
case ALIGN_LEFT: {
|
|
return 0;
|
|
}
|
|
case ALIGN_CENTER: {
|
|
int endChar = 0;
|
|
endChar = static_cast<int>(text.find('\n', charStart));
|
|
return (xLen - sizeText(text.substr(charStart,
|
|
static_cast<size_t>(endChar) != std::string::npos ?
|
|
endChar - charStart :
|
|
endChar))
|
|
.x) /
|
|
2.0f;
|
|
}
|
|
case ALIGN_RIGHT: {
|
|
int endChar = static_cast<int>(text.find('\n', charStart));
|
|
return xLen - (sizeText(text.substr(charStart,
|
|
static_cast<size_t>(endChar) != std::string::npos ?
|
|
endChar - charStart :
|
|
endChar))
|
|
.x);
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
TextCache* Font::buildTextCache(const std::string& text,
|
|
glm::vec2 offset,
|
|
unsigned int color,
|
|
float xLen,
|
|
Alignment alignment,
|
|
float lineSpacing,
|
|
bool noTopMargin)
|
|
{
|
|
float x = offset[0] + (xLen != 0 ? getNewlineStartOffset(text, 0, xLen, alignment) : 0);
|
|
float yTop = 0;
|
|
float yBot = 0;
|
|
|
|
if (noTopMargin) {
|
|
yTop = 0;
|
|
yBot = getHeight(1.5);
|
|
}
|
|
else {
|
|
yTop = getGlyph('S')->bearing.y;
|
|
yBot = getHeight(lineSpacing);
|
|
}
|
|
|
|
float y = offset[1] + (yBot + yTop) / 2.0f;
|
|
|
|
// Vertices by texture.
|
|
std::map<FontTexture*, std::vector<Renderer::Vertex>> vertMap;
|
|
|
|
size_t cursor = 0;
|
|
while (cursor < text.length()) {
|
|
// Also advances cursor.
|
|
unsigned int character = Utils::String::chars2Unicode(text, cursor);
|
|
Glyph* glyph;
|
|
|
|
// Invalid character.
|
|
if (character == 0)
|
|
continue;
|
|
|
|
if (character == '\n') {
|
|
y += getHeight(lineSpacing);
|
|
x = offset[0] + (xLen != 0 ?
|
|
getNewlineStartOffset(text,
|
|
static_cast<const unsigned int>(
|
|
cursor) /* cursor is already advanced */,
|
|
xLen, alignment) :
|
|
0);
|
|
continue;
|
|
}
|
|
|
|
glyph = getGlyph(character);
|
|
if (glyph == nullptr)
|
|
continue;
|
|
|
|
std::vector<Renderer::Vertex>& verts = vertMap[glyph->texture];
|
|
size_t oldVertSize = verts.size();
|
|
verts.resize(oldVertSize + 6);
|
|
Renderer::Vertex* vertices = verts.data() + oldVertSize;
|
|
|
|
const float glyphStartX{x + glyph->bearing.x};
|
|
const glm::ivec2& textureSize{glyph->texture->textureSize};
|
|
const unsigned int convertedColor = Renderer::convertRGBAToABGR(color);
|
|
|
|
vertices[1] = {{glyphStartX, y - glyph->bearing.y},
|
|
{glyph->texPos.x, glyph->texPos.y},
|
|
convertedColor};
|
|
vertices[2] = {{glyphStartX, y - glyph->bearing.y + (glyph->texSize.y * textureSize.y)},
|
|
{glyph->texPos.x, glyph->texPos.y + glyph->texSize.y},
|
|
convertedColor};
|
|
vertices[3] = {{glyphStartX + glyph->texSize.x * textureSize.x, y - glyph->bearing.y},
|
|
{glyph->texPos.x + glyph->texSize.x, glyph->texPos.y},
|
|
convertedColor};
|
|
vertices[4] = {{glyphStartX + glyph->texSize.x * textureSize.x,
|
|
y - glyph->bearing.y + (glyph->texSize.y * textureSize.y)},
|
|
{glyph->texPos.x + glyph->texSize.x, glyph->texPos.y + glyph->texSize.y},
|
|
convertedColor};
|
|
|
|
// Round vertices.
|
|
for (int i = 1; i < 5; ++i)
|
|
vertices[i].pos = glm::round(vertices[i].pos);
|
|
|
|
// Make duplicates of first and last vertex so this can be rendered as a triangle strip.
|
|
vertices[0] = vertices[1];
|
|
vertices[5] = vertices[4];
|
|
|
|
// Advance.
|
|
x += glyph->advance.x;
|
|
}
|
|
|
|
TextCache* cache = new TextCache();
|
|
cache->vertexLists.resize(vertMap.size());
|
|
cache->metrics = {sizeText(text, lineSpacing)};
|
|
|
|
unsigned int i = 0;
|
|
for (auto it = vertMap.cbegin(); it != vertMap.cend(); ++it) {
|
|
TextCache::VertexList& vertList = cache->vertexLists.at(i);
|
|
|
|
vertList.textureIdPtr = &it->first->textureId;
|
|
vertList.verts = it->second;
|
|
}
|
|
|
|
clearFaceCache();
|
|
return cache;
|
|
}
|
|
|
|
TextCache* Font::buildTextCache(const std::string& text,
|
|
float offsetX,
|
|
float offsetY,
|
|
unsigned int color,
|
|
float lineSpacing,
|
|
bool noTopMargin)
|
|
{
|
|
return buildTextCache(text, glm::vec2{offsetX, offsetY}, color, 0.0f, ALIGN_LEFT, lineSpacing,
|
|
noTopMargin);
|
|
}
|
|
|
|
void TextCache::setColor(unsigned int color)
|
|
{
|
|
const unsigned int convertedColor = Renderer::convertRGBAToABGR(color);
|
|
|
|
for (auto it = vertexLists.begin(); it != vertexLists.end(); ++it)
|
|
for (auto it2 = it->verts.begin(); it2 != it->verts.end(); ++it2)
|
|
it2->col = convertedColor;
|
|
}
|
|
|
|
std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem,
|
|
unsigned int properties,
|
|
const std::shared_ptr<Font>& orig)
|
|
{
|
|
using namespace ThemeFlags;
|
|
if (!(properties & FONT_PATH) && !(properties & FONT_SIZE))
|
|
return orig;
|
|
|
|
std::shared_ptr<Font> font;
|
|
int size = (orig ? orig->mSize : FONT_SIZE_MEDIUM);
|
|
std::string path = (orig ? orig->mPath : getDefaultPath());
|
|
|
|
float sh = static_cast<float>(Renderer::getScreenHeight());
|
|
if (properties & FONT_SIZE && elem->has("fontSize"))
|
|
size = static_cast<int>(sh * elem->get<float>("fontSize"));
|
|
if (properties & FONT_PATH && elem->has("fontPath"))
|
|
path = elem->get<std::string>("fontPath");
|
|
|
|
if (!((path[0] == ':') && (path[1] == '/')) && !Utils::FileSystem::exists(path)) {
|
|
LOG(LogError) << "Font file \"" << path
|
|
<< "\" defined by the theme does not exist, "
|
|
"falling back to \""
|
|
<< getDefaultPath() << "\"";
|
|
path = getDefaultPath();
|
|
}
|
|
|
|
return get(size, path);
|
|
}
|