Greatly improved text sizing and rendering.

This commit is contained in:
Leon Styhre 2022-10-25 00:39:40 +02:00
parent c5098a62d5
commit 84f019680d
11 changed files with 181 additions and 106 deletions

View file

@ -206,7 +206,7 @@ void GuiComponent::setDimming(float dimming)
const glm::mat4& GuiComponent::getTransform() const glm::mat4& GuiComponent::getTransform()
{ {
mTransform = Renderer::getIdentity(); mTransform = Renderer::getIdentity();
mTransform = glm::translate(mTransform, glm::round(mPosition)); mTransform = glm::translate(mTransform, mPosition);
if (mScale != 1.0f) if (mScale != 1.0f)
mTransform = glm::scale(mTransform, glm::vec3 {mScale}); mTransform = glm::scale(mTransform, glm::vec3 {mScale});

View file

@ -160,6 +160,8 @@ public:
mComponentThemeFlags ^= ComponentThemeFlags::METADATA_ELEMENT; mComponentThemeFlags ^= ComponentThemeFlags::METADATA_ELEMENT;
} }
virtual int getTextCacheGlyphHeight() { return 0; }
// Returns the center point of the image (takes origin into account). // Returns the center point of the image (takes origin into account).
const glm::vec2 getCenter() const; const glm::vec2 getCenter() const;

View file

@ -24,7 +24,6 @@ HelpStyle::HelpStyle()
, opacity {1.0f} , opacity {1.0f}
, letterCase {"uppercase"} , letterCase {"uppercase"}
{ {
if (FONT_SIZE_SMALL != 0) if (FONT_SIZE_SMALL != 0)
font = Font::get(FONT_SIZE_SMALL); font = Font::get(FONT_SIZE_SMALL);
else else
@ -61,7 +60,7 @@ void HelpStyle::applyTheme(const std::shared_ptr<ThemeData>& theme, const std::s
iconColorDimmed = iconColor; iconColorDimmed = iconColor;
if (elem->has("fontPath") || elem->has("fontSize")) 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")) if (elem->has("entrySpacing"))
entrySpacing = glm::clamp(elem->get<float>("entrySpacing"), 0.0f, 0.04f); entrySpacing = glm::clamp(elem->get<float>("entrySpacing"), 0.0f, 0.04f);

View file

@ -224,5 +224,5 @@ void DateTimeComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (properties & LINE_SPACING && elem->has("lineSpacing")) if (properties & LINE_SPACING && elem->has("lineSpacing"))
setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f)); setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f));
setFont(Font::getFromTheme(elem, properties, mFont, maxHeight)); setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, theme->isLegacyTheme()));
} }

View file

