From 9502942b7b5764e49dfa0129b9077cadb7580c4a Mon Sep 17 00:00:00 2001 From: Sasha Szpakowski Date: Wed, 28 Dec 2022 19:46:48 -0400 Subject: [PATCH] font rendering: reimplement tab-as-spaces support. --- src/modules/font/GenericShaper.cpp | 6 +- src/modules/font/TextShaper.cpp | 46 +++++--- src/modules/font/TextShaper.h | 8 ++ src/modules/font/freetype/HarfbuzzShaper.cpp | 118 ++++++++++++++----- src/modules/font/freetype/HarfbuzzShaper.h | 8 ++ src/modules/graphics/Font.cpp | 23 ---- src/modules/graphics/Font.h | 5 - 7 files changed, 136 insertions(+), 78 deletions(-) diff --git a/src/modules/font/GenericShaper.cpp b/src/modules/font/GenericShaper.cpp index 74d1b50d4..d89b11cfa 100644 --- a/src/modules/font/GenericShaper.cpp +++ b/src/modules/font/GenericShaper.cpp @@ -167,7 +167,7 @@ int GenericShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ran float newwidth = w + getKerning(prevglyph, g) + getGlyphAdvance(g); // Only wrap when there's a non-space character. - if (newwidth > wraplimit && g != ' ') + if (newwidth > wraplimit && !isWhitespace(g)) { // Rewind to the last seen space when wrapping. if (lastspaceindex != -1) @@ -179,10 +179,10 @@ int GenericShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ran } // Don't count trailing spaces in the output width. - if (g == ' ') + if (isWhitespace(g)) { lastspaceindex = i; - if (prevglyph != ' ') + if (!isWhitespace(prevglyph)) widthbeforelastspace = w; } else diff --git a/src/modules/font/TextShaper.cpp b/src/modules/font/TextShaper.cpp index 0b384081d..8d78b2ac3 100644 --- a/src/modules/font/TextShaper.cpp +++ b/src/modules/font/TextShaper.cpp @@ -30,7 +30,7 @@ namespace love namespace font { -void getCodepointsFromString(const std::string& text, std::vector& codepoints) +void getCodepointsFromString(const std::string &text, std::vector &codepoints) { codepoints.reserve(text.size()); @@ -45,20 +45,20 @@ void getCodepointsFromString(const std::string& text, std::vector& codep codepoints.push_back(g); } } - catch (utf8::exception& e) + catch (utf8::exception &e) { throw love::Exception("UTF-8 decoding error: %s", e.what()); } } -void getCodepointsFromString(const std::vector& strs, ColoredCodepoints& codepoints) +void getCodepointsFromString(const std::vector &strs, ColoredCodepoints &codepoints) { if (strs.empty()) return; codepoints.cps.reserve(strs[0].str.size()); - for (const ColoredString& cstr : strs) + for (const ColoredString &cstr : strs) { // No need to add the color if the string is empty anyway, and the code // further on assumes no two colors share the same starting position. @@ -87,7 +87,10 @@ TextShaper::TextShaper(Rasterizer *rasterizer) , dpiScales{rasterizer->getDPIScale()} , height(floorf(rasterizer->getHeight() / rasterizer->getDPIScale() + 0.5f)) , lineHeight(1) + , useSpacesForTab(false) { + if (!rasterizer->hasGlyph('\t')) + useSpacesForTab = true; } TextShaper::~TextShaper() @@ -141,7 +144,7 @@ bool TextShaper::hasGlyph(uint32 glyph) const return false; } -bool TextShaper::hasGlyphs(const std::string& text) const +bool TextShaper::hasGlyphs(const std::string &text) const { if (text.size() == 0) return false; @@ -178,7 +181,7 @@ float TextShaper::getKerning(uint32 leftglyph, uint32 rightglyph) float k = 0.0f; bool found = false; - for (const auto& r : rasterizers) + for (const auto &r : rasterizers) { if (r->hasGlyph(leftglyph) && r->hasGlyph(rightglyph)) { @@ -195,7 +198,7 @@ float TextShaper::getKerning(uint32 leftglyph, uint32 rightglyph) return k; } -float TextShaper::getKerning(const std::string& leftchar, const std::string& rightchar) +float TextShaper::getKerning(const std::string &leftchar, const std::string &rightchar) { uint32 left = 0; uint32 right = 0; @@ -215,8 +218,6 @@ float TextShaper::getKerning(const std::string& leftchar, const std::string& rig int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex) { - // TODO: use spaces for tab - const auto it = glyphAdvances.find(glyph); if (it != glyphAdvances.end()) { @@ -226,10 +227,14 @@ int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex) } int rasterizeri = 0; + uint32 realglyph = glyph; + + if (glyph == '\t' && isUsingSpacesForTab()) + realglyph = ' '; for (size_t i = 0; i < rasterizers.size(); i++) { - if (rasterizers[i]->hasGlyph(glyph)) + if (rasterizers[i]->hasGlyph(realglyph)) { rasterizeri = (int) i; break; @@ -237,9 +242,12 @@ int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex) } const auto &r = rasterizers[rasterizeri]; - int advance = floorf(r->getGlyphSpacing(glyph) / r->getDPIScale() + 0.5f); + int advance = floorf(r->getGlyphSpacing(realglyph) / r->getDPIScale() + 0.5f); - GlyphIndex glyphi = {r->getGlyphIndex(glyph), rasterizeri}; + if (glyph == '\t' && realglyph == ' ') + advance *= SPACES_PER_TAB; + + GlyphIndex glyphi = {r->getGlyphIndex(realglyph), rasterizeri}; glyphAdvances[glyph] = std::make_pair(advance, glyphi); if (glyphindex) @@ -260,7 +268,7 @@ int TextShaper::getWidth(const std::string &str) return info.width; } -static size_t findNewline(const ColoredCodepoints& codepoints, size_t start) +static size_t findNewline(const ColoredCodepoints &codepoints, size_t start) { for (size_t i = start; i < codepoints.cps.size(); i++) { @@ -273,7 +281,7 @@ static size_t findNewline(const ColoredCodepoints& codepoints, size_t start) return codepoints.cps.size(); } -void TextShaper::getWrap(const ColoredCodepoints& codepoints, float wraplimit, std::vector& lineranges, std::vector* linewidths) +void TextShaper::getWrap(const ColoredCodepoints &codepoints, float wraplimit, std::vector &lineranges, std::vector *linewidths) { size_t nextnewline = findNewline(codepoints, 0); @@ -317,7 +325,7 @@ void TextShaper::getWrap(const ColoredCodepoints& codepoints, float wraplimit, s } } -void TextShaper::getWrap(const std::vector& text, float wraplimit, std::vector& lines, std::vector* linewidths) +void TextShaper::getWrap(const std::vector &text, float wraplimit, std::vector &lines, std::vector *linewidths) { ColoredCodepoints cps; getCodepointsFromString(text, cps); @@ -327,7 +335,7 @@ void TextShaper::getWrap(const std::vector& text, float wraplimit std::string line; - for (const auto& range : codepointranges) + for (const auto &range : codepointranges) { line.clear(); @@ -347,9 +355,9 @@ void TextShaper::getWrap(const std::vector& text, float wraplimit } } -void TextShaper::setFallbacks(const std::vector& fallbacks) +void TextShaper::setFallbacks(const std::vector &fallbacks) { - for (Rasterizer* r : fallbacks) + for (Rasterizer *r : fallbacks) { if (r->getDataType() != rasterizers[0]->getDataType()) throw love::Exception("Font fallbacks must be of the same font type."); @@ -362,7 +370,7 @@ void TextShaper::setFallbacks(const std::vector& fallbacks) rasterizers.resize(1); dpiScales.resize(1); - for (Rasterizer* r : fallbacks) + for (Rasterizer *r : fallbacks) { rasterizers.push_back(r); dpiScales.push_back(r->getDPIScale()); diff --git a/src/modules/font/TextShaper.h b/src/modules/font/TextShaper.h index 72b92653b..72e0425e9 100644 --- a/src/modules/font/TextShaper.h +++ b/src/modules/font/TextShaper.h @@ -81,11 +81,15 @@ public: int height; }; + // This will be used if the Rasterizer doesn't have a tab character itself. + static const int SPACES_PER_TAB = 4; + static love::Type type; virtual ~TextShaper(); const std::vector> &getRasterizers() const { return rasterizers; } + bool isUsingSpacesForTab() const { return useSpacesForTab; } float getHeight() const; @@ -128,6 +132,8 @@ protected: TextShaper(Rasterizer *rasterizer); + static inline bool isWhitespace(uint32 codepoint) { return codepoint == ' ' || codepoint == '\t'; } + std::vector> rasterizers; std::vector dpiScales; @@ -136,6 +142,8 @@ private: int height; float lineHeight; + bool useSpacesForTab; + // maps glyphs to advance and glyph+rasterizer index. std::unordered_map> glyphAdvances; diff --git a/src/modules/font/freetype/HarfbuzzShaper.cpp b/src/modules/font/freetype/HarfbuzzShaper.cpp index 8f0b0037d..e8b73b169 100644 --- a/src/modules/font/freetype/HarfbuzzShaper.cpp +++ b/src/modules/font/freetype/HarfbuzzShaper.cpp @@ -36,30 +36,70 @@ namespace freetype HarfbuzzShaper::HarfbuzzShaper(TrueTypeRasterizer *rasterizer) : TextShaper(rasterizer) + , spaceGlyphIndex() + , tabSpacesAdvanceX(0) + , tabSpacesAdvanceY(0) { hbFonts.push_back(hb_ft_font_create_referenced((FT_Face)rasterizer->getHandle())); + hbBuffers.push_back(hb_buffer_create()); if (hbFonts[0] == nullptr || hbFonts[0] == hb_font_get_empty()) - throw love::Exception(""); + throw love::Exception("Could not create Harfbuzz font object."); + + if (hbBuffers[0] == nullptr || hbBuffers[0] == hb_buffer_get_empty()) + throw love::Exception("Could not create Harfbuzz buffer object."); + + updateSpacesForTabInfo(); } HarfbuzzShaper::~HarfbuzzShaper() { + for (hb_buffer_t *buffer : hbBuffers) + hb_buffer_destroy(buffer); for (hb_font_t *font : hbFonts) hb_font_destroy(font); } -void HarfbuzzShaper::setFallbacks(const std::vector& fallbacks) +void HarfbuzzShaper::setFallbacks(const std::vector &fallbacks) { - for (size_t i = 1; i < hbFonts.size(); i++) + for (size_t i = 1; i < rasterizers.size(); i++) + { + hb_buffer_destroy(hbBuffers[i]); hb_font_destroy(hbFonts[i]); + } TextShaper::setFallbacks(fallbacks); hbFonts.resize(rasterizers.size()); + hbBuffers.resize(rasterizers.size()); - for (size_t i = 1; i < hbFonts.size(); i++) + for (size_t i = 1; i < rasterizers.size(); i++) + { hbFonts[i] = hb_ft_font_create_referenced((FT_Face)rasterizers[i]->getHandle()); + hbBuffers[i] = hb_buffer_create(); + } + + updateSpacesForTabInfo(); +} + +void HarfbuzzShaper::updateSpacesForTabInfo() +{ + if (!isUsingSpacesForTab()) + return; + + hb_codepoint_t glyphid = 0; + for (size_t i = 0; i < hbFonts.size(); i++) + { + hb_font_t *hbfont = hbFonts[i]; + if (hb_font_get_glyph(hbfont, ' ', 0, &glyphid)) + { + spaceGlyphIndex.index = glyphid; + spaceGlyphIndex.rasterizerIndex = i; + tabSpacesAdvanceX = hb_font_get_glyph_h_advance(hbfont, glyphid) * SPACES_PER_TAB; + tabSpacesAdvanceY = hb_font_get_glyph_v_advance(hbfont, glyphid) * SPACES_PER_TAB; + break; + } + } } void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector *positions, std::vector *colors, TextInfo *info) @@ -70,18 +110,21 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, offset.y += getBaseline(); Vector2 curpos = offset; - hb_buffer_t *hbbuffer = hb_buffer_create(); + hb_buffer_t *hbbuffer = hbBuffers[0]; + hb_buffer_reset(hbbuffer); hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int) range.getOffset(), (int) range.getSize()); // TODO: Expose APIs for direction and script? hb_buffer_guess_segment_properties(hbbuffer); + hb_direction_t direction = hb_buffer_get_direction(hbbuffer); + hb_shape(hbFonts[0], hbbuffer, nullptr, 0); int glyphcount = (int) hb_buffer_get_length(hbbuffer); - hb_glyph_info_t* glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr); - hb_glyph_position_t* glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr); + hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr); + hb_glyph_position_t *glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr); if (positions) positions->reserve(positions->size() + glyphcount); @@ -93,7 +136,7 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, for (int i = 0; i < glyphcount; i++) { bool adding = false; - if (glyphinfos[i].codepoint == 0) + if (glyphinfos[i].codepoint == 0 && (codepoints.cps[glyphinfos[i].cluster] != '\t' || !isUsingSpacesForTab())) { if (fallbackranges.empty() || fallbackranges.back().getMax() != glyphinfos[i-1].cluster) fallbackranges.push_back(Range(glyphinfos[i].cluster, 1)); @@ -106,9 +149,7 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, { for (size_t rasti = 1; rasti < rasterizers.size(); rasti++) { - hb_buffer_t* hbbuffer = hb_buffer_create(); - hb_buffer_destroy(hbbuffer); } } } @@ -132,11 +173,10 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, int maxwidth = (int)curpos.x; // TODO: fallbacks - // TODO: use spaces for tab for (int i = 0; i < glyphcount; i++) { - const hb_glyph_info_t& info = glyphinfos[i]; - const hb_glyph_position_t& glyphpos = glyphpositions[i]; + const hb_glyph_info_t &info = glyphinfos[i]; + hb_glyph_position_t &glyphpos = glyphpositions[i]; // TODO: this doesn't handle situations where the user inserted a color // change in the middle of some characters that get combined into a single @@ -166,6 +206,21 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, if (clustercodepoint == '\r') continue; + // This is a glyph index at this point, despite the name. + GlyphIndex gindex = { info.codepoint, 0 }; + + if (clustercodepoint == '\t' && isUsingSpacesForTab()) + { + gindex = spaceGlyphIndex; + + // This should be safe to overwrite. + // TODO: RTL support? + glyphpos.x_offset = 0; + glyphpos.y_offset = 0; + glyphpos.x_advance = HB_DIRECTION_IS_HORIZONTAL(direction) ? tabSpacesAdvanceX : 0; + glyphpos.y_advance = HB_DIRECTION_IS_VERTICAL(direction) ? tabSpacesAdvanceY : 0; + } + if (colorToAdd.hasValue && colors && positions) { IndexedColor c = {colorToAdd.value, positions->size()}; @@ -175,8 +230,6 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, if (positions) { - // Despite the name this is a glyph index at this point. - GlyphIndex gindex = { info.codepoint, 0 }; GlyphPosition p = { curpos, gindex }; // Harfbuzz position coordinate systems are based on the given font. @@ -195,8 +248,6 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, curpos.x = floorf(curpos.x + extraspacing); } - hb_buffer_destroy(hbbuffer); - if (curpos.x > maxwidth) maxwidth = (int)curpos.x; @@ -209,23 +260,26 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, } } -int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width) +int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints &codepoints, Range range, float wraplimit, float *width) { if (!range.isValid()) range = Range(0, codepoints.cps.size()); - hb_buffer_t* hbbuffer = hb_buffer_create(); + hb_buffer_t *hbbuffer = hbBuffers[0]; + hb_buffer_reset(hbbuffer); hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int)range.getOffset(), (int)range.getSize()); // TODO: Expose APIs for direction and script? hb_buffer_guess_segment_properties(hbbuffer); + hb_direction_t direction = hb_buffer_get_direction(hbbuffer); + hb_shape(hbFonts[0], hbbuffer, nullptr, 0); int glyphcount = (int)hb_buffer_get_length(hbbuffer); - hb_glyph_info_t* glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr); - hb_glyph_position_t* glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr); + hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr); + hb_glyph_position_t *glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr); float w = 0.0f; float outwidth = 0.0f; @@ -237,8 +291,8 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ra for (int i = 0; i < glyphcount; i++) { - const hb_glyph_info_t& info = glyphinfos[i]; - const hb_glyph_position_t& glyphpos = glyphpositions[i]; + const hb_glyph_info_t &info = glyphinfos[i]; + hb_glyph_position_t &glyphpos = glyphpositions[i]; uint32 clustercodepoint = codepoints.cps[info.cluster]; @@ -248,10 +302,20 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ra continue; } + if (clustercodepoint == '\t' && isUsingSpacesForTab()) + { + // This should be safe to overwrite. + // TODO: RTL support? + glyphpos.x_offset = 0; + glyphpos.y_offset = 0; + glyphpos.x_advance = HB_DIRECTION_IS_HORIZONTAL(direction) ? tabSpacesAdvanceX : 0; + glyphpos.y_advance = HB_DIRECTION_IS_VERTICAL(direction) ? tabSpacesAdvanceY : 0; + } + float newwidth = w + floorf((glyphpos.x_advance >> 6) / dpiScales[0] + 0.5f); // Only wrap when there's a non-space character. - if (newwidth > wraplimit && clustercodepoint != ' ') + if (newwidth > wraplimit && !isWhitespace(clustercodepoint)) { // Rewind to the last seen space when wrapping. if (lastspaceindex != -1) @@ -263,10 +327,10 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ra } // Don't count trailing spaces in the output width. - if (clustercodepoint == ' ') + if (isWhitespace(clustercodepoint)) { lastspaceindex = info.cluster; - if (prevcodepoint != ' ') + if (!isWhitespace(prevcodepoint)) widthbeforelastspace = w; } else @@ -277,8 +341,6 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ra wrapindex = info.cluster; } - hb_buffer_destroy(hbbuffer); - if (width) *width = outwidth; diff --git a/src/modules/font/freetype/HarfbuzzShaper.h b/src/modules/font/freetype/HarfbuzzShaper.h index f26d362c7..33d52fd9b 100644 --- a/src/modules/font/freetype/HarfbuzzShaper.h +++ b/src/modules/font/freetype/HarfbuzzShaper.h @@ -26,6 +26,7 @@ extern "C" { typedef struct hb_font_t hb_font_t; +typedef struct hb_buffer_t hb_buffer_t; } namespace love @@ -50,7 +51,14 @@ public: private: + void updateSpacesForTabInfo(); + std::vector hbFonts; + std::vector hbBuffers; + + GlyphIndex spaceGlyphIndex; + int tabSpacesAdvanceX; + int tabSpacesAdvanceY; }; // HarfbuzzShaper diff --git a/src/modules/graphics/Font.cpp b/src/modules/graphics/Font.cpp index 9785532aa..be55cc070 100644 --- a/src/modules/graphics/Font.cpp +++ b/src/modules/graphics/Font.cpp @@ -61,7 +61,6 @@ Font::Font(love::font::Rasterizer *r, const SamplerState &s) , textureHeight(128) , samplerState() , dpiScale(r->getDPIScale()) - , useSpacesAsTab(false) , textureCacheID(0) { samplerState.minFilter = s.minFilter; @@ -92,10 +91,6 @@ Font::Font(love::font::Rasterizer *r, const SamplerState &s) if (pixelFormat == PIXELFORMAT_LA8_UNORM && !gfx->isPixelFormatSupported(pixelFormat, PIXELFORMATUSAGEFLAGS_SAMPLE)) pixelFormat = PIXELFORMAT_RGBA8_UNORM; - uint32 tab = '\t'; - if (!r->hasGlyph(tab)) // No tab character in the Rasterizer. - useSpacesAsTab = true; - loadVolatile(); ++fontCount; } @@ -232,24 +227,6 @@ void Font::unloadVolatile() love::font::GlyphData *Font::getRasterizerGlyphData(love::font::TextShaper::GlyphIndex glyphindex, float &dpiscale) { const auto &r = shaper->getRasterizers()[glyphindex.rasterizerIndex]; - - // Use spaces for the tab 'glyph'. FIXME - if (/*glyph == '\t' &&*/ false && useSpacesAsTab) - { - love::font::GlyphData *spacegd = r->getGlyphData(32); - PixelFormat fmt = spacegd->getFormat(); - - love::font::GlyphMetrics gm = {}; - gm.advance = spacegd->getAdvance() * SPACES_PER_TAB; - gm.bearingX = spacegd->getBearingX(); - gm.bearingY = spacegd->getBearingY(); - - spacegd->release(); - - dpiscale = r->getDPIScale(); - return new love::font::GlyphData('\t', gm, fmt); - } - dpiscale = r->getDPIScale(); return r->getGlyphDataForIndex(glyphindex.index); } diff --git a/src/modules/graphics/Font.h b/src/modules/graphics/Font.h index f3cc0afd4..67792da11 100644 --- a/src/modules/graphics/Font.h +++ b/src/modules/graphics/Font.h @@ -201,8 +201,6 @@ private: int textureX, textureY; int rowHeight; - bool useSpacesAsTab; - // ID which is incremented when the texture cache is invalidated. uint32 textureCacheID; @@ -211,9 +209,6 @@ private: // use, for edge antialiasing. static const int TEXTURE_PADDING = 2; - // This will be used if the Rasterizer doesn't have a tab character itself. - static const int SPACES_PER_TAB = 4; - static StringMap::Entry alignModeEntries[]; static StringMap alignModes;