From cdb6f8f44b68ec17a2c8bfcf356b3b7e13db99b7 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Thu, 13 Jun 2024 20:20:14 +0900 Subject: [PATCH 1/2] support tofu (missing glyphs) in font atlas --- include/limitless/text/font_atlas.hpp | 65 ++++++++--- src/limitless/text/font_atlas.cpp | 150 ++++++++++++++++---------- 2 files changed, 145 insertions(+), 70 deletions(-) diff --git a/include/limitless/text/font_atlas.hpp b/include/limitless/text/font_atlas.hpp index 86f4c55d..c55ac687 100644 --- a/include/limitless/text/font_atlas.hpp +++ b/include/limitless/text/font_atlas.hpp @@ -2,21 +2,40 @@ #include #include -#include #include + #include #include -#include +#include + +#include #include #include FT_FREETYPE_H #include namespace Limitless { - struct FontCharacter { + struct FontChar { + /** + * Character glyph bounding box size, in pixels. + */ glm::ivec2 size; + + /** + * Character glyph offset inside its bounding box, in pixels. + */ glm::ivec2 bearing; + + /** + * Horizontal advance width in (1/26.6) pixel units. + * It is the distance until next character used during typesetting. + * Multiply by ~27 (or 32, which is faster) to get value in pixels. + */ uint32_t advance; + + /** + * UVs of this character glyph on font atlas texture. + */ std::array uvs; }; @@ -26,21 +45,20 @@ namespace Limitless { }; class FontAtlas { - private: - std::map chars; - std::shared_ptr texture; - FT_Face face {}; - uint32_t font_size; - - static constexpr auto TAB_WIDTH_IN_SPACES = 4; - - bool isSynthetizedGlyph(uint32_t utf32_codepoint) const noexcept; public: FontAtlas(const fs::path& path, uint32_t pixel_size); ~FontAtlas(); + /** + * Return font vertical size in pixels. + */ [[nodiscard]] auto getFontSize() const noexcept { return font_size; } - [[nodiscard]] const auto& getFontCharacter(uint32_t ucs) const { return chars.at(ucs); } + + /** + * Return font character for given Unicode codepoint. + * If font does not have it, then ""missing/tofu" font character is returned. + */ + [[nodiscard]] const FontChar& getFontChar(uint32_t utf32_codepoint) const noexcept; [[nodiscard]] const auto& getTexture() const { return texture; } @@ -54,6 +72,23 @@ namespace Limitless { */ [[nodiscard]] glm::vec2 getTextSize(const std::string& text) const; - std::vector getSelectionGeometry(std::string_view text, size_t begin, size_t end); + /** + * Return selection geometry for UTF-8 encoded string, from [@begin; @end) range position runes. + */ + std::vector getSelectionGeometry(std::string_view text, size_t begin, size_t end) const; + + private: + std::unordered_map chars; + std::shared_ptr texture; + FT_Face face {}; + uint32_t font_size; + + static constexpr auto TAB_WIDTH_IN_SPACES = 4; + + /** + * Return font character for given Unicode codepoint. + * If font does not have it, then "missing/tofu" font character is returned. + */ + FontChar& fontCharFor(uint32_t utf32_codepoint) noexcept; }; -} \ No newline at end of file +} diff --git a/src/limitless/text/font_atlas.cpp b/src/limitless/text/font_atlas.cpp index 058bdbc1..e9124ca6 100644 --- a/src/limitless/text/font_atlas.cpp +++ b/src/limitless/text/font_atlas.cpp @@ -8,6 +8,9 @@ using namespace Limitless; using namespace std::literals::string_literals; +static constexpr const uint32_t UNDEFINED_GLYPH_INDEX = 0; +static constexpr const uint32_t UNDEFINED_GLYPH_CHAR_CODE = 0; + FontAtlas::FontAtlas(const fs::path& path, uint32_t pixel_size) : font_size {pixel_size} { static FT_Library ft {nullptr}; @@ -22,7 +25,7 @@ FontAtlas::FontAtlas(const fs::path& path, uint32_t pixel_size) throw font_error{"Failed to load the font at path"s + path.string()}; } - FT_Set_Pixel_Sizes(face, 0, size); + FT_Set_Pixel_Sizes(face, 0, font_size); struct GlyphInfo { glm::ivec2 bearing; @@ -39,31 +42,44 @@ FontAtlas::FontAtlas(const fs::path& path, uint32_t pixel_size) FT_ULong char_code; FT_UInt glyph_index; - size_t char_count = 0; - for ( - char_code = FT_Get_First_Char(face, &glyph_index); - glyph_index != 0; - char_code = FT_Get_Next_Char(face, char_code, &glyph_index) - ) { - if (FT_Load_Char(face, char_code, FT_LOAD_RENDER) != 0) { - throw font_error {"Failed to load char with code " + std::to_string(char_code)}; - } - - const auto& glyph_bitmap = face->glyph->bitmap; + auto loadGlyphFrom = [&](const FT_GlyphSlot ft_glyph, uint32_t char_code){ + const auto& glyph_bitmap = ft_glyph->bitmap; if (glyph_bitmap.pitch < 0) { throw font_error {"font has negative glyph bitmap pitch, which is not supported"}; } auto char_bitmap = std::vector(glyph_bitmap.pitch * glyph_bitmap.rows); memcpy(char_bitmap.data(), glyph_bitmap.buffer, char_bitmap.size()); - glyph_for_char.emplace(char_code, GlyphInfo { - {face->glyph->bitmap_left, face->glyph->bitmap_top}, - static_cast(face->glyph->advance.x), + + auto [_, emplaced] = glyph_for_char.emplace(char_code, GlyphInfo { + {ft_glyph->bitmap_left, ft_glyph->bitmap_top}, + static_cast(ft_glyph->advance.x), std::move(char_bitmap) }); - ++char_count; - packer_rects.emplace_back(stbrp_rect{char_code, glyph_bitmap.width, glyph_bitmap.rows, 0, 0, 0}); + // TODO: support char glyph variants? + if (emplaced) { + packer_rects.emplace_back(stbrp_rect{char_code, glyph_bitmap.width, glyph_bitmap.rows, 0, 0, 0}); + } + }; + + if (FT_Load_Glyph(face, UNDEFINED_GLYPH_INDEX, FT_LOAD_RENDER) != 0) { + throw font_error {"Failed to load tofu (missing) glyph"}; + } + + // Put "tofu" as 0 char code glyph -- null terminators (\0) are not normally rendered anyway. + loadGlyphFrom(face->glyph, UNDEFINED_GLYPH_CHAR_CODE); + + for ( + char_code = FT_Get_First_Char(face, &glyph_index); + glyph_index != 0; + char_code = FT_Get_Next_Char(face, char_code, &glyph_index) + ) { + if (FT_Load_Char(face, char_code, FT_LOAD_RENDER) != 0) { + throw font_error {"Failed to load char with code " + std::to_string(char_code)}; + } + + loadGlyphFrom(face->glyph, char_code); } packer_nodes.resize(packer_rects.size()); @@ -109,7 +125,7 @@ FontAtlas::FontAtlas(const fs::path& path, uint32_t pixel_size) static_cast(rect.y + rect.h) / static_cast(atlas_size.y) }; - chars.emplace(char_code, FontCharacter{ + chars.emplace(char_code, FontChar{ {rect.w, rect.h}, glyph_info.bearing, glyph_info.advance, @@ -165,9 +181,12 @@ static size_t utf8CharLength(char c) { return 0; } -// Invokes void(uint32_t) function for each unicode code point of a UTF-8 encoded string. +/** + * Invokes bool(uint32_t) function for each Unicode codepoint of a UTF-8 encoded string. + * If that function returns false, then iteration is stopped. + */ template -static void forEachUnicodeCodepoint(const std::string& str, T&& func) { +static void forEachUnicodeCodepoint(std::string_view str, T&& func) { size_t i = 0; while (i < str.size()) { size_t char_len = utf8CharLength(str[i]); @@ -185,7 +204,9 @@ static void forEachUnicodeCodepoint(const std::string& str, T&& func) { codepoint = (codepoint << 6) | (continuation_byte & 0x3F); } - func(codepoint); + if (!func(codepoint)) { + return; + } i += char_len; } @@ -201,10 +222,10 @@ std::vector FontAtlas::generate(const std::string& text) const { if (cp == '\n') { offset.y -= font_size; offset.x = 0; - return; + return true; } - auto& fc = chars.at(cp); + const auto& fc = getFontChar(cp); float x = offset.x + fc.bearing.x; float y = offset.y + fc.bearing.y - fc.size.y; @@ -218,6 +239,7 @@ std::vector FontAtlas::generate(const std::string& text) const { vertices.emplace_back(glm::vec2{x + fc.size.x, y + fc.size.y}, fc.uvs[3]); offset.x += (fc.advance >> 6); + return true; }); return vertices; @@ -228,7 +250,7 @@ glm::vec2 FontAtlas::getTextSize(const std::string& text) const { float size {}; forEachUnicodeCodepoint(text, [&](uint32_t cp) { - auto& fc = chars.at(cp); + const auto& fc = getFontChar(cp); size += (fc.advance >> 6); @@ -238,16 +260,16 @@ glm::vec2 FontAtlas::getTextSize(const std::string& text) const { } max_size.x = std::max(max_size.x, size); + return true; }); return max_size; } -std::vector FontAtlas::getSelectionGeometry(std::string_view text, size_t begin, size_t end) { +std::vector FontAtlas::getSelectionGeometry(std::string_view text, size_t begin, size_t end) const { std::vector vertices; - // adding rect function - auto add_rect = [&] (glm::vec2 pos, glm::vec2 size) { + auto addRect = [&] (glm::vec2 pos, glm::vec2 size) { vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}, glm::vec2{}); vertices.emplace_back(glm::vec2{pos.x, -pos.y}, glm::vec2{}); vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}, glm::vec2{}); @@ -257,7 +279,6 @@ std::vector FontAtlas::getSelectionGeometry(std::string_view text, s vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y + size.y}, glm::vec2{}); }; - glm::ivec2 offset{}; // finds character max y value @@ -265,43 +286,62 @@ std::vector FontAtlas::getSelectionGeometry(std::string_view text, s offset.y = std::max(offset.y, fc.size.y - fc.bearing.y); } - // finds first offset - for (size_t i = 0; i < begin; ++i) { - offset.x += (chars.at(text[i]).advance >> 6); - if (text[i] == '\n') { - offset.x = 0; - offset.y += font_size; - } - } + size_t pos = 0; + size_t size = 0; - size_t size{}; - for (size_t i = begin; i < end; ++i) { - if (text[i] == '\n') { - add_rect({offset.x, offset.y}, glm::vec2{size, font_size}); - size = 0; - offset.x = 0; - offset.y += font_size; + forEachUnicodeCodepoint(text, [&](uint32_t cp) { + if (pos < begin) { + // Skip until selection range starts. + offset.x += getFontChar(cp).advance >> 6; + if (cp == '\n') { + offset.x = 0; + offset.y += font_size; + } + ++pos; + return true; - if (i != end - 1) { - if (text[i + 1] == '\n') { - size += chars.at(' ').advance >> 6; + } else if (pos < end) { + if (cp == '\n') { + if (size != 0) { + // Finish this selection line. + addRect({offset.x, offset.y}, glm::vec2{size, font_size}); + size = 0; + offset.x = 0; + offset.y += font_size; + } else { + // An empty line, still add small selection geometry for it. + size = getFontChar(' ').advance >> 6; } + } else { + size += getFontChar(cp).advance >> 6; } + ++pos; + return true; + } else { - size += chars.at(text[i]).advance >> 6; + return false; } - } + }); - add_rect({offset.x, offset.y}, glm::vec2{size, font_size}); + addRect({offset.x, offset.y}, glm::vec2{size, font_size}); return vertices; } -bool FontAtlas::isSynthetizedGlyph(uint32_t utf32_codepoint) const noexcept { - switch (utf32_codepoint) { - case '\t': - return true; - default: - return false; +FontChar& FontAtlas::fontCharFor(uint32_t utf32_codepoint) noexcept { + auto it = chars.find(utf32_codepoint); + if (it == chars.end()) { + return chars.at(UNDEFINED_GLYPH_CHAR_CODE); + } + + return it->second; +} + +const FontChar& FontAtlas::getFontChar(uint32_t utf32_codepoint) const noexcept { + auto it = chars.find(utf32_codepoint); + if (it == chars.end()) { + return chars.at(UNDEFINED_GLYPH_CHAR_CODE); } + + return it->second; } From 160582b048b9700b7d0971404f2be04681f74f8f Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Thu, 13 Jun 2024 20:23:00 +0900 Subject: [PATCH 2/2] remove unused private FontAtlas::fontCharFor method --- include/limitless/text/font_atlas.hpp | 6 ------ src/limitless/text/font_atlas.cpp | 9 --------- 2 files changed, 15 deletions(-) diff --git a/include/limitless/text/font_atlas.hpp b/include/limitless/text/font_atlas.hpp index c55ac687..d3c0cd42 100644 --- a/include/limitless/text/font_atlas.hpp +++ b/include/limitless/text/font_atlas.hpp @@ -84,11 +84,5 @@ namespace Limitless { uint32_t font_size; static constexpr auto TAB_WIDTH_IN_SPACES = 4; - - /** - * Return font character for given Unicode codepoint. - * If font does not have it, then "missing/tofu" font character is returned. - */ - FontChar& fontCharFor(uint32_t utf32_codepoint) noexcept; }; } diff --git a/src/limitless/text/font_atlas.cpp b/src/limitless/text/font_atlas.cpp index e9124ca6..551ad24d 100644 --- a/src/limitless/text/font_atlas.cpp +++ b/src/limitless/text/font_atlas.cpp @@ -328,15 +328,6 @@ std::vector FontAtlas::getSelectionGeometry(std::string_view text, s return vertices; } -FontChar& FontAtlas::fontCharFor(uint32_t utf32_codepoint) noexcept { - auto it = chars.find(utf32_codepoint); - if (it == chars.end()) { - return chars.at(UNDEFINED_GLYPH_CHAR_CODE); - } - - return it->second; -} - const FontChar& FontAtlas::getFontChar(uint32_t utf32_codepoint) const noexcept { auto it = chars.find(utf32_codepoint); if (it == chars.end()) {