ES-DE/es-core/src/components/TextComponent.cpp
Leon Styhre 08d5e4eff0 Enabled vertical abbreviations of multiline text entries in TextComponent.
Also fixed an issue where the debug overlay would not get rendered correctly for scrollable containers.
2022-08-21 16:51:21 +02:00

525 lines
16 KiB
C++

// 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()
: mFont {Font::get(FONT_SIZE_MEDIUM)}
, mRenderer {Renderer::getInstance()}
, mColor {0x000000FF}
, mBgColor {0x00000000}
, mColorOpacity {1.0f}
, mBgColorOpacity {0.0f}
, mRenderBackground {false}
, mUppercase {false}
, mLowercase {false}
, mCapitalize {false}
, mAutoCalcExtent {1, 1}
, mHorizontalAlignment {ALIGN_LEFT}
, mVerticalAlignment {ALIGN_CENTER}
, mLineSpacing {1.5f}
, mNoTopMargin {false}
, mSelectable {false}
{
}
TextComponent::TextComponent(const std::string& text,
const std::shared_ptr<Font>& font,
unsigned int color,
Alignment align,
glm::vec3 pos,
glm::vec2 size,
unsigned int bgcolor)
: mFont {nullptr}
, mRenderer {Renderer::getInstance()}
, mColor {0x000000FF}
, mBgColor {0x00000000}
, mColorOpacity {1.0f}
, mBgColorOpacity {0.0f}
, mRenderBackground {false}
, mUppercase {false}
, mLowercase {false}
, mCapitalize {false}
, mAutoCalcExtent {1, 1}
, mHorizontalAlignment {align}
, mVerticalAlignment {ALIGN_CENTER}
, mLineSpacing {1.5f}
, mNoTopMargin {false}
, mSelectable {false}
{
setFont(font);
setColor(color);
setBackgroundColor(bgcolor);
setText(text, false);
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>& font)
{
if (mFont == font)
return;
mFont = font;
onTextChanged();
}
// Set the color of the font/text.
void TextComponent::setColor(unsigned int color)
{
mColor = color;
mColorOpacity = static_cast<float>(mColor & 0x000000FF) / 255.0f;
onColorChanged();
}
// Set the color of the background box.
void TextComponent::setBackgroundColor(unsigned int color)
{
mBgColor = color;
mBgColorOpacity = static_cast<float>(mBgColor & 0x000000FF) / 255.0f;
}
void TextComponent::setOpacity(float opacity)
{
float textOpacity {opacity * mColorOpacity};
mColor = (mColor & 0xFFFFFF00) | static_cast<unsigned char>(textOpacity * 255.0f);
float textBackgroundOpacity {opacity * mBgColorOpacity};
mBgColor = (mBgColor & 0xFFFFFF00) | static_cast<unsigned char>(textBackgroundOpacity * 255.0f);
onColorChanged();
GuiComponent::setOpacity(opacity);
if (mTextCache)
mTextCache->setOpacity(mThemeOpacity);
}
void TextComponent::setDimming(float dimming)
{
mDimming = dimming;
if (mTextCache)
mTextCache->setDimming(dimming);
}
void TextComponent::setText(const std::string& text, bool update)
{
if (mText == text)
return;
mText = text;
if (update)
onTextChanged();
}
void TextComponent::setUppercase(bool uppercase)
{
mUppercase = uppercase;
if (uppercase) {
mLowercase = false;
mCapitalize = false;
}
onTextChanged();
}
void TextComponent::setLowercase(bool lowercase)
{
mLowercase = lowercase;
if (lowercase) {
mUppercase = false;
mCapitalize = false;
}
onTextChanged();
}
void TextComponent::setCapitalize(bool capitalize)
{
mCapitalize = capitalize;
if (capitalize) {
mUppercase = false;
mLowercase = false;
}
onTextChanged();
}
void TextComponent::render(const glm::mat4& parentTrans)
{
if (!isVisible() || mThemeOpacity == 0.0f || mSize.x == 0.0f || mSize.y == 0.0f)
return;
glm::mat4 trans {parentTrans * getTransform()};
mRenderer->setMatrix(trans);
if (mRenderBackground)
mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, mBgColor, mBgColor, false,
mOpacity * mThemeOpacity, mDimming);
if (mTextCache) {
const glm::vec2& textSize {mTextCache->metrics.size};
float yOff {0.0f};
if (mSize.y > textSize.y) {
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;
}
}
}
else {
// If height is smaller than the font height, then always center vertically.
yOff = (getSize().y - textSize.y) / 2.0f;
}
// Draw the overall textbox area. If we're inside a scrollable container then this
// area is rendered inside that component instead of here.
if (Settings::getInstance()->getBool("DebugText")) {
if (!mParent || !mParent->isScrollable())
mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x0000FF33, 0x0000FF33);
}
trans = glm::translate(trans, glm::vec3 {0.0f, std::round(yOff), 0.0f});
// Don't round vertices if scaled as it may lead to single-pixel alignment issues.
if (mScale == 1.0f)
mRenderer->setMatrix(trans, true);
else
mRenderer->setMatrix(trans, false);
// Draw the text area, where the text actually is located.
if (Settings::getInstance()->getBool("DebugText")) {
switch (mHorizontalAlignment) {
case ALIGN_LEFT: {
mRenderer->drawRect(0.0f, 0.0f, mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
break;
}
case ALIGN_CENTER: {
mRenderer->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: {
mRenderer->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) {
if (mUppercase)
mSize = mFont->sizeText(Utils::String::toUpper(mText), mLineSpacing);
else if (mLowercase)
mSize = mFont->sizeText(Utils::String::toLower(mText), mLineSpacing);
else if (mCapitalize)
mSize = mFont->sizeText(Utils::String::toCapitalized(mText), mLineSpacing);
else
mSize = mFont->sizeText(mText, mLineSpacing); // Original case.
}
else {
if (mAutoCalcExtent.y) {
if (mUppercase) {
mSize.y =
mFont->sizeWrappedText(Utils::String::toUpper(mText), getSize().x, mLineSpacing)
.y;
}
else if (mLowercase) {
mSize.y =
mFont->sizeWrappedText(Utils::String::toLower(mText), getSize().x, mLineSpacing)
.y;
}
else if (mCapitalize) {
mSize.y = mFont
->sizeWrappedText(Utils::String::toCapitalized(mText), getSize().x,
mLineSpacing)
.y;
}
else {
mSize.y = mFont->sizeWrappedText(mText, getSize().x, mLineSpacing).y;
}
}
}
}
void TextComponent::onTextChanged()
{
calculateExtent();
if (!mFont || mText.empty() || mSize.x == 0.0f || mSize.y == 0.0f) {
mTextCache.reset();
return;
}
std::string text;
if (mUppercase)
text = Utils::String::toUpper(mText);
else if (mLowercase)
text = Utils::String::toLower(mText);
else if (mCapitalize)
text = Utils::String::toCapitalized(mText);
else
text = mText; // Original case.
std::shared_ptr<Font> f {mFont};
const float lineHeight {f->getHeight(mLineSpacing)};
const bool isMultiline {mSize.y > lineHeight};
const bool isScrollable {mParent && mParent->isScrollable()};
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 && text.size() && (size.x > mSize.x || addAbbrev)) {
// Abbreviate text.
const std::string abbrev {"..."};
float abbrevSize {f->sizeText(abbrev).x};
while (text.size() && size.x + abbrevSize > mSize.x) {
size_t newSize {Utils::String::prevCursor(text, text.size())};
text.erase(newSize, text.size() - newSize);
if (!text.empty() && text.back() == ' ')
text.pop_back();
size = f->sizeText(text);
}
text.append(abbrev);
mTextCache = std::shared_ptr<TextCache>(f->buildTextCache(
text, glm::vec2 {}, mColor, mSize.x, mHorizontalAlignment, mLineSpacing, mNoTopMargin));
}
else if (isMultiline && text.size() && !isScrollable) {
const std::string wrappedText {
f->wrapText(text, mSize.x, mSize.y - lineHeight, mLineSpacing)};
mTextCache = std::shared_ptr<TextCache>(f->buildTextCache(wrappedText, glm::vec2 {}, mColor,
mSize.x, mHorizontalAlignment,
mLineSpacing, mNoTopMargin));
}
else {
mTextCache = std::shared_ptr<TextCache>(
f->buildTextCache(f->wrapText(text, mSize.x), glm::vec2 {}, mColor, mSize.x,
mHorizontalAlignment, mLineSpacing, mNoTopMargin));
}
if (mOpacity != 1.0f || mThemeOpacity != 1.0f)
setOpacity(mOpacity);
// This is required to set the color transparency.
onColorChanged();
}
void TextComponent::onColorChanged()
{
if (mTextCache)
mTextCache->setColor(mColor);
}
void TextComponent::setHorizontalAlignment(Alignment align)
{
mHorizontalAlignment = align;
onTextChanged();
}
void TextComponent::setLineSpacing(float spacing)
{
mLineSpacing = spacing;
onTextChanged();
}
void TextComponent::setNoTopMargin(bool margin)
{
mNoTopMargin = margin;
onTextChanged();
}
std::vector<HelpPrompt> TextComponent::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
if (mSelectable)
prompts.push_back(HelpPrompt("a", "select"));
return prompts;
}
void TextComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
const std::string& view,
const std::string& element,
unsigned int properties)
{
using namespace ThemeFlags;
GuiComponent::applyTheme(theme, view, element, properties);
std::string elementType {"text"};
std::string componentName {"TextComponent"};
if (element.substr(0, 13) == "gamelistinfo_") {
elementType = "gamelistinfo";
componentName = "gamelistInfoComponent";
}
const ThemeData::ThemeElement* elem = theme->getElement(view, element, elementType);
if (!elem)
return;
if (elem->has("metadataElement") && elem->get<bool>("metadataElement"))
mComponentThemeFlags |= ComponentThemeFlags::METADATA_ELEMENT;
if (properties & COLOR && elem->has("color"))
setColor(elem->get<unsigned int>("color"));
setRenderBackground(false);
if (properties & COLOR && elem->has("backgroundColor")) {
setBackgroundColor(elem->get<unsigned int>("backgroundColor"));
setRenderBackground(true);
}
if (properties & ALIGNMENT && elem->has("horizontalAlignment")) {
std::string str {elem->get<std::string>("horizontalAlignment")};
if (str == "left")
setHorizontalAlignment(ALIGN_LEFT);
else if (str == "center")
setHorizontalAlignment(ALIGN_CENTER);
else if (str == "right")
setHorizontalAlignment(ALIGN_RIGHT);
else
LOG(LogWarning) << componentName
<< ": Invalid theme configuration, property "
"<horizontalAlignment> defined as \""
<< str << "\"";
}
if (properties & ALIGNMENT && elem->has("verticalAlignment")) {
std::string str {elem->get<std::string>("verticalAlignment")};
if (str == "top")
setVerticalAlignment(ALIGN_TOP);
else if (str == "center")
setVerticalAlignment(ALIGN_CENTER);
else if (str == "bottom")
setVerticalAlignment(ALIGN_BOTTOM);
else
LOG(LogWarning) << componentName
<< ": Invalid theme configuration, property "
"<verticalAlignment> defined as \""
<< str << "\"";
}
// Legacy themes only.
if (properties & ALIGNMENT && elem->has("alignment")) {
std::string str {elem->get<std::string>("alignment")};
if (str == "left")
setHorizontalAlignment(ALIGN_LEFT);
else if (str == "center")
setHorizontalAlignment(ALIGN_CENTER);
else if (str == "right")
setHorizontalAlignment(ALIGN_RIGHT);
else
LOG(LogWarning) << componentName
<< ": Invalid theme configuration, property "
"<alignment> defined as \""
<< str << "\"";
}
if (properties & TEXT && elem->has("text"))
setText(elem->get<std::string>("text"));
if (properties & METADATA && elem->has("systemdata")) {
mThemeSystemdata = "";
const std::string systemdata {elem->get<std::string>("systemdata")};
for (auto& type : supportedSystemdataTypes) {
if (type == systemdata) {
mThemeSystemdata = type;
break;
}
}
if (mThemeSystemdata == "") {
LOG(LogError)
<< "TextComponent: Invalid theme configuration, property <systemdata> defined as \""
<< systemdata << "\"";
}
}
if (properties & METADATA && elem->has("metadata")) {
mThemeMetadata = "";
const std::string metadata {elem->get<std::string>("metadata")};
for (auto& type : supportedMetadataTypes) {
if (type == metadata) {
mThemeMetadata = type;
break;
}
}
if (mThemeMetadata == "") {
LOG(LogError)
<< "TextComponent: Invalid theme configuration, property <metadata> defined as \""
<< metadata << "\"";
}
}
if (properties & LETTER_CASE && elem->has("letterCase")) {
std::string letterCase {elem->get<std::string>("letterCase")};
if (letterCase == "uppercase") {
setUppercase(true);
}
else if (letterCase == "lowercase") {
setLowercase(true);
}
else if (letterCase == "capitalize") {
setCapitalize(true);
}
else if (letterCase != "none") {
LOG(LogWarning)
<< "TextComponent: Invalid theme configuration, property <letterCase> defined as \""
<< letterCase << "\"";
}
}
// Legacy themes only.
if (properties & FORCE_UPPERCASE && elem->has("forceUppercase"))
setUppercase(elem->get<bool>("forceUppercase"));
if (properties & LINE_SPACING && elem->has("lineSpacing"))
setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f));
setFont(Font::getFromTheme(elem, properties, mFont));
}