From 84f019680d101fab44f5ad5a7dc62ed53b181325 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Tue, 25 Oct 2022 00:39:40 +0200 Subject: [PATCH] Greatly improved text sizing and rendering. --- es-core/src/GuiComponent.cpp | 2 +- es-core/src/GuiComponent.h | 2 + es-core/src/HelpStyle.cpp | 3 +- es-core/src/components/DateTimeComponent.cpp | 2 +- .../src/components/ScrollableContainer.cpp | 35 ++++--- es-core/src/components/TextComponent.cpp | 33 +++++-- es-core/src/components/TextComponent.h | 6 ++ .../components/primary/CarouselComponent.h | 34 +++---- .../components/primary/TextListComponent.h | 26 +++-- es-core/src/resources/Font.cpp | 99 ++++++++++++------- es-core/src/resources/Font.h | 45 +++++---- 11 files changed, 181 insertions(+), 106 deletions(-) diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index d66849627..93085c563 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -206,7 +206,7 @@ void GuiComponent::setDimming(float dimming) const glm::mat4& GuiComponent::getTransform() { mTransform = Renderer::getIdentity(); - mTransform = glm::translate(mTransform, glm::round(mPosition)); + mTransform = glm::translate(mTransform, mPosition); if (mScale != 1.0f) mTransform = glm::scale(mTransform, glm::vec3 {mScale}); diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 03dc787f9..d9f354317 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -160,6 +160,8 @@ public: mComponentThemeFlags ^= ComponentThemeFlags::METADATA_ELEMENT; } + virtual int getTextCacheGlyphHeight() { return 0; } + // Returns the center point of the image (takes origin into account). const glm::vec2 getCenter() const; diff --git a/es-core/src/HelpStyle.cpp b/es-core/src/HelpStyle.cpp index 885e59f7c..0f505e52f 100644 --- a/es-core/src/HelpStyle.cpp +++ b/es-core/src/HelpStyle.cpp @@ -24,7 +24,6 @@ HelpStyle::HelpStyle() , opacity {1.0f} , letterCase {"uppercase"} { - if (FONT_SIZE_SMALL != 0) font = Font::get(FONT_SIZE_SMALL); else @@ -61,7 +60,7 @@ void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::s iconColorDimmed = iconColor; if (elem->has("fontPath") || elem->has("fontSize")) - font = Font::getFromTheme(elem, ThemeFlags::ALL, font); + font = Font::getFromTheme(elem, ThemeFlags::ALL, font, 0.0f, theme->isLegacyTheme()); if (elem->has("entrySpacing")) entrySpacing = glm::clamp(elem->get("entrySpacing"), 0.0f, 0.04f); diff --git a/es-core/src/components/DateTimeComponent.cpp b/es-core/src/components/DateTimeComponent.cpp index d9bfb6de6..ed4dfb27e 100644 --- a/es-core/src/components/DateTimeComponent.cpp +++ b/es-core/src/components/DateTimeComponent.cpp @@ -224,5 +224,5 @@ void DateTimeComponent::applyTheme(const std::shared_ptr& theme, if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(glm::clamp(elem->get("lineSpacing"), 0.5f, 3.0f)); - setFont(Font::getFromTheme(elem, properties, mFont, maxHeight)); + setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, theme->isLegacyTheme())); } diff --git a/es-core/src/components/ScrollableContainer.cpp b/es-core/src/components/ScrollableContainer.cpp index 1199fb11c..74cbb7250 100644 --- a/es-core/src/components/ScrollableContainer.cpp +++ b/es-core/src/components/ScrollableContainer.cpp @@ -66,10 +66,15 @@ void ScrollableContainer::reset() mAutoScrollResetAccumulator = 0; mAutoScrollAccumulator = -mAutoScrollDelay + mAutoScrollSpeed; mAtEnd = false; - // This is needed to resize to the designated area when the backgrund image gets invalidated. + // This is needed to resize to the designated area when the background image gets invalidated. if (!mChildren.empty()) { - float combinedHeight { - mChildren.front()->getFont()->getHeight(mChildren.front()->getLineSpacing())}; + float combinedHeight {0.0f}; + const float cacheGlyphHeight { + static_cast(mChildren.front()->getTextCacheGlyphHeight())}; + if (cacheGlyphHeight > 0.0f) + combinedHeight = cacheGlyphHeight * mChildren.front()->getLineSpacing(); + else + return; if (mChildren.front()->getSize().y > mSize.y) { if (mVerticalSnap) { float numLines {std::floor(mSize.y / combinedHeight)}; @@ -121,8 +126,13 @@ void ScrollableContainer::update(int deltaTime) const glm::vec2 contentSize {glm::round(mChildren.front()->getSize())}; float rowModifier {1.0f}; - float lineSpacing {mChildren.front()->getLineSpacing()}; - float combinedHeight {mChildren.front()->getFont()->getHeight(lineSpacing)}; + const float lineSpacing {mChildren.front()->getLineSpacing()}; + float combinedHeight {0.0f}; + const float cacheGlyphHeight {static_cast(mChildren.front()->getTextCacheGlyphHeight())}; + if (cacheGlyphHeight > 0.0f) + combinedHeight = cacheGlyphHeight * lineSpacing; + else + return; // Calculate the spacing which will be used to clip the container. if (lineSpacing > 1.2f && mClipSpacing == 0.0f) { @@ -175,7 +185,7 @@ void ScrollableContainer::update(int deltaTime) mAutoScrollAccumulator += deltaTime; while (mAutoScrollAccumulator >= static_cast(rowModifier * static_cast(mAdjustedAutoScrollSpeed))) { - if (contentSize.y > mAdjustedHeight) + if (!mAtEnd && contentSize.y > mAdjustedHeight) mScrollPos += mScrollDir; mAutoScrollAccumulator -= static_cast(rowModifier * static_cast(mAdjustedAutoScrollSpeed)); @@ -193,13 +203,10 @@ void ScrollableContainer::update(int deltaTime) mAtEnd = true; } - if (contentSize.y < mAdjustedHeight) { + if (contentSize.y < mAdjustedHeight) mScrollPos.y = 0.0f; - } - else if (mScrollPos.y + mAdjustedHeight > contentSize.y) { - mScrollPos.y = contentSize.y - mAdjustedHeight; + else if (mScrollPos.y + mAdjustedHeight > contentSize.y) mAtEnd = true; - } if (mAtEnd) { mAutoScrollResetAccumulator += deltaTime; @@ -237,8 +244,8 @@ void ScrollableContainer::render(const glm::mat4& parentTrans) dimScaled.x = std::fabs(trans[3].x + mSize.x); dimScaled.y = std::fabs(trans[3].y + mAdjustedHeight); - glm::ivec2 clipDim {static_cast(ceilf(dimScaled.x - trans[3].x)), - static_cast(ceilf(dimScaled.y - trans[3].y))}; + glm::ivec2 clipDim {static_cast(dimScaled.x - trans[3].x), + static_cast(dimScaled.y - trans[3].y)}; // By effectively clipping the upper and lower boundaries of the container we mostly avoid // scrolling outside the vertical starting and ending positions. @@ -247,7 +254,7 @@ void ScrollableContainer::render(const glm::mat4& parentTrans) mRenderer->pushClipRect(clipPos, clipDim); - trans = glm::translate(trans, glm::round(-glm::vec3 {mScrollPos.x, mScrollPos.y, 0.0f})); + trans = glm::translate(trans, -glm::vec3 {mScrollPos.x, mScrollPos.y, 0.0f}); mRenderer->setMatrix(trans); if (Settings::getInstance()->getBool("DebugText")) diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index ea32207ff..687e758dd 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -30,6 +30,7 @@ TextComponent::TextComponent() , mNoTopMargin {false} , mSelectable {false} , mVerticalAutoSizing {false} + , mLegacyTheme {false} { } @@ -58,6 +59,7 @@ TextComponent::TextComponent(const std::string& text, , mNoTopMargin {false} , mSelectable {false} , mVerticalAutoSizing {false} + , mLegacyTheme {false} { setFont(font); setColor(color); @@ -180,11 +182,11 @@ void TextComponent::render(const glm::mat4& parentTrans) break; } case ALIGN_BOTTOM: { - yOff = (getSize().y - textSize.y); + yOff = mSize.y - textSize.y; break; } case ALIGN_CENTER: { - yOff = std::round((getSize().y - textSize.y) / 2.0f); + yOff = (mSize.y - textSize.y) / 2.0f; break; } default: { @@ -194,7 +196,7 @@ void TextComponent::render(const glm::mat4& parentTrans) } else { // If height is smaller than the font height, then always center vertically. - yOff = std::round((getSize().y - textSize.y) / 2.0f); + yOff = (mSize.y - textSize.y) / 2.0f; } // Draw the overall textbox area. If we're inside a scrollable container then this @@ -264,10 +266,23 @@ void TextComponent::onTextChanged() if (!mFont || text.empty() || mSize.x < 0.0f) return; - std::shared_ptr font {mFont}; - const float lineHeight {font->getHeight(mLineSpacing)}; - const bool isMultiline {mAutoCalcExtent.y == 1 || mSize.y > lineHeight}; + float lineHeight {0.0f}; const bool isScrollable {mParent && mParent->isScrollable()}; + std::shared_ptr font {mFont}; + + if (mLegacyTheme && !isScrollable && (mVerticalAutoSizing || mAutoCalcExtent.x)) { + // This is needed to retain a bug from the legacy theme engine where lineSpacing + // is not sized correctly when using automatic text element sizing. This is only + // applied to legacy themes for backward compatibility reasons. + font->useLegacyMaxGlyphHeight(); + lineHeight = font->getHeight(mLineSpacing); + } + else { + // Used to initialize all glyphs, which is needed to populate mMaxGlyphHeight. + lineHeight = mFont->loadGlyphs(text + "\n") * mLineSpacing; + } + + const bool isMultiline {mAutoCalcExtent.y == 1 || mSize.y > lineHeight}; if (isMultiline && !isScrollable) { const std::string wrappedText { @@ -284,7 +299,7 @@ void TextComponent::onTextChanged() } if (mAutoCalcExtent.y) - mSize.y = font->getTextSize().y; + mSize.y = mTextCache->metrics.size.y; if (mOpacity != 1.0f || mThemeOpacity != 1.0f) setOpacity(mOpacity); @@ -333,6 +348,8 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, using namespace ThemeFlags; GuiComponent::applyTheme(theme, view, element, properties); + mLegacyTheme = theme->isLegacyTheme(); + std::string elementType {"text"}; std::string componentName {"TextComponent"}; @@ -472,5 +489,5 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(glm::clamp(elem->get("lineSpacing"), 0.5f, 3.0f)); - setFont(Font::getFromTheme(elem, properties, mFont, maxHeight)); + setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, theme->isLegacyTheme())); } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index a8a04be9b..d117f6a2a 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -78,6 +78,11 @@ public: Alignment getHorizontalAlignment() { return mHorizontalAlignment; } Alignment getVerticalAlignment() { return mVerticalAlignment; } + int getTextCacheGlyphHeight() override + { + return (mTextCache == nullptr ? 0 : mTextCache->metrics.maxGlyphHeight); + } + protected: virtual void onTextChanged(); @@ -114,6 +119,7 @@ private: bool mNoTopMargin; bool mSelectable; bool mVerticalAutoSizing; + bool mLegacyTheme; }; #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 505c0e18d..6f3351918 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -183,8 +183,8 @@ CarouselComponent::CarouselComponent() , mMaxItemCount {3.0f} , mItemsBeforeCenter {8} , mItemsAfterCenter {8} - , mItemSize {glm::round( - glm::vec2 {Renderer::getScreenWidth() * 0.25f, Renderer::getScreenHeight() * 0.155f})} + , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.25f, + Renderer::getScreenHeight() * 0.155f}} , mLinearInterpolation {false} , mInstantItemTransitions {false} , mItemAxisHorizontal {false} @@ -568,11 +568,10 @@ template void CarouselComponent::render(const glm::mat4& parentT glm::mat4 carouselTrans {parentTrans}; carouselTrans = glm::translate( - carouselTrans, - glm::round(glm::vec3 {GuiComponent::mPosition.x, GuiComponent::mPosition.y, 0.0f})); - carouselTrans = glm::translate( - carouselTrans, glm::round(glm::vec3 {GuiComponent::mOrigin.x * mSize.x * -1.0f, - GuiComponent::mOrigin.y * mSize.y * -1.0f, 0.0f})); + carouselTrans, glm::vec3 {GuiComponent::mPosition.x, GuiComponent::mPosition.y, 0.0f}); + carouselTrans = + glm::translate(carouselTrans, glm::vec3 {GuiComponent::mOrigin.x * mSize.x * -1.0f, + GuiComponent::mOrigin.y * mSize.y * -1.0f, 0.0f}); if (carouselTrans[3].x + mSize.x <= 0.0f || carouselTrans[3].y + mSize.y <= 0.0f) return; @@ -650,8 +649,7 @@ template void CarouselComponent::render(const glm::mat4& parentT } } else if (mType == CarouselType::VERTICAL) { - itemSpacing.y = - std::round(((mSize.y - (mItemSize.y * mMaxItemCount)) / mMaxItemCount) + mItemSize.y); + itemSpacing.y = ((mSize.y - (mItemSize.y * mMaxItemCount)) / mMaxItemCount) + mItemSize.y; yOff = (mSize.y - mItemSize.y) / 2.0f - (camOffset * itemSpacing.y); if (mItemHorizontalAlignment == ALIGN_LEFT) { if (mLegacyMode) @@ -670,8 +668,7 @@ template void CarouselComponent::render(const glm::mat4& parentT } } else { // HORIZONTAL. - itemSpacing.x = - std::round(((mSize.x - (mItemSize.x * mMaxItemCount)) / mMaxItemCount) + mItemSize.x); + itemSpacing.x = ((mSize.x - (mItemSize.x * mMaxItemCount)) / mMaxItemCount) + mItemSize.x; xOff = (mSize.x - mItemSize.x) / 2.0f - (camOffset * itemSpacing.x); if (mItemVerticalAlignment == ALIGN_TOP) { if (mLegacyMode) @@ -780,9 +777,8 @@ template void CarouselComponent::render(const glm::mat4& parentT if (singleEntry) itemTrans = glm::translate(carouselTrans, glm::vec3 {xOff, yOff, 0.0f}); else - itemTrans = - glm::translate(itemTrans, glm::vec3 {std::round(i * itemSpacing.x + xOff), - std::round(i * itemSpacing.y + yOff), 0.0f}); + itemTrans = glm::translate(itemTrans, glm::vec3 {(i * itemSpacing.x) + xOff, + (i * itemSpacing.y) + yOff, 0.0f}); float opacity {0.0f}; @@ -1011,8 +1007,8 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("itemSize")) { const glm::vec2 itemSize {glm::clamp(elem->get("itemSize"), 0.05f, 1.0f)}; - mItemSize = glm::round( - itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())); + mItemSize = + itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); } if (elem->has("itemScale")) @@ -1164,8 +1160,8 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, itemSize.x = glm::clamp(itemSize.x, 0.005f, 1.0f); itemSize.y = glm::clamp(itemSize.y, 0.005f, 1.0f); } - mItemSize = glm::round( - itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())); + mItemSize = + itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); } if (elem->has("maxLogoCount")) { @@ -1210,7 +1206,7 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, } } - mFont = Font::getFromTheme(elem, properties, mFont); + mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, mLegacyMode); if (elem->has("textColor")) mTextColor = elem->get("textColor"); diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index ad6e5b046..6cfebe848 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -350,10 +350,19 @@ template void TextListComponent::render(const glm::mat4& parentT int screenCount {0}; float y {0.0f}; - const float entrySize { - std::max(floorf(font->getHeight(1.0f)), floorf(static_cast(font->getSize()))) * - mLineSpacing}; - const float lineSpacingHeight {floorf(font->getHeight(mLineSpacing) - font->getHeight(1.0f))}; + float entrySize {0.0f}; + float lineSpacingHeight {0.0f}; + + // The vertical spacing between rows for legacy themes is very inaccurate and will look + // different depending on the resolution, but it's done for maximum backward compatibility. + if (mLegacyMode) { + entrySize = std::floor(font->getSize()) * mLineSpacing; + lineSpacingHeight = std::floor(font->getSize()) * mLineSpacing - font->getSize() * 1.0f; + } + else { + entrySize = font->getSize() * mLineSpacing; + lineSpacingHeight = font->getSize() * mLineSpacing - font->getSize() * 1.0f; + } if (mLegacyMode) { // This extra vertical margin is technically incorrect, but it adds a little extra leeway @@ -366,7 +375,9 @@ template void TextListComponent::render(const glm::mat4& parentT floorf((mSize.y + lineSpacingHeight / 2.0f + extraMargin) / entrySize)); } else { - screenCount = static_cast(floorf((mSize.y + lineSpacingHeight / 2.0f) / entrySize)); + // Number of entries that can fit on the screen simultaneously. + screenCount = + static_cast(std::floor((mSize.y + lineSpacingHeight / 2.0f) / entrySize)); } if (size() >= screenCount) { @@ -555,9 +566,8 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, setColor(1, elem->get("secondaryColor")); } - setFont(Font::getFromTheme(elem, properties, mFont)); - const float selectorHeight { - std::max(mFont->getHeight(1.0), static_cast(mFont->getSize())) * mLineSpacing}; + setFont(Font::getFromTheme(elem, properties, mFont, 0.0f, mLegacyMode)); + const float selectorHeight {mFont->getSize() * mLineSpacing}; setSelectorHeight(selectorHeight); if (properties & ALIGNMENT) { diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 8ea08cb42..b44d75a96 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -15,19 +15,20 @@ #include "utils/PlatformUtil.h" #include "utils/StringUtil.h" -Font::Font(int size, const std::string& path) +Font::Font(float size, const std::string& path) : mRenderer {Renderer::getInstance()} , mPath(path) - , mTextSize {0.0f, 0.0f} - , mFontSize(size) - , mMaxGlyphHeight {0} + , mFontSize {size} + , mLetterHeight {0.0f} + , mMaxGlyphHeight {static_cast(std::round(size))} + , mLegacyMaxGlyphHeight {0} { - if (mFontSize < 3) { - mFontSize = 3; + if (mFontSize < 3.0f) { + mFontSize = 3.0f; LOG(LogWarning) << "Requested font size too small, changing to minimum supported size"; } else if (mFontSize > Renderer::getScreenHeight() * 1.5f) { - mFontSize = static_cast(Renderer::getScreenHeight() * 1.5f); + mFontSize = Renderer::getScreenHeight() * 1.5f; LOG(LogWarning) << "Requested font size too large, changing to maximum supported size"; } @@ -35,7 +36,7 @@ Font::Font(int size, const std::string& path) initLibrary(); // Always initialize ASCII characters. - for (unsigned int i = 32; i < 128; ++i) + for (unsigned int i = 32; i < 127; ++i) getGlyph(i); clearFaceCache(); @@ -45,7 +46,7 @@ Font::~Font() { unload(ResourceManager::getInstance()); - auto fontEntry = sFontMap.find(std::pair(mPath, mFontSize)); + auto fontEntry = sFontMap.find(std::pair(mPath, mFontSize)); if (fontEntry != sFontMap.cend()) sFontMap.erase(fontEntry); @@ -56,11 +57,11 @@ Font::~Font() } } -std::shared_ptr Font::get(int size, const std::string& path) +std::shared_ptr Font::get(float size, const std::string& path) { const std::string canonicalPath {Utils::FileSystem::getCanonicalPath(path)}; - std::pair def {canonicalPath.empty() ? getDefaultPath() : canonicalPath, - size}; + const std::pair def { + canonicalPath.empty() ? getDefaultPath() : canonicalPath, size}; auto foundFont = sFontMap.find(def); if (foundFont != sFontMap.cend()) { @@ -76,11 +77,9 @@ std::shared_ptr Font::get(int size, const std::string& path) glm::vec2 Font::sizeText(std::string text, float lineSpacing) { + const float lineHeight {getHeight(lineSpacing)}; float lineWidth {0.0f}; float highestWidth {0.0f}; - - const float lineHeight {getHeight(lineSpacing)}; - float y {lineHeight}; size_t i {0}; @@ -106,6 +105,20 @@ glm::vec2 Font::sizeText(std::string text, float lineSpacing) return glm::vec2 {highestWidth, y}; } +int Font::loadGlyphs(const std::string& text) +{ + mMaxGlyphHeight = static_cast(std::round(mFontSize)); + + for (size_t i = 0; i < text.length();) { + unsigned int character {Utils::String::chars2Unicode(text, i)}; // Advances i. + Glyph* glyph {getGlyph(character)}; + + if (glyph->rows > mMaxGlyphHeight) + mMaxGlyphHeight = glyph->rows; + } + return mMaxGlyphHeight; +} + TextCache* Font::buildTextCache(const std::string& text, float offsetX, float offsetY, @@ -134,6 +147,9 @@ TextCache* Font::buildTextCache(const std::string& text, yBot = getHeight(1.5); } else { + // TODO: This is lacking some precision which is especially visible at higher resolutions + // like 4K where the text is not always placed entirely correctly vertically. Try to find + // a way to improve on this. yTop = getGlyph('S')->bearing.y; yBot = getHeight(lineSpacing); } @@ -203,7 +219,8 @@ TextCache* Font::buildTextCache(const std::string& text, TextCache* cache {new TextCache()}; cache->vertexLists.resize(vertMap.size()); - cache->metrics = {sizeText(text, lineSpacing)}; + cache->metrics.size = {sizeText(text, lineSpacing)}; + cache->metrics.maxGlyphHeight = mMaxGlyphHeight; size_t i {0}; for (auto it = vertMap.cbegin(); it != vertMap.cend(); ++it) { @@ -366,7 +383,6 @@ std::string Font::wrapText(const std::string& text, wrappedText.append("..."); } - mTextSize = {maxLength, accumHeight}; return wrappedText; } @@ -396,31 +412,36 @@ glm::vec2 Font::getWrappedTextCursorOffset(const std::string& wrappedText, float Font::getLetterHeight() { - Glyph* glyph {getGlyph('S')}; - assert(glyph); - return glyph->texSize.y * glyph->texture->textureSize.y; + if (mLetterHeight == 0.0f) + return mFontSize * 0.737; // Only needed if face does not contain the letter 'S'. + else + return mLetterHeight; } std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig, - const float maxHeight) + const float maxHeight, + const bool legacyTheme) { + mLegacyTheme = legacyTheme; + using namespace ThemeFlags; if (!(properties & FONT_PATH) && !(properties & FONT_SIZE)) return orig; - int size {static_cast(orig ? orig->mFontSize : FONT_SIZE_MEDIUM)}; + float size {static_cast(orig ? orig->mFontSize : FONT_SIZE_MEDIUM)}; std::string path {orig ? orig->mPath : getDefaultPath()}; float screenHeight {static_cast(Renderer::getScreenHeight())}; - if (properties & FONT_SIZE && elem->has("fontSize")) - size = static_cast(glm::clamp(screenHeight * elem->get("fontSize"), - screenHeight * 0.001f, screenHeight * 1.5f)); + if (properties & FONT_SIZE && elem->has("fontSize")) { + size = glm::clamp(screenHeight * elem->get("fontSize"), screenHeight * 0.001f, + screenHeight * 1.5f); + } - if (maxHeight != 0.0f && static_cast(size) > maxHeight) - size = static_cast(maxHeight); + if (maxHeight != 0.0f && size > maxHeight) + size = maxHeight; if (properties & FONT_PATH && elem->has("fontPath")) path = elem->get("fontPath"); @@ -433,7 +454,10 @@ std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, path = getDefaultPath(); } - return get(size, path); + if (mLegacyTheme) + return get(std::floor(size), path); + else + return get(size, path); } size_t Font::getMemUsage() const @@ -558,7 +582,7 @@ void Font::FontTexture::deinitTexture() } } -Font::FontFace::FontFace(ResourceData&& d, int size, const std::string& path) +Font::FontFace::FontFace(ResourceData&& d, float size, const std::string& path) : data {d} { if (FT_New_Memory_Face(sLibrary, d.ptr.get(), static_cast(d.length), 0, &face) != 0) { @@ -566,7 +590,10 @@ Font::FontFace::FontFace(ResourceData&& d, int size, const std::string& path) Utils::Platform::emergencyShutdown(); } - FT_Set_Pixel_Sizes(face, 0, size); + // Even though a fractional font size can be requested, the glyphs will always be rounded + // to integers. It's not useless to call FT_Set_Char_Size() instead of FT_Set_Pixel_Sizes() + // though as the glyphs will still be much more evenely sized across different resolutions. + FT_Set_Char_Size(face, 0.0f, size * 64.0f, 0, 0); } Font::FontFace::~FontFace() @@ -712,6 +739,12 @@ Font::Glyph* Font::getGlyph(const unsigned int id) return nullptr; } + // Use the letter 'S' as a size reference. + if (mLetterHeight == 0 && id == 'S') { + const float ratio {static_cast(glyphSize.y) / std::round(mFontSize)}; + mLetterHeight = mFontSize * ratio; + } + // Create glyph. Glyph& glyph {mGlyphMap[id]}; @@ -720,18 +753,18 @@ Font::Glyph* Font::getGlyph(const unsigned int id) cursor.y / static_cast(tex->textureSize.y)}; glyph.texSize = glm::vec2 {glyphSize.x / static_cast(tex->textureSize.x), glyphSize.y / static_cast(tex->textureSize.y)}; - glyph.advance = glm::vec2 {static_cast(glyphSlot->metrics.horiAdvance) / 64.0f, static_cast(glyphSlot->metrics.vertAdvance) / 64.0f}; glyph.bearing = glm::vec2 {static_cast(glyphSlot->metrics.horiBearingX) / 64.0f, static_cast(glyphSlot->metrics.horiBearingY) / 64.0f}; + glyph.rows = glyphSlot->bitmap.rows; // Upload glyph bitmap to texture. mRenderer->updateTexture(tex->textureId, Renderer::TextureType::RED, cursor.x, cursor.y, glyphSize.x, glyphSize.y, glyphSlot->bitmap.buffer); - if (glyphSize.y > mMaxGlyphHeight) - mMaxGlyphHeight = glyphSize.y; + if (glyphSize.y > mLegacyMaxGlyphHeight) + mLegacyMaxGlyphHeight = glyphSize.y; return &glyph; } diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index df2b5d506..b3fc40cde 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -21,16 +21,10 @@ class TextCache; -// clang-format off -#define FONT_SIZE_MINI (static_cast(0.030f * \ - std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()))) -#define FONT_SIZE_SMALL (static_cast(0.035f * \ - std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()))) -#define FONT_SIZE_MEDIUM (static_cast(0.045f * \ - std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()))) -#define FONT_SIZE_LARGE (static_cast(0.085f * \ - std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()))) -// clang-format on +#define FONT_SIZE_MINI 0.030f * std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()) +#define FONT_SIZE_SMALL 0.035f * std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()) +#define FONT_SIZE_MEDIUM 0.045f * std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()) +#define FONT_SIZE_LARGE 0.085f * std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()) #define FONT_PATH_LIGHT ":/fonts/Akrobat-Regular.ttf" #define FONT_PATH_REGULAR ":/fonts/Akrobat-SemiBold.ttf" @@ -42,13 +36,19 @@ class Font : public IReloadable { public: virtual ~Font(); - static std::shared_ptr get(int size, const std::string& path = getDefaultPath()); + static std::shared_ptr get(float size, const std::string& path = getDefaultPath()); // Returns the expected size of a string when rendered. Extra spacing is applied to the Y axis. glm::vec2 sizeText(std::string text, float lineSpacing = 1.5f); - // Returns the size of the overall text area. - const glm::vec2 getTextSize() { return mTextSize; } + // Used to determine mMaxGlyphHeight upfront which is needed for accurate text sizing by + // wrapText and buildTextCache. This is required as the requested font height is not + // guaranteed and can be exceeded by a few pixels for some glyphs. + int loadGlyphs(const std::string& text); + + // This is needed to retain a bug from the legacy theme engine where lineSpacing is not + // sized correctly when using automatic text element sizing. + void useLegacyMaxGlyphHeight() { mMaxGlyphHeight = mLegacyMaxGlyphHeight; } TextCache* buildTextCache(const std::string& text, float offsetX, @@ -86,14 +86,15 @@ public: void reload(ResourceManager& rm) override { rebuildTextures(); } void unload(ResourceManager& rm) override { unloadTextures(); } - int getSize() const { return mFontSize; } + const float getSize() const { return mFontSize; } const std::string& getPath() const { return mPath; } static std::string getDefaultPath() { return FONT_PATH_REGULAR; } static std::shared_ptr getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig, - const float maxHeight = 0.0f); + const float maxHeight = 0.0f, + const bool legacyTheme = false); // Returns an approximation of VRAM used by this font's texture (in bytes). size_t getMemUsage() const; @@ -101,7 +102,7 @@ public: static size_t getTotalMemUsage(); private: - Font(int size, const std::string& path); + Font(float size, const std::string& path); static void initLibrary(); struct FontTexture { @@ -127,7 +128,7 @@ private: const ResourceData data; FT_Face face; - FontFace(ResourceData&& d, int size, const std::string& path); + FontFace(ResourceData&& d, float size, const std::string& path); virtual ~FontFace(); }; @@ -137,6 +138,7 @@ private: glm::vec2 texSize; // In texels. glm::vec2 advance; glm::vec2 bearing; + int rows; }; // Completely recreate the texture data for all textures based on mGlyphs information. @@ -159,7 +161,8 @@ private: void clearFaceCache() { mFaceCache.clear(); } static inline FT_Library sLibrary {nullptr}; - static inline std::map, std::weak_ptr> sFontMap; + static inline std::map, std::weak_ptr> sFontMap; + static inline bool mLegacyTheme {false}; Renderer* mRenderer; std::vector> mTextures; @@ -167,9 +170,10 @@ private: std::map mGlyphMap; const std::string mPath; - glm::vec2 mTextSize; - int mFontSize; + float mFontSize; + float mLetterHeight; int mMaxGlyphHeight; + int mLegacyMaxGlyphHeight; }; // Used to store a sort of "pre-rendered" string. @@ -183,6 +187,7 @@ class TextCache public: struct CacheMetrics { glm::vec2 size; + int maxGlyphHeight; } metrics; void setColor(unsigned int color);