@ -66,10 +66,15 @@ void ScrollableContainer::reset()
mAutoScrollResetAccumulator = 0; mAutoScrollResetAccumulator = 0;
mAutoScrollAccumulator = -mAutoScrollDelay + mAutoScrollSpeed; mAutoScrollAccumulator = -mAutoScrollDelay + mAutoScrollSpeed;
mAtEnd = false; 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()) { if (!mChildren.empty()) {
float combinedHeight { float combinedHeight {0.0f};
mChildren.front()->getFont()->getHeight(mChildren.front()->getLineSpacing())}; const float cacheGlyphHeight {
static_cast<float>(mChildren.front()->getTextCacheGlyphHeight())};
if (cacheGlyphHeight > 0.0f)
combinedHeight = cacheGlyphHeight * mChildren.front()->getLineSpacing();
else
return;
if (mChildren.front()->getSize().y > mSize.y) { if (mChildren.front()->getSize().y > mSize.y) {
if (mVerticalSnap) { if (mVerticalSnap) {
float numLines {std::floor(mSize.y / combinedHeight)}; 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())}; const glm::vec2 contentSize {glm::round(mChildren.front()->getSize())};
float rowModifier {1.0f}; float rowModifier {1.0f};
float lineSpacing {mChildren.front()->getLineSpacing()}; const float lineSpacing {mChildren.front()->getLineSpacing()};
float combinedHeight {mChildren.front()->getFont()->getHeight(lineSpacing)}; float combinedHeight {0.0f};
const float cacheGlyphHeight {static_cast<float>(mChildren.front()->getTextCacheGlyphHeight())};
if (cacheGlyphHeight > 0.0f)
combinedHeight = cacheGlyphHeight * lineSpacing;
else
return;
// Calculate the spacing which will be used to clip the container. // Calculate the spacing which will be used to clip the container.
if (lineSpacing > 1.2f && mClipSpacing == 0.0f) { if (lineSpacing > 1.2f && mClipSpacing == 0.0f) {
@ -175,7 +185,7 @@ void ScrollableContainer::update(int deltaTime)
mAutoScrollAccumulator += deltaTime; mAutoScrollAccumulator += deltaTime;
while (mAutoScrollAccumulator >= while (mAutoScrollAccumulator >=
static_cast<int>(rowModifier * static_cast<float>(mAdjustedAutoScrollSpeed))) { static_cast<int>(rowModifier * static_cast<float>(mAdjustedAutoScrollSpeed))) {
if (contentSize.y > mAdjustedHeight) if (!mAtEnd && contentSize.y > mAdjustedHeight)
mScrollPos += mScrollDir; mScrollPos += mScrollDir;
mAutoScrollAccumulator -= mAutoScrollAccumulator -=
static_cast<int>(rowModifier * static_cast<float>(mAdjustedAutoScrollSpeed)); static_cast<int>(rowModifier * static_cast<float>(mAdjustedAutoScrollSpeed));
@ -193,13 +203,10 @@ void ScrollableContainer::update(int deltaTime)
mAtEnd = true; mAtEnd = true;
} }
if (contentSize.y < mAdjustedHeight) { if (contentSize.y < mAdjustedHeight)
mScrollPos.y = 0.0f; mScrollPos.y = 0.0f;
} else if (mScrollPos.y + mAdjustedHeight > contentSize.y)
else if (mScrollPos.y + mAdjustedHeight > contentSize.y) {
mScrollPos.y = contentSize.y - mAdjustedHeight;
mAtEnd = true; mAtEnd = true;
}
if (mAtEnd) { if (mAtEnd) {
mAutoScrollResetAccumulator += deltaTime; mAutoScrollResetAccumulator += deltaTime;
@ -237,8 +244,8 @@ void ScrollableContainer::render(const glm::mat4& parentTrans)
dimScaled.x = std::fabs(trans[3].x + mSize.x); dimScaled.x = std::fabs(trans[3].x + mSize.x);
dimScaled.y = std::fabs(trans[3].y + mAdjustedHeight); dimScaled.y = std::fabs(trans[3].y + mAdjustedHeight);
glm::ivec2 clipDim {static_cast<int>(ceilf(dimScaled.x - trans[3].x)), glm::ivec2 clipDim {static_cast<int>(dimScaled.x - trans[3].x),
static_cast<int>(ceilf(dimScaled.y - trans[3].y))}; static_cast<int>(dimScaled.y - trans[3].y)};
// By effectively clipping the upper and lower boundaries of the container we mostly avoid // By effectively clipping the upper and lower boundaries of the container we mostly avoid
// scrolling outside the vertical starting and ending positions. // scrolling outside the vertical starting and ending positions.
@ -247,7 +254,7 @@ void ScrollableContainer::render(const glm::mat4& parentTrans)
mRenderer->pushClipRect(clipPos, clipDim); 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); mRenderer->setMatrix(trans);
if (Settings::getInstance()->getBool("DebugText")) if (Settings::getInstance()->getBool("DebugText"))

View file

@ -30,6 +30,7 @@ TextComponent::TextComponent()
, mNoTopMargin {false} , mNoTopMargin {false}
, mSelectable {false} , mSelectable {false}
, mVerticalAutoSizing {false} , mVerticalAutoSizing {false}
, mLegacyTheme {false}
{ {
} }
@ -58,6 +59,7 @@ TextComponent::TextComponent(const std::string& text,
, mNoTopMargin {false} , mNoTopMargin {false}
, mSelectable {false} , mSelectable {false}
, mVerticalAutoSizing {false} , mVerticalAutoSizing {false}
, mLegacyTheme {false}
{ {
setFont(font); setFont(font);
setColor(color); setColor(color);
@ -180,11 +182,11 @@ void TextComponent::render(const glm::mat4& parentTrans)
break; break;
} }
case ALIGN_BOTTOM: { case ALIGN_BOTTOM: {
yOff = (getSize().y - textSize.y); yOff = mSize.y - textSize.y;
break; break;
} }
case ALIGN_CENTER: { case ALIGN_CENTER: {
yOff = std::round((getSize().y - textSize.y) / 2.0f); yOff = (mSize.y - textSize.y) / 2.0f;
break; break;
} }
default: { default: {
@ -194,7 +196,7 @@ void TextComponent::render(const glm::mat4& parentTrans)
} }
else { else {
// If height is smaller than the font height, then always center vertically. // 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 // 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) if (!mFont || text.empty() || mSize.x < 0.0f)
return; return;
std::shared_ptr<Font> font {mFont}; float lineHeight {0.0f};
const float lineHeight {font->getHeight(mLineSpacing)};
const bool isMultiline {mAutoCalcExtent.y == 1 || mSize.y > lineHeight};
const bool isScrollable {mParent && mParent->isScrollable()}; const bool isScrollable {mParent && mParent->isScrollable()};
std::shared_ptr<Font> 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) { if (isMultiline && !isScrollable) {
const std::string wrappedText { const std::string wrappedText {
@ -284,7 +299,7 @@ void TextComponent::onTextChanged()
} }
if (mAutoCalcExtent.y) if (mAutoCalcExtent.y)
mSize.y = font->getTextSize().y; mSize.y = mTextCache->metrics.size.y;
if (mOpacity != 1.0f || mThemeOpacity != 1.0f) if (mOpacity != 1.0f || mThemeOpacity != 1.0f)
setOpacity(mOpacity); setOpacity(mOpacity);
@ -333,6 +348,8 @@ void TextComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
using namespace ThemeFlags; using namespace ThemeFlags;
GuiComponent::applyTheme(theme, view, element, properties); GuiComponent::applyTheme(theme, view, element, properties);
mLegacyTheme = theme->isLegacyTheme();
std::string elementType {"text"}; std::string elementType {"text"};
std::string componentName {"TextComponent"}; std::string componentName {"TextComponent"};
@ -472,5 +489,5 @@ void TextComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (properties & LINE_SPACING && elem->has("lineSpacing")) if (properties & LINE_SPACING && elem->has("lineSpacing"))
setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f)); setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f));
setFont(Font::getFromTheme(elem, properties, mFont, maxHeight)); setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, theme->isLegacyTheme()));
} }

View file

@ -78,6 +78,11 @@ public:
Alignment getHorizontalAlignment() { return mHorizontalAlignment; } Alignment getHorizontalAlignment() { return mHorizontalAlignment; }
Alignment getVerticalAlignment() { return mVerticalAlignment; } Alignment getVerticalAlignment() { return mVerticalAlignment; }
int getTextCacheGlyphHeight() override
{
return (mTextCache == nullptr ? 0 : mTextCache->metrics.maxGlyphHeight);
}
protected: protected:
virtual void onTextChanged(); virtual void onTextChanged();
@ -114,6 +119,7 @@ private:
bool mNoTopMargin; bool mNoTopMargin;
bool mSelectable; bool mSelectable;
bool mVerticalAutoSizing; bool mVerticalAutoSizing;
bool mLegacyTheme;
}; };
#endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H

View file

@ -183,8 +183,8 @@ CarouselComponent<T>::CarouselComponent()
, mMaxItemCount {3.0f} , mMaxItemCount {3.0f}
, mItemsBeforeCenter {8} , mItemsBeforeCenter {8}
, mItemsAfterCenter {8} , mItemsAfterCenter {8}
, mItemSize {glm::round( , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.25f,
glm::vec2 {Renderer::getScreenWidth() * 0.25f, Renderer::getScreenHeight() * 0.155f})} Renderer::getScreenHeight() * 0.155f}}
, mLinearInterpolation {false} , mLinearInterpolation {false}
, mInstantItemTransitions {false} , mInstantItemTransitions {false}
, mItemAxisHorizontal {false} , mItemAxisHorizontal {false}
@ -568,11 +568,10 @@ template <typename T> void CarouselComponent<T>::render(const glm::mat4& parentT
glm::mat4 carouselTrans {parentTrans}; glm::mat4 carouselTrans {parentTrans};
carouselTrans = glm::translate( carouselTrans = glm::translate(
carouselTrans, carouselTrans, glm::vec3 {GuiComponent::mPosition.x, GuiComponent::mPosition.y, 0.0f});
glm::round(glm::vec3 {GuiComponent::mPosition.x, GuiComponent::mPosition.y, 0.0f})); carouselTrans =
carouselTrans = glm::translate( glm::translate(carouselTrans, glm::vec3 {GuiComponent::mOrigin.x * mSize.x * -1.0f,
carouselTrans, glm::round(glm::vec3 {GuiComponent::mOrigin.x * mSize.x * -1.0f, GuiComponent::mOrigin.y * mSize.y * -1.0f, 0.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) if (carouselTrans[3].x + mSize.x <= 0.0f || carouselTrans[3].y + mSize.y <= 0.0f)
return; return;
@ -650,8 +649,7 @@ template <typename T> void CarouselComponent<T>::render(const glm::mat4& parentT
} }
} }
else if (mType == CarouselType::VERTICAL) { else if (mType == CarouselType::VERTICAL) {
itemSpacing.y = itemSpacing.y = ((mSize.y - (mItemSize.y * mMaxItemCount)) / mMaxItemCount) + mItemSize.y;
std::round(((mSize.y - (mItemSize.y * mMaxItemCount)) / mMaxItemCount) + mItemSize.y);
yOff = (mSize.y - mItemSize.y) / 2.0f - (camOffset * itemSpacing.y); yOff = (mSize.y - mItemSize.y) / 2.0f - (camOffset * itemSpacing.y);
if (mItemHorizontalAlignment == ALIGN_LEFT) { if (mItemHorizontalAlignment == ALIGN_LEFT) {
if (mLegacyMode) if (mLegacyMode)
@ -670,8 +668,7 @@ template <typename T> void CarouselComponent<T>::render(const glm::mat4& parentT
} }
} }
else { // HORIZONTAL. else { // HORIZONTAL.
itemSpacing.x = itemSpacing.x = ((mSize.x - (mItemSize.x * mMaxItemCount)) / mMaxItemCount) + mItemSize.x;
std::round(((mSize.x - (mItemSize.x * mMaxItemCount)) / mMaxItemCount) + mItemSize.x);
xOff = (mSize.x - mItemSize.x) / 2.0f - (camOffset * itemSpacing.x); xOff = (mSize.x - mItemSize.x) / 2.0f - (camOffset * itemSpacing.x);
if (mItemVerticalAlignment == ALIGN_TOP) { if (mItemVerticalAlignment == ALIGN_TOP) {
if (mLegacyMode) if (mLegacyMode)
@ -780,9 +777,8 @@ template <typename T> void CarouselComponent<T>::render(const glm::mat4& parentT
if (singleEntry) if (singleEntry)
itemTrans = glm::translate(carouselTrans, glm::vec3 {xOff, yOff, 0.0f}); itemTrans = glm::translate(carouselTrans, glm::vec3 {xOff, yOff, 0.0f});
else else
itemTrans = itemTrans = glm::translate(itemTrans, glm::vec3 {(i * itemSpacing.x) + xOff,
glm::translate(itemTrans, glm::vec3 {std::round(i * itemSpacing.x + xOff), (i * itemSpacing.y) + yOff, 0.0f});
std::round(i * itemSpacing.y + yOff), 0.0f});
float opacity {0.0f}; float opacity {0.0f};
@ -1011,8 +1007,8 @@ void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (elem->has("itemSize")) { if (elem->has("itemSize")) {
const glm::vec2 itemSize {glm::clamp(elem->get<glm::vec2>("itemSize"), 0.05f, 1.0f)}; const glm::vec2 itemSize {glm::clamp(elem->get<glm::vec2>("itemSize"), 0.05f, 1.0f)};
mItemSize = glm::round( mItemSize =
itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())); itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight());
} }
if (elem->has("itemScale")) if (elem->has("itemScale"))
@ -1164,8 +1160,8 @@ void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
itemSize.x = glm::clamp(itemSize.x, 0.005f, 1.0f); itemSize.x = glm::clamp(itemSize.x, 0.005f, 1.0f);
itemSize.y = glm::clamp(itemSize.y, 0.005f, 1.0f); itemSize.y = glm::clamp(itemSize.y, 0.005f, 1.0f);
} }
mItemSize = glm::round( mItemSize =
itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())); itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight());
} }
if (elem->has("maxLogoCount")) { if (elem->has("maxLogoCount")) {
@ -1210,7 +1206,7 @@ void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
} }
} }
mFont = Font::getFromTheme(elem, properties, mFont); mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, mLegacyMode);
if (elem->has("textColor")) if (elem->has("textColor"))
mTextColor = elem->get<unsigned int>("textColor"); mTextColor = elem->get<unsigned int>("textColor");

