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()
{
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});

View file

@ -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;

View file

@ -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<ThemeData>& 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<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"))
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;
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<float>(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<float>(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<int>(rowModifier * static_cast<float>(mAdjustedAutoScrollSpeed))) {
if (contentSize.y > mAdjustedHeight)
if (!mAtEnd && contentSize.y > mAdjustedHeight)
mScrollPos += mScrollDir;
mAutoScrollAccumulator -=
static_cast<int>(rowModifier * static_cast<float>(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<int>(ceilf(dimScaled.x - trans[3].x)),
static_cast<int>(ceilf(dimScaled.y - trans[3].y))};
glm::ivec2 clipDim {static_cast<int>(dimScaled.x - trans[3].x),
static_cast<int>(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"))

View file

@ -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> 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> 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<ThemeData>& 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<ThemeData>& theme,
if (properties & LINE_SPACING && elem->has("lineSpacing"))
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 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

View file

@ -183,8 +183,8 @@ CarouselComponent<T>::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 <typename T> void CarouselComponent<T>::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 <typename T> void CarouselComponent<T>::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 <typename T> void CarouselComponent<T>::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 <typename T> void CarouselComponent<T>::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<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (elem->has("itemSize")) {
const glm::vec2 itemSize {glm::clamp(elem->get<glm::vec2>("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<T>::applyTheme(const std::shared_ptr<ThemeData>& 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<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"))
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};
float y {0.0f};
const float entrySize {
std::max(floorf(font->getHeight(1.0f)), floorf(static_cast<float>(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 <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
floorf((mSize.y + lineSpacingHeight / 2.0f + extraMargin) / entrySize));
}
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) {
@ -555,9 +566,8 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
setColor(1, elem->get<unsigned int>("secondaryColor"));
}
setFont(Font::getFromTheme(elem, properties, mFont));
const float selectorHeight {
std::max(mFont->getHeight(1.0), static_cast<float>(mFont->getSize())) * mLineSpacing};
setFont(Font::getFromTheme(elem, properties, mFont, 0.0f, mLegacyMode));
const float selectorHeight {mFont->getSize() * mLineSpacing};
setSelectorHeight(selectorHeight);
if (properties & ALIGNMENT) {

View file

@ -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<int>(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<int>(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<std::string, int>(mPath, mFontSize));
auto fontEntry = sFontMap.find(std::pair<std::string, float>(mPath, mFontSize));
if (fontEntry != sFontMap.cend())
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)};
std::pair<std::string, int> def {canonicalPath.empty() ? getDefaultPath() : canonicalPath,
size};
const std::pair<std::string, float> def {
canonicalPath.empty() ? getDefaultPath() : canonicalPath, size};
auto foundFont = sFontMap.find(def);
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)
{
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<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,
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> Font::getFromTheme(const ThemeData::ThemeElement* elem,
unsigned int properties,
const std::shared_ptr<Font>& 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<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()};
float screenHeight {static_cast<float>(Renderer::getScreenHeight())};
if (properties & FONT_SIZE && elem->has("fontSize"))
size = static_cast<int>(glm::clamp(screenHeight * elem->get<float>("fontSize"),
screenHeight * 0.001f, screenHeight * 1.5f));
if (properties & FONT_SIZE && elem->has("fontSize")) {
size = glm::clamp(screenHeight * elem->get<float>("fontSize"), screenHeight * 0.001f,
screenHeight * 1.5f);
}
if (maxHeight != 0.0f && static_cast<float>(size) > maxHeight)
size = static_cast<int>(maxHeight);
if (maxHeight != 0.0f && size > maxHeight)
size = maxHeight;
if (properties & FONT_PATH && elem->has("fontPath"))
path = elem->get<std::string>("fontPath");
@ -433,6 +454,9 @@ std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem,
path = getDefaultPath();
}
if (mLegacyTheme)
return get(std::floor(size), path);
else
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}
{
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();
}
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<float>(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<float>(tex->textureSize.y)};
glyph.texSize = glm::vec2 {glyphSize.x / static_cast<float>(tex->textureSize.x),
glyphSize.y / static_cast<float>(tex->textureSize.y)};
glyph.advance = glm::vec2 {static_cast<float>(glyphSlot->metrics.horiAdvance) / 64.0f,
static_cast<float>(glyphSlot->metrics.vertAdvance) / 64.0f};
glyph.bearing = glm::vec2 {static_cast<float>(glyphSlot->metrics.horiBearingX) / 64.0f,
static_cast<float>(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;
}

View file

@ -21,16 +21,10 @@
class TextCache;
// clang-format off
#define FONT_SIZE_MINI (static_cast<unsigned int>(0.030f * \
std::min(Renderer::getScreenHeight(), Renderer::getScreenWidth())))
#define FONT_SIZE_SMALL (static_cast<unsigned int>(0.035f * \
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_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<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.
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<Font> getFromTheme(const ThemeData::ThemeElement* elem,
unsigned int properties,
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).
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::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;
std::vector<std::unique_ptr<FontTexture>> mTextures;
@ -167,9 +170,10 @@ private:
std::map<unsigned int, Glyph> 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);