mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2025-02-18 04:45:39 +00:00
Added initial text shaping support
This commit is contained in:
parent
71ccaf193e
commit
539cdd8146
|
@ -3,7 +3,7 @@
|
||||||
// ES-DE Frontend
|
// ES-DE Frontend
|
||||||
// Font.h
|
// Font.h
|
||||||
//
|
//
|
||||||
// Loading, unloading, caching and rendering of fonts.
|
// Loading, unloading, caching, shaping and rendering of fonts.
|
||||||
// Also functions for text wrapping and similar.
|
// Also functions for text wrapping and similar.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@
|
||||||
Font::Font(float size, const std::string& path)
|
Font::Font(float size, const std::string& path)
|
||||||
: mRenderer {Renderer::getInstance()}
|
: mRenderer {Renderer::getInstance()}
|
||||||
, mPath(path)
|
, mPath(path)
|
||||||
|
, mFontHB {nullptr}
|
||||||
|
, mLastFontHB {nullptr}
|
||||||
|
, mBufHB {nullptr}
|
||||||
, mFontSize {size}
|
, mFontSize {size}
|
||||||
, mLetterHeight {0.0f}
|
, mLetterHeight {0.0f}
|
||||||
, mMaxGlyphHeight {static_cast<int>(std::round(size))}
|
, mMaxGlyphHeight {static_cast<int>(std::round(size))}
|
||||||
|
@ -31,18 +34,31 @@ Font::Font(float size, const std::string& path)
|
||||||
LOG(LogWarning) << "Requested font size too large, changing to maximum supported size";
|
LOG(LogWarning) << "Requested font size too large, changing to maximum supported size";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sLibrary)
|
if (!sLibrary) {
|
||||||
initLibrary();
|
initLibrary();
|
||||||
|
sFallbackFonts = getFallbackFontPaths();
|
||||||
|
}
|
||||||
|
|
||||||
// Always initialize ASCII characters.
|
const std::string fontPath {ResourceManager::getInstance().getResourcePath(mPath)};
|
||||||
for (unsigned int i {32}; i < 127; ++i)
|
hb_blob_t* blobHB {hb_blob_create_from_file(fontPath.c_str())};
|
||||||
getGlyph(i);
|
hb_face_t* faceHB {hb_face_create(blobHB, 0)};
|
||||||
|
mFontHB = hb_font_create(faceHB);
|
||||||
|
hb_font_set_ptem(mFontHB, mFontSize);
|
||||||
|
hb_face_destroy(faceHB);
|
||||||
|
hb_blob_destroy(blobHB);
|
||||||
|
|
||||||
clearFaceCache();
|
mBufHB = hb_buffer_create();
|
||||||
|
|
||||||
|
ResourceData data {ResourceManager::getInstance().getFileData(fontPath)};
|
||||||
|
mFontFace = std::make_unique<FontFace>(std::move(data), mFontSize, path, mFontHB);
|
||||||
}
|
}
|
||||||
|
|
||||||
Font::~Font()
|
Font::~Font()
|
||||||
{
|
{
|
||||||
|
mFontFace.reset();
|
||||||
|
hb_buffer_destroy(mBufHB);
|
||||||
|
hb_font_destroy(mFontHB);
|
||||||
|
|
||||||
unload(ResourceManager::getInstance());
|
unload(ResourceManager::getInstance());
|
||||||
|
|
||||||
auto fontEntry = sFontMap.find(std::tuple<float, std::string>(mFontSize, mPath));
|
auto fontEntry = sFontMap.find(std::tuple<float, std::string>(mFontSize, mPath));
|
||||||
|
@ -51,6 +67,9 @@ Font::~Font()
|
||||||
sFontMap.erase(fontEntry);
|
sFontMap.erase(fontEntry);
|
||||||
|
|
||||||
if (sFontMap.empty() && sLibrary) {
|
if (sFontMap.empty() && sLibrary) {
|
||||||
|
for (auto& font : sFallbackFonts)
|
||||||
|
hb_font_destroy(font.fontHB);
|
||||||
|
sFallbackFonts.clear();
|
||||||
FT_Done_FreeType(sLibrary);
|
FT_Done_FreeType(sLibrary);
|
||||||
sLibrary = nullptr;
|
sLibrary = nullptr;
|
||||||
}
|
}
|
||||||
|
@ -155,62 +174,163 @@ TextCache* Font::buildTextCache(const std::string& text,
|
||||||
// Vertices by texture.
|
// Vertices by texture.
|
||||||
std::map<FontTexture*, std::vector<Renderer::Vertex>> vertMap;
|
std::map<FontTexture*, std::vector<Renderer::Vertex>> vertMap;
|
||||||
|
|
||||||
|
// HarfBuzz segments.
|
||||||
|
std::vector<ShapeSegment> segmentsHB;
|
||||||
|
|
||||||
|
{
|
||||||
|
hb_font_t* lastFont {nullptr};
|
||||||
|
unsigned int lastCursor {0};
|
||||||
|
unsigned int byteLength {0};
|
||||||
|
bool addSegment {false};
|
||||||
|
bool shapeSegment {true};
|
||||||
|
bool lastWasNoShaping {false};
|
||||||
|
size_t textCursor {0};
|
||||||
|
size_t lastFlushPos {0};
|
||||||
|
|
||||||
|
while (textCursor < text.length()) {
|
||||||
|
addSegment = false;
|
||||||
|
shapeSegment = true;
|
||||||
|
lastCursor = textCursor;
|
||||||
|
const unsigned int unicode {Utils::String::chars2Unicode(text, textCursor)};
|
||||||
|
Glyph* currGlyph {getGlyph(unicode)};
|
||||||
|
byteLength = textCursor - lastCursor;
|
||||||
|
|
||||||
|
if (unicode == '\'' || unicode == '\n') {
|
||||||
|
// HarfBuzz converts ' and newline characters to invalid characters, so we
|
||||||
|
// need to exclude these from getting shaped. This means adding a new segment.
|
||||||
|
addSegment = true;
|
||||||
|
if (!lastWasNoShaping) {
|
||||||
|
textCursor -= byteLength;
|
||||||
|
if (lastFlushPos == textCursor)
|
||||||
|
addSegment = false;
|
||||||
|
lastWasNoShaping = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
shapeSegment = false;
|
||||||
|
lastWasNoShaping = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (textCursor == text.length()) {
|
||||||
|
// Last (and possibly only) segment for this text.
|
||||||
|
addSegment = true;
|
||||||
|
}
|
||||||
|
else if (lastFont != nullptr && lastFont != currGlyph->fontHB) {
|
||||||
|
// The font changed, which requires a new segment.
|
||||||
|
addSegment = true;
|
||||||
|
textCursor -= byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addSegment) {
|
||||||
|
ShapeSegment segment;
|
||||||
|
segment.startPos = lastFlushPos;
|
||||||
|
segment.length = textCursor - lastFlushPos;
|
||||||
|
segment.fontHB = (lastFont == nullptr ? currGlyph->fontHB : lastFont);
|
||||||
|
segment.doShape = shapeSegment;
|
||||||
|
if (!shapeSegment)
|
||||||
|
segment.substring = text.substr(lastFlushPos, textCursor - lastFlushPos);
|
||||||
|
|
||||||
|
segmentsHB.emplace_back(std::move(segment));
|
||||||
|
|
||||||
|
lastFlushPos = textCursor;
|
||||||
|
}
|
||||||
|
lastFont = currGlyph->fontHB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
size_t cursor {0};
|
size_t cursor {0};
|
||||||
while (cursor < text.length()) {
|
size_t length {0};
|
||||||
// Also advances cursor.
|
hb_glyph_info_t* glyphInfo {nullptr};
|
||||||
unsigned int character {Utils::String::chars2Unicode(text, cursor)};
|
hb_glyph_position_t* glyphPos {nullptr};
|
||||||
Glyph* glyph {nullptr};
|
unsigned int glyphCount {0};
|
||||||
|
|
||||||
// Invalid character.
|
for (auto& segment : segmentsHB) {
|
||||||
if (character == 0)
|
cursor = 0;
|
||||||
continue;
|
length = 0;
|
||||||
|
|
||||||
if (character == '\n') {
|
if (segment.doShape) {
|
||||||
y += getHeight(lineSpacing);
|
hb_buffer_reset(mBufHB);
|
||||||
x = offset[0] + (xLen != 0 ?
|
hb_buffer_add_utf8(mBufHB, text.c_str(), text.length(), segment.startPos,
|
||||||
getNewlineStartOffset(text,
|
segment.length);
|
||||||
|
hb_buffer_guess_segment_properties(mBufHB);
|
||||||
|
hb_shape(segment.fontHB, mBufHB, nullptr, 0);
|
||||||
|
|
||||||
|
glyphInfo = hb_buffer_get_glyph_infos(mBufHB, &glyphCount);
|
||||||
|
glyphPos = hb_buffer_get_glyph_positions(mBufHB, &glyphCount);
|
||||||
|
length = glyphCount;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
length = segment.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (cursor < length) {
|
||||||
|
unsigned int character {0};
|
||||||
|
|
||||||
|
if (segment.doShape) {
|
||||||
|
character = glyphInfo[cursor].codepoint;
|
||||||
|
++cursor;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This also advances the cursor.
|
||||||
|
character = Utils::String::chars2Unicode(segment.substring, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Glyph* glyph {nullptr};
|
||||||
|
|
||||||
|
// Invalid character.
|
||||||
|
if (character == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (character == '\n') {
|
||||||
|
y += getHeight(lineSpacing);
|
||||||
|
x = offset[0] +
|
||||||
|
(xLen != 0 ? getNewlineStartOffset(text,
|
||||||
static_cast<const unsigned int>(
|
static_cast<const unsigned int>(
|
||||||
cursor) /* cursor is already advanced */,
|
cursor) /* cursor is already advanced */,
|
||||||
xLen, alignment) :
|
xLen, alignment) :
|
||||||
0);
|
0);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.doShape)
|
||||||
|
glyph = getGlyphByIndex(character, segment.fontHB);
|
||||||
|
else
|
||||||
|
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};
|
||||||
|
|
||||||
|
vertices[1] = {
|
||||||
|
{glyphStartX, y - glyph->bearing.y}, {glyph->texPos.x, glyph->texPos.y}, color};
|
||||||
|
vertices[2] = {{glyphStartX, y - glyph->bearing.y + (glyph->texSize.y * textureSize.y)},
|
||||||
|
{glyph->texPos.x, glyph->texPos.y + glyph->texSize.y},
|
||||||
|
color};
|
||||||
|
vertices[3] = {{glyphStartX + glyph->texSize.x * textureSize.x, y - glyph->bearing.y},
|
||||||
|
{glyph->texPos.x + glyph->texSize.x, glyph->texPos.y},
|
||||||
|
color};
|
||||||
|
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},
|
||||||
|
color};
|
||||||
|
|
||||||
|
// Round vertices.
|
||||||
|
for (int i {1}; i < 5; ++i)
|
||||||
|
vertices[i].position = glm::round(vertices[i].position);
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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};
|
|
||||||
|
|
||||||
vertices[1] = {
|
|
||||||
{glyphStartX, y - glyph->bearing.y}, {glyph->texPos.x, glyph->texPos.y}, color};
|
|
||||||
vertices[2] = {{glyphStartX, y - glyph->bearing.y + (glyph->texSize.y * textureSize.y)},
|
|
||||||
{glyph->texPos.x, glyph->texPos.y + glyph->texSize.y},
|
|
||||||
color};
|
|
||||||
vertices[3] = {{glyphStartX + glyph->texSize.x * textureSize.x, y - glyph->bearing.y},
|
|
||||||
{glyph->texPos.x + glyph->texSize.x, glyph->texPos.y},
|
|
||||||
color};
|
|
||||||
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},
|
|
||||||
color};
|
|
||||||
|
|
||||||
// Round vertices.
|
|
||||||
for (int i {1}; i < 5; ++i)
|
|
||||||
vertices[i].position = glm::round(vertices[i].position);
|
|
||||||
|
|
||||||
// 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()};
|
TextCache* cache {new TextCache()};
|
||||||
|
@ -227,7 +347,6 @@ TextCache* Font::buildTextCache(const std::string& text,
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearFaceCache();
|
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -472,12 +591,15 @@ std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem,
|
||||||
|
|
||||||
size_t Font::getMemUsage() const
|
size_t Font::getMemUsage() const
|
||||||
{
|
{
|
||||||
|
// TODO: Summarize actual textures properly instead.
|
||||||
size_t memUsage {0};
|
size_t memUsage {0};
|
||||||
for (auto it = mTextures.cbegin(); it != mTextures.cend(); ++it)
|
for (auto it = mTextures.cbegin(); it != mTextures.cend(); ++it)
|
||||||
memUsage += (*it)->textureSize.x * (*it)->textureSize.y * 4;
|
memUsage += (*it)->textureSize.x * (*it)->textureSize.y * 4;
|
||||||
|
|
||||||
for (auto it = mFaceCache.cbegin(); it != mFaceCache.cend(); ++it)
|
for (auto it = sFallbackFonts.cbegin(); it != sFallbackFonts.cend(); ++it)
|
||||||
memUsage += it->second->data.length;
|
memUsage += it->face->data.length;
|
||||||
|
|
||||||
|
memUsage += mFontFace->data.length;
|
||||||
|
|
||||||
return memUsage;
|
return memUsage;
|
||||||
}
|
}
|
||||||
|
@ -500,32 +622,43 @@ size_t Font::getTotalMemUsage()
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> Font::getFallbackFontPaths()
|
std::vector<Font::FallbackFontCache> Font::getFallbackFontPaths()
|
||||||
{
|
{
|
||||||
std::vector<std::string> fontPaths;
|
std::vector<FallbackFontCache> fontPaths;
|
||||||
|
|
||||||
// Default application fonts.
|
// Default application fonts.
|
||||||
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-Regular.ttf");
|
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-Regular.ttf");
|
||||||
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-SemiBold.ttf");
|
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-SemiBold.ttf");
|
||||||
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-Bold.ttf");
|
ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-Bold.ttf");
|
||||||
|
|
||||||
// Ubuntu Condensed.
|
const std::vector<std::string> fallbackFonts {
|
||||||
fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/Ubuntu-C.ttf"));
|
// Ubuntu Condensed.
|
||||||
// Vera sans Unicode.
|
":/fonts/Ubuntu-C.ttf",
|
||||||
fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/DejaVuSans.ttf"));
|
// Vera sans Unicode.
|
||||||
// GNU FreeFont monospaced.
|
":/fonts/DejaVuSans.ttf",
|
||||||
fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/FreeMono.ttf"));
|
// GNU FreeFont monospaced.
|
||||||
// Various languages, such as Japanese and Chinese.
|
":/fonts/FreeMono.ttf",
|
||||||
fontPaths.push_back(
|
// Various languages, such as Japanese and Chinese.
|
||||||
ResourceManager::getInstance().getResourcePath(":/fonts/DroidSansFallbackFull.ttf"));
|
":/fonts/DroidSansFallbackFull.ttf",
|
||||||
// Korean.
|
// Korean
|
||||||
fontPaths.push_back(
|
":/fonts/NanumMyeongjo.ttf",
|
||||||
ResourceManager::getInstance().getResourcePath(":/fonts/NanumMyeongjo.ttf"));
|
// Font Awesome icon glyphs, used for various special symbols like stars, folders etc.
|
||||||
// Font Awesome icon glyphs, used for various special symbols like stars, folders etc.
|
":/fonts/fontawesome-webfont.ttf", ":/fonts/NotoEmoji.ttf"};
|
||||||
fontPaths.push_back(
|
|
||||||
ResourceManager::getInstance().getResourcePath(":/fonts/fontawesome-webfont.ttf"));
|
for (auto& font : fallbackFonts) {
|
||||||
// Google Noto Emoji.
|
FallbackFontCache fallbackFont;
|
||||||
fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/NotoEmoji.ttf"));
|
const std::string path {ResourceManager::getInstance().getResourcePath(font)};
|
||||||
|
fallbackFont.path = path;
|
||||||
|
hb_blob_t* blobHB {hb_blob_create_from_file(path.c_str())};
|
||||||
|
hb_face_t* faceHB {hb_face_create(blobHB, 0)};
|
||||||
|
hb_font_t* fontHB {hb_font_create(faceHB)};
|
||||||
|
fallbackFont.fontHB = fontHB;
|
||||||
|
hb_face_destroy(faceHB);
|
||||||
|
hb_blob_destroy(blobHB);
|
||||||
|
ResourceData data {ResourceManager::getInstance().getFileData(path)};
|
||||||
|
fallbackFont.face = std::make_shared<FontFace>(std::move(data), 10.0f, path, fontHB);
|
||||||
|
fontPaths.emplace_back(fallbackFont);
|
||||||
|
}
|
||||||
|
|
||||||
return fontPaths;
|
return fontPaths;
|
||||||
}
|
}
|
||||||
|
@ -594,7 +727,7 @@ void Font::FontTexture::deinitTexture()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Font::FontFace::FontFace(ResourceData&& d, float size, const std::string& path)
|
Font::FontFace::FontFace(ResourceData&& d, float size, const std::string& path, hb_font_t* fontArg)
|
||||||
: data {d}
|
: data {d}
|
||||||
{
|
{
|
||||||
if (FT_New_Memory_Face(sLibrary, d.ptr.get(), static_cast<FT_Long>(d.length), 0, &face) != 0) {
|
if (FT_New_Memory_Face(sLibrary, d.ptr.get(), static_cast<FT_Long>(d.length), 0, &face) != 0) {
|
||||||
|
@ -607,6 +740,7 @@ Font::FontFace::FontFace(ResourceData&& d, float size, const std::string& path)
|
||||||
// though as the glyphs will still be much more evenely sized across different resolutions.
|
// though as the glyphs will still be much more evenely sized across different resolutions.
|
||||||
FT_Set_Char_Size(face, static_cast<FT_F26Dot6>(0.0f), static_cast<FT_F26Dot6>(size * 64.0f), 0,
|
FT_Set_Char_Size(face, static_cast<FT_F26Dot6>(0.0f), static_cast<FT_F26Dot6>(size * 64.0f), 0,
|
||||||
0);
|
0);
|
||||||
|
fontHB = fontArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
Font::FontFace::~FontFace()
|
Font::FontFace::~FontFace()
|
||||||
|
@ -633,11 +767,31 @@ void Font::rebuildTextures()
|
||||||
|
|
||||||
// Re-upload the texture data.
|
// Re-upload the texture data.
|
||||||
for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); ++it) {
|
for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); ++it) {
|
||||||
FT_Face face {getFaceForChar(it->first)};
|
FT_Face* face {getFaceForChar(it->first)};
|
||||||
FT_GlyphSlot glyphSlot {face->glyph};
|
FT_GlyphSlot glyphSlot {(*face)->glyph};
|
||||||
|
|
||||||
// Load the glyph bitmap through FT.
|
// Load the glyph bitmap through FreeType.
|
||||||
FT_Load_Char(face, it->first, FT_LOAD_RENDER);
|
FT_Load_Char(*face, it->first, FT_LOAD_RENDER);
|
||||||
|
|
||||||
|
const glm::ivec2 glyphSize {glyphSlot->bitmap.width, glyphSlot->bitmap.rows};
|
||||||
|
const glm::ivec2 cursor {
|
||||||
|
static_cast<int>(it->second.texPos.x * it->second.texture->textureSize.x),
|
||||||
|
static_cast<int>(it->second.texPos.y * it->second.texture->textureSize.y)};
|
||||||
|
|
||||||
|
// Upload glyph bitmap to texture.
|
||||||
|
if (glyphSize.x > 0 && glyphSize.y > 0) {
|
||||||
|
mRenderer->updateTexture(it->second.texture->textureId, 0, Renderer::TextureType::RED,
|
||||||
|
cursor.x, cursor.y, glyphSize.x, glyphSize.y,
|
||||||
|
glyphSlot->bitmap.buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = mGlyphMapByIndex.cbegin(); it != mGlyphMapByIndex.cend(); ++it) {
|
||||||
|
FT_Face* face {getFaceForGlyphIndex(it->first.first, it->first.second)};
|
||||||
|
FT_GlyphSlot glyphSlot {(*face)->glyph};
|
||||||
|
|
||||||
|
// Load the glyph bitmap through FreeType.
|
||||||
|
FT_Load_Glyph(*face, it->first.first, FT_LOAD_RENDER);
|
||||||
|
|
||||||
const glm::ivec2 glyphSize {glyphSlot->bitmap.width, glyphSlot->bitmap.rows};
|
const glm::ivec2 glyphSize {glyphSlot->bitmap.width, glyphSlot->bitmap.rows};
|
||||||
const glm::ivec2 cursor {
|
const glm::ivec2 cursor {
|
||||||
|
@ -683,28 +837,46 @@ void Font::getTextureForNewGlyph(const glm::ivec2& glyphSize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FT_Face Font::getFaceForChar(unsigned int id)
|
FT_Face* Font::getFaceForChar(unsigned int id)
|
||||||
{
|
{
|
||||||
static const std::vector<std::string> fallbackFonts {getFallbackFontPaths()};
|
|
||||||
|
|
||||||
// Look for the glyph in our current font and then in the fallback fonts if needed.
|
// Look for the glyph in our current font and then in the fallback fonts if needed.
|
||||||
for (unsigned int i {0}; i < fallbackFonts.size() + 1; ++i) {
|
if (FT_Get_Char_Index(mFontFace->face, id) != 0) {
|
||||||
auto fit = mFaceCache.find(i);
|
mLastFontHB = mFontHB;
|
||||||
|
return &mFontFace->face;
|
||||||
if (fit == mFaceCache.cend()) {
|
|
||||||
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), mFontSize, mPath));
|
|
||||||
fit = mFaceCache.find(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FT_Get_Char_Index(fit->second->face, id) != 0)
|
|
||||||
return fit->second->face;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Couldn't find a valid glyph, return the "real" face so we get a "missing" character.
|
for (auto& font : sFallbackFonts) {
|
||||||
return mFaceCache.cbegin()->second->face;
|
if (FT_Get_Char_Index(font.face->face, id) != 0) {
|
||||||
|
// This is most definitely not thread safe.
|
||||||
|
FT_Set_Char_Size(font.face->face, static_cast<FT_F26Dot6>(0.0f),
|
||||||
|
static_cast<FT_F26Dot6>(mFontSize * 64.0f), 0, 0);
|
||||||
|
mLastFontHB = font.fontHB;
|
||||||
|
return &font.face->face;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't find a valid glyph, return the current font face so we get a "missing" character.
|
||||||
|
return &mFontFace->face;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Face* Font::getFaceForGlyphIndex(unsigned int id, hb_font_t* fontArg)
|
||||||
|
{
|
||||||
|
if (mFontFace->fontHB == fontArg && FT_Load_Glyph(mFontFace->face, id, FT_LOAD_RENDER) == 0) {
|
||||||
|
mLastFontHB = mFontHB;
|
||||||
|
return &mFontFace->face;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& font : sFallbackFonts) {
|
||||||
|
if (font.fontHB == fontArg && FT_Load_Glyph(font.face->face, id, FT_LOAD_RENDER) == 0) {
|
||||||
|
FT_Set_Char_Size(font.face->face, static_cast<FT_F26Dot6>(0.0f),
|
||||||
|
static_cast<FT_F26Dot6>(mFontSize * 64.0f), 0, 0);
|
||||||
|
mLastFontHB = font.fontHB;
|
||||||
|
return &font.face->face;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't find a valid glyph, return the current font face so we get a "missing" character.
|
||||||
|
return &mFontFace->face;
|
||||||
}
|
}
|
||||||
|
|
||||||
Font::Glyph* Font::getGlyph(const unsigned int id)
|
Font::Glyph* Font::getGlyph(const unsigned int id)
|
||||||
|
@ -715,14 +887,14 @@ Font::Glyph* Font::getGlyph(const unsigned int id)
|
||||||
return &it->second;
|
return &it->second;
|
||||||
|
|
||||||
// We need to create a new entry.
|
// We need to create a new entry.
|
||||||
FT_Face face {getFaceForChar(id)};
|
FT_Face* face {getFaceForChar(id)};
|
||||||
if (!face) {
|
if (!face) {
|
||||||
LOG(LogError) << "Couldn't find appropriate font face for character " << id << " for font "
|
LOG(LogError) << "Couldn't find appropriate font face for character " << id << " for font "
|
||||||
<< mPath;
|
<< mPath;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FT_GlyphSlot glyphSlot {face->glyph};
|
const FT_GlyphSlot glyphSlot {(*face)->glyph};
|
||||||
|
|
||||||
// If the font does not contain hinting information then force the use of the automatic
|
// If the font does not contain hinting information then force the use of the automatic
|
||||||
// hinter that is built into FreeType. Note: Using font-supplied hints generally looks worse
|
// hinter that is built into FreeType. Note: Using font-supplied hints generally looks worse
|
||||||
|
@ -730,7 +902,7 @@ Font::Glyph* Font::getGlyph(const unsigned int id)
|
||||||
// const bool hasHinting {static_cast<bool>(glyphSlot->face->face_flags & FT_FACE_FLAG_HINTER)};
|
// const bool hasHinting {static_cast<bool>(glyphSlot->face->face_flags & FT_FACE_FLAG_HINTER)};
|
||||||
const bool hasHinting {true};
|
const bool hasHinting {true};
|
||||||
|
|
||||||
if (FT_Load_Char(face, id,
|
if (FT_Load_Char(*face, id,
|
||||||
(hasHinting ?
|
(hasHinting ?
|
||||||
FT_LOAD_RENDER :
|
FT_LOAD_RENDER :
|
||||||
FT_LOAD_RENDER | FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_LIGHT))) {
|
FT_LOAD_RENDER | FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_LIGHT))) {
|
||||||
|
@ -758,6 +930,77 @@ Font::Glyph* Font::getGlyph(const unsigned int id)
|
||||||
// Create glyph.
|
// Create glyph.
|
||||||
Glyph& glyph {mGlyphMap[id]};
|
Glyph& glyph {mGlyphMap[id]};
|
||||||
|
|
||||||
|
glyph.fontHB = mLastFontHB;
|
||||||
|
glyph.texture = tex;
|
||||||
|
glyph.texPos = {cursor.x / static_cast<float>(tex->textureSize.x),
|
||||||
|
cursor.y / static_cast<float>(tex->textureSize.y)};
|
||||||
|
glyph.texSize = {glyphSize.x / static_cast<float>(tex->textureSize.x),
|
||||||
|
glyphSize.y / static_cast<float>(tex->textureSize.y)};
|
||||||
|
glyph.advance = {glyphSlot->metrics.horiAdvance >> 6, glyphSlot->metrics.vertAdvance >> 6};
|
||||||
|
glyph.bearing = {glyphSlot->metrics.horiBearingX >> 6, glyphSlot->metrics.horiBearingY >> 6};
|
||||||
|
glyph.rows = glyphSize.y;
|
||||||
|
|
||||||
|
// Upload glyph bitmap to texture.
|
||||||
|
if (glyphSize.x > 0 && glyphSize.y > 0) {
|
||||||
|
mRenderer->updateTexture(tex->textureId, 0, Renderer::TextureType::RED, cursor.x, cursor.y,
|
||||||
|
glyphSize.x, glyphSize.y, glyphSlot->bitmap.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return &glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
Font::Glyph* Font::getGlyphByIndex(const unsigned int id, hb_font_t* fontArg)
|
||||||
|
{
|
||||||
|
// Check if the glyph has already been loaded.
|
||||||
|
auto it = mGlyphMapByIndex.find(std::make_pair(id, fontArg));
|
||||||
|
if (it != mGlyphMapByIndex.cend())
|
||||||
|
return &it->second;
|
||||||
|
|
||||||
|
// We need to create a new entry.
|
||||||
|
FT_Face* face {getFaceForGlyphIndex(id, fontArg)};
|
||||||
|
if (!face) {
|
||||||
|
LOG(LogError) << "Couldn't find appropriate font face for character " << id << " for font "
|
||||||
|
<< mPath;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FT_GlyphSlot glyphSlot {(*face)->glyph};
|
||||||
|
|
||||||
|
// If the font does not contain hinting information then force the use of the automatic
|
||||||
|
// hinter that is built into FreeType. Note: Using font-supplied hints generally looks worse
|
||||||
|
// than using the auto-hinter so it's disabled for now.
|
||||||
|
// const bool hasHinting {static_cast<bool>(glyphSlot->face->face_flags & FT_FACE_FLAG_HINTER)};
|
||||||
|
const bool hasHinting {true};
|
||||||
|
|
||||||
|
if (FT_Load_Glyph(*face, id,
|
||||||
|
(hasHinting ?
|
||||||
|
FT_LOAD_RENDER :
|
||||||
|
FT_LOAD_RENDER | FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_LIGHT))) {
|
||||||
|
LOG(LogError) << "Couldn't find glyph for character " << id << " for font " << mPath
|
||||||
|
<< ", size " << mFontSize;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FontTexture* tex {nullptr};
|
||||||
|
glm::ivec2 cursor {0, 0};
|
||||||
|
const glm::ivec2 glyphSize {glyphSlot->bitmap.width, glyphSlot->bitmap.rows};
|
||||||
|
getTextureForNewGlyph(glyphSize, tex, cursor);
|
||||||
|
|
||||||
|
// This should (hopefully) never occur as size constraints are enforced earlier on.
|
||||||
|
if (tex == nullptr) {
|
||||||
|
LOG(LogError) << "Couldn't create glyph for character " << id << " for font " << mPath
|
||||||
|
<< ", size " << mFontSize << " (no suitable texture found)";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the letter 'S' as a size reference.
|
||||||
|
if (mLetterHeight == 0 && id == 'S')
|
||||||
|
mLetterHeight = static_cast<float>(glyphSize.y);
|
||||||
|
|
||||||
|
// Create glyph.
|
||||||
|
Glyph& glyph {mGlyphMapByIndex[std::make_pair(id, mLastFontHB)]};
|
||||||
|
|
||||||
|
glyph.fontHB = mLastFontHB;
|
||||||
glyph.texture = tex;
|
glyph.texture = tex;
|
||||||
glyph.texPos = {cursor.x / static_cast<float>(tex->textureSize.x),
|
glyph.texPos = {cursor.x / static_cast<float>(tex->textureSize.x),
|
||||||
cursor.y / static_cast<float>(tex->textureSize.y)};
|
cursor.y / static_cast<float>(tex->textureSize.y)};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// ES-DE Frontend
|
// ES-DE Frontend
|
||||||
// Font.h
|
// Font.h
|
||||||
//
|
//
|
||||||
// Loading, unloading, caching and rendering of fonts.
|
// Loading, unloading, caching, shaping and rendering of fonts.
|
||||||
// Also functions for text wrapping and similar.
|
// Also functions for text wrapping and similar.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
#include <ft2build.h>
|
#include <ft2build.h>
|
||||||
#include FT_FREETYPE_H
|
#include FT_FREETYPE_H
|
||||||
|
#include <hb-ft.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class TextCache;
|
class TextCache;
|
||||||
|
@ -165,13 +166,15 @@ private:
|
||||||
struct FontFace {
|
struct FontFace {
|
||||||
const ResourceData data;
|
const ResourceData data;
|
||||||
FT_Face face;
|
FT_Face face;
|
||||||
|
hb_font_t* fontHB;
|
||||||
|
|
||||||
FontFace(ResourceData&& d, float size, const std::string& path);
|
FontFace(ResourceData&& d, float size, const std::string& path, hb_font_t* fontArg);
|
||||||
virtual ~FontFace();
|
virtual ~FontFace();
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Glyph {
|
struct Glyph {
|
||||||
FontTexture* texture;
|
FontTexture* texture;
|
||||||
|
hb_font_t* fontHB;
|
||||||
glm::vec2 texPos;
|
glm::vec2 texPos;
|
||||||
glm::vec2 texSize; // In texels.
|
glm::vec2 texSize; // In texels.
|
||||||
glm::ivec2 advance;
|
glm::ivec2 advance;
|
||||||
|
@ -179,6 +182,28 @@ private:
|
||||||
int rows;
|
int rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FallbackFontCache {
|
||||||
|
std::string path;
|
||||||
|
std::shared_ptr<FontFace> face;
|
||||||
|
hb_font_t* fontHB;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShapeSegment {
|
||||||
|
unsigned int startPos;
|
||||||
|
unsigned int length;
|
||||||
|
hb_font_t* fontHB;
|
||||||
|
bool doShape;
|
||||||
|
std::string substring;
|
||||||
|
|
||||||
|
ShapeSegment()
|
||||||
|
: startPos {0}
|
||||||
|
, length {0}
|
||||||
|
, fontHB {nullptr}
|
||||||
|
, doShape {false}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Completely recreate the texture data for all textures based on mGlyphs information.
|
// Completely recreate the texture data for all textures based on mGlyphs information.
|
||||||
void rebuildTextures();
|
void rebuildTextures();
|
||||||
void unloadTextures();
|
void unloadTextures();
|
||||||
|
@ -187,26 +212,32 @@ private:
|
||||||
FontTexture*& texOut,
|
FontTexture*& texOut,
|
||||||
glm::ivec2& cursorOut);
|
glm::ivec2& cursorOut);
|
||||||
|
|
||||||
std::vector<std::string> getFallbackFontPaths();
|
std::vector<FallbackFontCache> getFallbackFontPaths();
|
||||||
FT_Face getFaceForChar(unsigned int id);
|
FT_Face* getFaceForChar(unsigned int id);
|
||||||
|
FT_Face* getFaceForGlyphIndex(unsigned int id, hb_font_t* fontArg);
|
||||||
Glyph* getGlyph(const unsigned int id);
|
Glyph* getGlyph(const unsigned int id);
|
||||||
|
Glyph* getGlyphByIndex(const unsigned int id, hb_font_t* fontArg);
|
||||||
|
|
||||||
float getNewlineStartOffset(const std::string& text,
|
float getNewlineStartOffset(const std::string& text,
|
||||||
const unsigned int& charStart,
|
const unsigned int& charStart,
|
||||||
const float& xLen,
|
const float& xLen,
|
||||||
const Alignment& alignment);
|
const Alignment& alignment);
|
||||||
|
|
||||||
void clearFaceCache() { mFaceCache.clear(); }
|
|
||||||
|
|
||||||
static inline FT_Library sLibrary {nullptr};
|
static inline FT_Library sLibrary {nullptr};
|
||||||
static inline std::map<std::tuple<float, std::string>, std::weak_ptr<Font>> sFontMap;
|
static inline std::map<std::tuple<float, std::string>, std::weak_ptr<Font>> sFontMap;
|
||||||
|
static inline std::vector<FallbackFontCache> sFallbackFonts;
|
||||||
|
|
||||||
Renderer* mRenderer;
|
Renderer* mRenderer;
|
||||||
|
std::unique_ptr<FontFace> mFontFace;
|
||||||
std::vector<std::unique_ptr<FontTexture>> mTextures;
|
std::vector<std::unique_ptr<FontTexture>> mTextures;
|
||||||
std::map<unsigned int, std::unique_ptr<FontFace>> mFaceCache;
|
|
||||||
std::map<unsigned int, Glyph> mGlyphMap;
|
std::map<unsigned int, Glyph> mGlyphMap;
|
||||||
|
std::map<std::pair<unsigned int, hb_font_t*>, Glyph> mGlyphMapByIndex;
|
||||||
|
|
||||||
const std::string mPath;
|
const std::string mPath;
|
||||||
|
hb_font_t* mFontHB;
|
||||||
|
hb_font_t* mLastFontHB;
|
||||||
|
hb_buffer_t* mBufHB;
|
||||||
|
|
||||||
float mFontSize;
|
float mFontSize;
|
||||||
float mLetterHeight;
|
float mLetterHeight;
|
||||||
int mMaxGlyphHeight;
|
int mMaxGlyphHeight;
|
||||||
|
@ -215,7 +246,7 @@ private:
|
||||||
// Used to store a sort of "pre-rendered" string.
|
// Used to store a sort of "pre-rendered" string.
|
||||||
// When a TextCache is constructed (Font::buildTextCache()), the vertices and texture coordinates
|
// When a TextCache is constructed (Font::buildTextCache()), the vertices and texture coordinates
|
||||||
// of the string are calculated and stored in the TextCache object. Rendering a previously
|
// of the string are calculated and stored in the TextCache object. Rendering a previously
|
||||||
// constructed TextCache (Font::renderTextCache) every frame is MUCH faster than rebuilding
|
// constructed TextCache (Font::renderTextCache) every frame is much faster than rebuilding
|
||||||
// one every frame. Keep in mind you still need the Font object to render a TextCache (as the
|
// one every frame. Keep in mind you still need the Font object to render a TextCache (as the
|
||||||
// Font holds the OpenGL texture), and if a Font changes your TextCache may become invalid.
|
// Font holds the OpenGL texture), and if a Font changes your TextCache may become invalid.
|
||||||
class TextCache
|
class TextCache
|
||||||
|
|
Loading…
Reference in a new issue