View file

@ -350,10 +350,19 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
int screenCount {0}; int screenCount {0};
float y {0.0f}; float y {0.0f};
const float entrySize { float entrySize {0.0f};
std::max(floorf(font->getHeight(1.0f)), floorf(static_cast<float>(font->getSize()))) * float lineSpacingHeight {0.0f};
mLineSpacing};
const float lineSpacingHeight {floorf(font->getHeight(mLineSpacing) - font->getHeight(1.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) { if (mLegacyMode) {
// This extra vertical margin is technically incorrect, but it adds a little extra leeway // This extra vertical margin is technically incorrect, but it adds a little extra leeway
@ -366,7 +375,9 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
floorf((mSize.y + lineSpacingHeight / 2.0f + extraMargin) / entrySize)); floorf((mSize.y + lineSpacingHeight / 2.0f + extraMargin) / entrySize));
} }
else { else {
screenCount = static_cast<int>(floorf((mSize.y + lineSpacingHeight / 2.0f) / entrySize)); // Number of entries that can fit on the screen simultaneously.
screenCount =
static_cast<int>(std::floor((mSize.y + lineSpacingHeight / 2.0f) / entrySize));
} }
if (size() >= screenCount) { if (size() >= screenCount) {
@ -555,9 +566,8 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
setColor(1, elem->get<unsigned int>("secondaryColor")); setColor(1, elem->get<unsigned int>("secondaryColor"));
} }
setFont(Font::getFromTheme(elem, properties, mFont)); setFont(Font::getFromTheme(elem, properties, mFont, 0.0f, mLegacyMode));
const float selectorHeight { const float selectorHeight {mFont->getSize() * mLineSpacing};
std::max(mFont->getHeight(1.0), static_cast<float>(mFont->getSize())) * mLineSpacing};
setSelectorHeight(selectorHeight); setSelectorHeight(selectorHeight);
if (properties & ALIGNMENT) { if (properties & ALIGNMENT) {

View file

@ -15,19 +15,20 @@
#include "utils/PlatformUtil.h" #include "utils/PlatformUtil.h"
#include "utils/StringUtil.h" #include "utils/StringUtil.h"
Font::Font(int size, const std::string& path) Font::Font(float size, const std::string& path)
: mRenderer {Renderer::getInstance()} : mRenderer {Renderer::getInstance()}
, mPath(path) , mPath(path)
, mTextSize {0.0f, 0.0f} , mFontSize {size}
, mFontSize(size) , mLetterHeight {0.0f}
, mMaxGlyphHeight {0} , mMaxGlyphHeight {static_cast<int>(std::round(size))}
, mLegacyMaxGlyphHeight {0}
{ {
if (mFontSize < 3) { if (mFontSize < 3.0f) {
mFontSize = 3; mFontSize = 3.0f;
LOG(LogWarning) << "Requested font size too small, changing to minimum supported size"; LOG(LogWarning) << "Requested font size too small, changing to minimum supported size";
} }
else if (mFontSize > Renderer::getScreenHeight() * 1.5f) { else if (mFontSize > Renderer::getScreenHeight() * 1.5f) {
mFontSize = static_cast<int>(Renderer::getScreenHeight() * 1.5f); mFontSize = Renderer::getScreenHeight() * 1.5f;
LOG(LogWarning) << "Requested font size too large, changing to maximum supported size"; 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(); initLibrary();
// Always initialize ASCII characters. // Always initialize ASCII characters.
for (unsigned int i = 32; i < 128; ++i) for (unsigned int i = 32; i < 127; ++i)
getGlyph(i); getGlyph(i);
clearFaceCache(); clearFaceCache();
@ -45,7 +46,7 @@ Font::~Font()
{ {
unload(ResourceManager::getInstance()); unload(ResourceManager::getInstance());
auto fontEntry = sFontMap.find(std::pair<std::string, int>(mPath, mFontSize)); auto fontEntry = sFontMap.find(std::pair<std::string, float>(mPath, mFontSize));
if (fontEntry != sFontMap.cend()) if (fontEntry != sFontMap.cend())
sFontMap.erase(fontEntry); sFontMap.erase(fontEntry);
@ -56,11 +57,11 @@ Font::~Font()
} }
} }
std::shared_ptr<Font> Font::get(int size, const std::string& path) std::shared_ptr<Font> Font::get(float size, const std::string& path)
{ {
const std::string canonicalPath {Utils::FileSystem::getCanonicalPath(path)}; const std::string canonicalPath {Utils::FileSystem::getCanonicalPath(path)};
std::pair<std::string, int> def {canonicalPath.empty() ? getDefaultPath() : canonicalPath, const std::pair<std::string, float> def {
size}; canonicalPath.empty() ? getDefaultPath() : canonicalPath, size};
auto foundFont = sFontMap.find(def); auto foundFont = sFontMap.find(def);
if (foundFont != sFontMap.cend()) { if (foundFont != sFontMap.cend()) {
@ -76,11 +77,9 @@ std::shared_ptr<Font> Font::get(int size, const std::string& path)
glm::vec2 Font::sizeText(std::string text, float lineSpacing) glm::vec2 Font::sizeText(std::string text, float lineSpacing)
{ {
const float lineHeight {getHeight(lineSpacing)};
float lineWidth {0.0f}; float lineWidth {0.0f};
float highestWidth {0.0f}; float highestWidth {0.0f};
const float lineHeight {getHeight(lineSpacing)};
float y {lineHeight}; float y {lineHeight};
size_t i {0}; size_t i {0};
@ -106,6 +105,20 @@ glm::vec2 Font::sizeText(std::string text, float lineSpacing)
return glm::vec2 {highestWidth, y}; return glm::vec2 {highestWidth, y};
} }
int Font::loadGlyphs(const std::string& text)
{
mMaxGlyphHeight = static_cast<int>(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, TextCache* Font::buildTextCache(const std::string& text,
float offsetX, float offsetX,
float offsetY, float offsetY,
@ -134,6 +147,9 @@ TextCache* Font::buildTextCache(const std::string& text,
yBot = getHeight(1.5); yBot = getHeight(1.5);
} }
else { 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; yTop = getGlyph('S')->bearing.y;
yBot = getHeight(lineSpacing); yBot = getHeight(lineSpacing);
} }
@ -203,7 +219,8 @@ TextCache* Font::buildTextCache(const std::string& text,
TextCache* cache {new TextCache()}; TextCache* cache {new TextCache()};
cache->vertexLists.resize(vertMap.size()); cache->vertexLists.resize(vertMap.size());
cache->metrics = {sizeText(text, lineSpacing)}; cache->metrics.size = {sizeText(text, lineSpacing)};
cache->metrics.maxGlyphHeight = mMaxGlyphHeight;
size_t i {0}; size_t i {0};
for (auto it = vertMap.cbegin(); it != vertMap.cend(); ++it) { for (auto it = vertMap.cbegin(); it != vertMap.cend(); ++it) {
@ -366,7 +383,6 @@ std::string Font::wrapText(const std::string& text,
wrappedText.append("..."); wrappedText.append("...");
} }
mTextSize = {maxLength, accumHeight};
return wrappedText; return wrappedText;
} }
@ -396,31 +412,36 @@ glm::vec2 Font::getWrappedTextCursorOffset(const std::string& wrappedText,
float Font::getLetterHeight() float Font::getLetterHeight()
{ {
Glyph* glyph {getGlyph('S')}; if (mLetterHeight == 0.0f)
assert(glyph); return mFontSize * 0.737; // Only needed if face does not contain the letter 'S'.
return glyph->texSize.y * glyph->texture->textureSize.y; else
return mLetterHeight;
} }
std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem, std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem,
unsigned int properties, unsigned int properties,
const std::shared_ptr<Font>& orig, const std::shared_ptr<Font>& orig,
const float maxHeight) const float maxHeight,
const bool legacyTheme)
{ {
mLegacyTheme = legacyTheme;
using namespace ThemeFlags; using namespace ThemeFlags;
if (!(properties & FONT_PATH) && !(properties & FONT_SIZE)) if (!(properties & FONT_PATH) && !(properties & FONT_SIZE))
return orig; return orig;
int size {static_cast<int>(orig ? orig->mFontSize : FONT_SIZE_MEDIUM)}; float size {static_cast<float>(orig ? orig->mFontSize : FONT_SIZE_MEDIUM)};
std::string path {orig ? orig->mPath : getDefaultPath()}; std::string path {orig ? orig->mPath : getDefaultPath()};
float screenHeight {static_cast<float>(Renderer::getScreenHeight())}; float screenHeight {static_cast<float>(Renderer::getScreenHeight())};
if (properties & FONT_SIZE && elem->has("fontSize")) if (properties & FONT_SIZE && elem->has("fontSize")) {
size = static_cast<int>(glm::clamp(screenHeight * elem->get<float>("fontSize"), size = glm::clamp(screenHeight * elem->get<float>("fontSize"), screenHeight * 0.001f,
screenHeight * 0.001f, screenHeight * 1.5f)); screenHeight * 1.5f);
}
if (maxHeight != 0.0f && static_cast<float>(size) > maxHeight) if (maxHeight != 0.0f && size > maxHeight)
size = static_cast<int>(maxHeight); size = maxHeight;
if (properties & FONT_PATH && elem->has("fontPath")) if (properties & FONT_PATH && elem->has("fontPath"))
path = elem->get<std::string>("fontPath"); path = elem->get<std::string>("fontPath");
@ -433,6 +454,9 @@ std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem,
path = getDefaultPath(); path = getDefaultPath();
} }
if (mLegacyTheme)
return get(std::floor(size), path);
else
return get(size, path); return get(size, path);
} }
@ -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} : 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) {
@ -566,7 +590,10 @@ Font::FontFace::FontFace(ResourceData&& d, int size, const std::string& path)
Utils::Platform::emergencyShutdown(); 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() Font::FontFace::~FontFace()
@ -712,6 +739,12 @@ Font::Glyph* Font::getGlyph(const unsigned int id)
return nullptr; return nullptr;
} }
// Use the letter 'S' as a size reference.
if (mLetterHeight == 0 && id == 'S') {
const float ratio {static_cast<float>(glyphSize.y) / std::round(mFontSize)};
mLetterHeight = mFontSize * ratio;
}
// Create glyph. // Create glyph.
Glyph& glyph {mGlyphMap[id]}; Glyph& glyph {mGlyphMap[id]};
@ -720,18 +753,18 @@ Font::Glyph* Font::getGlyph(const unsigned int id)
cursor.y / static_cast<float>(tex->textureSize.y)}; cursor.y / static_cast<float>(tex->textureSize.y)};
glyph.texSize = glm::vec2 {glyphSize.x / static_cast<float>(tex->textureSize.x), glyph.texSize = glm::vec2 {glyphSize.x / static_cast<float>(tex->textureSize.x),
glyphSize.y / static_cast<float>(tex->textureSize.y)}; glyphSize.y / static_cast<float>(tex->textureSize.y)};
glyph.advance = glm::vec2 {static_cast<float>(glyphSlot->metrics.horiAdvance) / 64.0f, glyph.advance = glm::vec2 {static_cast<float>(glyphSlot->metrics.horiAdvance) / 64.0f,
static_cast<float>(glyphSlot->metrics.vertAdvance) / 64.0f}; static_cast<float>(glyphSlot->metrics.vertAdvance) / 64.0f};
glyph.bearing = glm::vec2 {static_cast<float>(glyphSlot->metrics.horiBearingX) / 64.0f, glyph.bearing = glm::vec2 {static_cast<float>(glyphSlot->metrics.horiBearingX) / 64.0f,
static_cast<float>(glyphSlot->metrics.horiBearingY) / 64.0f}; static_cast<float>(glyphSlot->metrics.horiBearingY) / 64.0f};
glyph.rows = glyphSlot->bitmap.rows;
// Upload glyph bitmap to texture. // Upload glyph bitmap to texture.
mRenderer->updateTexture(tex->textureId, Renderer::TextureType::RED, cursor.x, cursor.y, mRenderer->updateTexture(tex->textureId, Renderer::TextureType::RED, cursor.x, cursor.y,
glyphSize.x, glyphSize.y, glyphSlot->bitmap.buffer); glyphSize.x, glyphSize.y, glyphSlot->bitmap.buffer);
if (glyphSize.y > mMaxGlyphHeight) if (glyphSize.y > mLegacyMaxGlyphHeight)
mMaxGlyphHeight = glyphSize.y; mLegacyMaxGlyphHeight = glyphSize.y;
return &glyph; return &glyph;
} }

