// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // TextComponent.cpp // // Displays text. // #include "components/TextComponent.h" #include "Log.h" #include "Settings.h" #include "utils/StringUtil.h" TextComponent::TextComponent(Window* window) : GuiComponent(window) , mFont(Font::get(FONT_SIZE_MEDIUM)) , mUppercase(false) , mColor(0x000000FF) , mAutoCalcExtent(true, true) , mHorizontalAlignment(ALIGN_LEFT) , mVerticalAlignment(ALIGN_CENTER) , mLineSpacing(1.5f) , mNoTopMargin(false) , mBgColor(0) , mMargin(0.0f) , mRenderBackground(false) { } TextComponent::TextComponent(Window* window, const std::string& text, const std::shared_ptr& font, unsigned int color, Alignment align, glm::vec3 pos, glm::vec2 size, unsigned int bgcolor, float margin) : GuiComponent(window) , mFont(nullptr) , mUppercase(false) , mColor(0x000000FF) , mAutoCalcExtent(true, true) , mHorizontalAlignment(align) , mVerticalAlignment(ALIGN_CENTER) , mLineSpacing(1.5f) , mNoTopMargin(false) , mBgColor(0) , mMargin(margin) , mRenderBackground(false) { setFont(font); setColor(color); setBackgroundColor(bgcolor); setText(text); setPosition(pos); setSize(size); } void TextComponent::onSizeChanged() { mAutoCalcExtent = glm::ivec2{(getSize().x == 0), (getSize().y == 0)}; onTextChanged(); } void TextComponent::setFont(const std::shared_ptr& font) { mFont = font; onTextChanged(); } // Set the color of the font/text. void TextComponent::setColor(unsigned int color) { mColor = color; mColorOpacity = mColor & 0x000000FF; onColorChanged(); } // Set the color of the background box. void TextComponent::setBackgroundColor(unsigned int color) { mBgColor = color; mBgColorOpacity = mBgColor & 0x000000FF; } // Scale the opacity. void TextComponent::setOpacity(unsigned char opacity) { // This function is mostly called to do fade in and fade out of the text component element. // Therefore we assume here that opacity is a fractional value (expressed as an unsigned // char 0 - 255) of the opacity originally set with setColor() or setBackgroundColor(). unsigned char o = static_cast(static_cast(opacity) / 255.0f * static_cast(mColorOpacity)); mColor = (mColor & 0xFFFFFF00) | static_cast(o); unsigned char bgo = static_cast(static_cast(opacity) / 255.0f * static_cast(mBgColorOpacity)); mBgColor = (mBgColor & 0xFFFFFF00) | static_cast(bgo); onColorChanged(); GuiComponent::setOpacity(opacity); } void TextComponent::setText(const std::string& text) { mText = text; onTextChanged(); } void TextComponent::setUppercase(bool uppercase) { mUppercase = uppercase; onTextChanged(); } void TextComponent::render(const glm::mat4& parentTrans) { if (!isVisible()) return; glm::mat4 trans{parentTrans * getTransform()}; if (mRenderBackground) { Renderer::setMatrix(trans); Renderer::drawRect(0.0f, 0.0f, mSize.x, mSize.y, mBgColor, mBgColor); } if (mTextCache) { const glm::vec2& textSize{mTextCache->metrics.size}; float yOff = 0.0f; switch (mVerticalAlignment) { case ALIGN_TOP: { yOff = 0.0f; break; } case ALIGN_BOTTOM: { yOff = (getSize().y - textSize.y); break; } case ALIGN_CENTER: { yOff = (getSize().y - textSize.y) / 2.0f; break; } default: { break; } } glm::vec3 off{0.0f, yOff, 0.0f}; if (Settings::getInstance()->getBool("DebugText")) { // Draw the "textbox" area, what we are aligned within. Renderer::setMatrix(trans); Renderer::drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x0000FF33, 0x0000FF33); } trans = glm::translate(trans, off); Renderer::setMatrix(trans); // Draw the text area, where the text actually is located. if (Settings::getInstance()->getBool("DebugText")) { switch (mHorizontalAlignment) { case ALIGN_LEFT: { Renderer::drawRect(0.0f, 0.0f, mTextCache->metrics.size.x, mTextCache->metrics.size.y, 0x00000033, 0x00000033); break; } case ALIGN_CENTER: { Renderer::drawRect((mSize.x - mTextCache->metrics.size.x) / 2.0f, 0.0f, mTextCache->metrics.size.x, mTextCache->metrics.size.y, 0x00000033, 0x00000033); break; } case ALIGN_RIGHT: { Renderer::drawRect(mSize.x - mTextCache->metrics.size.x, 0.0f, mTextCache->metrics.size.x, mTextCache->metrics.size.y, 0x00000033, 0x00000033); break; } default: { break; } } } mFont->renderTextCache(mTextCache.get()); } } void TextComponent::calculateExtent() { if (mAutoCalcExtent.x) { mSize = mFont->sizeText(mUppercase ? Utils::String::toUpper(mText) : mText, mLineSpacing); } else { if (mAutoCalcExtent.y) mSize.y = mFont ->sizeWrappedText(mUppercase ? Utils::String::toUpper(mText) : mText, getSize().x, mLineSpacing) .y; } } void TextComponent::onTextChanged() { calculateExtent(); if (!mFont || mText.empty()) { mTextCache.reset(); return; } std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; std::shared_ptr f = mFont; const bool isMultiline = (mSize.y == 0.0f || mSize.y > f->getHeight() * 1.2f); bool addAbbrev = false; if (!isMultiline) { size_t newline = text.find('\n'); // Single line of text - stop at the first newline since it'll mess everything up. text = text.substr(0, newline); addAbbrev = newline != std::string::npos; } glm::vec2 size{f->sizeText(text)}; if (!isMultiline && mSize.x > 0.0f && text.size() && (size.x > mSize.x || addAbbrev)) { // Abbreviate text. const std::string abbrev = "..."; glm::vec2 abbrevSize{f->sizeText(abbrev)}; // mMargin adds a margin around the text if it's abbreviated. float marginAdjustedSize = mSize.x - (mSize.x * mMargin); while (text.size() && size.x + abbrevSize.x > marginAdjustedSize) { size_t newSize = Utils::String::prevCursor(text, text.size()); text.erase(newSize, text.size() - newSize); size = f->sizeText(text); } text.append(abbrev); mTextCache = std::shared_ptr( f->buildTextCache(text, glm::vec2{}, (mColor >> 8 << 8) | mOpacity, mSize.x, mHorizontalAlignment, mLineSpacing, mNoTopMargin)); } else { mTextCache = std::shared_ptr(f->buildTextCache( f->wrapText(text, mSize.x), glm::vec2{}, (mColor >> 8 << 8) | mOpacity, mSize.x, mHorizontalAlignment, mLineSpacing, mNoTopMargin)); } } void TextComponent::onColorChanged() { if (mTextCache) mTextCache->setColor(mColor); } void TextComponent::setHorizontalAlignment(Alignment align) { mHorizontalAlignment = align; onTextChanged(); } void TextComponent::setVerticalAlignment(Alignment align) { mVerticalAlignment = align; } void TextComponent::setLineSpacing(float spacing) { mLineSpacing = spacing; onTextChanged(); } void TextComponent::setNoTopMargin(bool margin) { mNoTopMargin = margin; onTextChanged(); } void TextComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { GuiComponent::applyTheme(theme, view, element, properties); using namespace ThemeFlags; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "text"); if (!elem) return; if (properties & COLOR && elem->has("color")) setColor(elem->get("color")); setRenderBackground(false); if (properties & COLOR && elem->has("backgroundColor")) { setBackgroundColor(elem->get("backgroundColor")); setRenderBackground(true); } if (properties & ALIGNMENT && elem->has("alignment")) { std::string str = elem->get("alignment"); if (str == "left") setHorizontalAlignment(ALIGN_LEFT); else if (str == "center") setHorizontalAlignment(ALIGN_CENTER); else if (str == "right") setHorizontalAlignment(ALIGN_RIGHT); else LOG(LogError) << "Unknown text alignment string: " << str; } if (properties & TEXT && elem->has("text")) setText(elem->get("text")); if (properties & FORCE_UPPERCASE && elem->has("forceUppercase")) setUppercase(elem->get("forceUppercase")); if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(elem->get("lineSpacing")); setFont(Font::getFromTheme(elem, properties, mFont)); }