View file

@ -21,16 +21,10 @@
class TextCache; class TextCache;
// clang-format off #define FONT_SIZE_MINI 0.030f * std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth())
#define FONT_SIZE_MINI (static_cast<unsigned int>(0.030f * \ #define FONT_SIZE_SMALL 0.035f * std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth())
std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth()))) #define FONT_SIZE_MEDIUM 0.045f * std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth())
#define FONT_SIZE_SMALL (static_cast<unsigned int>(0.035f * \ #define FONT_SIZE_LARGE 0.085f * std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth())
std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth())))
#define FONT_SIZE_MEDIUM (static_cast<unsigned int>(0.045f * \
std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth())))
#define FONT_SIZE_LARGE (static_cast<unsigned int>(0.085f * \
std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth())))
// clang-format on
#define FONT_PATH_LIGHT ":/fonts/Akrobat-Regular.ttf" #define FONT_PATH_LIGHT ":/fonts/Akrobat-Regular.ttf"
#define FONT_PATH_REGULAR ":/fonts/Akrobat-SemiBold.ttf" #define FONT_PATH_REGULAR ":/fonts/Akrobat-SemiBold.ttf"
@ -42,13 +36,19 @@ class Font : public IReloadable
{ {
public: public:
virtual ~Font(); virtual ~Font();
static std::shared_ptr<Font> get(int size, const std::string& path = getDefaultPath()); static std::shared_ptr<Font> 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. // 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); glm::vec2 sizeText(std::string text, float lineSpacing = 1.5f);
// Returns the size of the overall text area. // Used to determine mMaxGlyphHeight upfront which is needed for accurate text sizing by
const glm::vec2 getTextSize() { return mTextSize; } // 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, TextCache* buildTextCache(const std::string& text,
float offsetX, float offsetX,
@ -86,14 +86,15 @@ public:
void reload(ResourceManager& rm) override { rebuildTextures(); } void reload(ResourceManager& rm) override { rebuildTextures(); }
void unload(ResourceManager& rm) override { unloadTextures(); } void unload(ResourceManager& rm) override { unloadTextures(); }
int getSize() const { return mFontSize; } const float getSize() const { return mFontSize; }
const std::string& getPath() const { return mPath; } const std::string& getPath() const { return mPath; }
static std::string getDefaultPath() { return FONT_PATH_REGULAR; } static std::string getDefaultPath() { return FONT_PATH_REGULAR; }
static std::shared_ptr<Font> getFromTheme(const ThemeData::ThemeElement* elem, static std::shared_ptr<Font> getFromTheme(const ThemeData::ThemeElement* elem,
unsigned int properties, unsigned int properties,
const std::shared_ptr<Font>& orig, const std::shared_ptr<Font>& 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). // Returns an approximation of VRAM used by this font's texture (in bytes).
size_t getMemUsage() const; size_t getMemUsage() const;
@ -101,7 +102,7 @@ public:
static size_t getTotalMemUsage(); static size_t getTotalMemUsage();
private: private:
Font(int size, const std::string& path); Font(float size, const std::string& path);
static void initLibrary(); static void initLibrary();
struct FontTexture { struct FontTexture {
@ -127,7 +128,7 @@ private:
const ResourceData data; const ResourceData data;
FT_Face face; FT_Face face;
FontFace(ResourceData&& d, int size, const std::string& path); FontFace(ResourceData&& d, float size, const std::string& path);
virtual ~FontFace(); virtual ~FontFace();
}; };
@ -137,6 +138,7 @@ private:
glm::vec2 texSize; // In texels. glm::vec2 texSize; // In texels.
glm::vec2 advance; glm::vec2 advance;
glm::vec2 bearing; glm::vec2 bearing;
int rows;
}; };
// Completely recreate the texture data for all textures based on mGlyphs information. // Completely recreate the texture data for all textures based on mGlyphs information.
@ -159,7 +161,8 @@ private:
void clearFaceCache() { mFaceCache.clear(); } void clearFaceCache() { mFaceCache.clear(); }
static inline FT_Library sLibrary {nullptr}; static inline FT_Library sLibrary {nullptr};
static inline std::map<std::pair<std::string, int>, std::weak_ptr<Font>> sFontMap; static inline std::map<std::pair<std::string, float>, std::weak_ptr<Font>> sFontMap;
static inline bool mLegacyTheme {false};
Renderer* mRenderer; Renderer* mRenderer;
std::vector<std::unique_ptr<FontTexture>> mTextures; std::vector<std::unique_ptr<FontTexture>> mTextures;
@ -167,9 +170,10 @@ private:
std::map<unsigned int, Glyph> mGlyphMap; std::map<unsigned int, Glyph> mGlyphMap;
const std::string mPath; const std::string mPath;
glm::vec2 mTextSize; float mFontSize;
int mFontSize; float mLetterHeight;
int mMaxGlyphHeight; int mMaxGlyphHeight;
int mLegacyMaxGlyphHeight;
}; };
// Used to store a sort of "pre-rendered" string. // Used to store a sort of "pre-rendered" string.
@ -183,6 +187,7 @@ class TextCache
public: public:
struct CacheMetrics { struct CacheMetrics {
glm::vec2 size; glm::vec2 size;
int maxGlyphHeight;
} metrics; } metrics;
void setColor(unsigned int color); void setColor(unsigned int color);