From e7ada6111bba32bca73cfa39a4f4741d88d9e243 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 7 Aug 2023 22:58:35 +0200 Subject: [PATCH] Added a 'containerType' property to the text element to select between vertical and horizontal containers --- es-app/src/views/GamelistView.cpp | 6 + es-app/src/views/SystemView.cpp | 12 + es-core/src/ThemeData.cpp | 1 + es-core/src/components/TextComponent.cpp | 287 +++++++++++++++++------ es-core/src/components/TextComponent.h | 21 ++ es-core/src/resources/Font.cpp | 8 +- 6 files changed, 262 insertions(+), 73 deletions(-) diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index 8a32a5e48..2215f9a2e 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -270,6 +270,9 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) bool container {false}; if (element.second.has("container")) { container = element.second.get("container"); + if (element.second.has("containerType") && + element.second.get("containerType") == "horizontal") + container = false; } else if (element.second.has("metadata") && element.second.get("metadata") == "description") { @@ -794,6 +797,9 @@ void GamelistView::updateView(const CursorState& state) for (auto& container : mContainerComponents) container->reset(); + for (auto& scrollableText : mTextComponents) + scrollableText->resetLooping(); + for (auto& rating : mRatingComponents) rating->setValue(file->metadata.get("rating")); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 73f82ca19..13d193e5c 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -69,6 +69,9 @@ void SystemView::goToSystem(SystemData* system, bool animate) selector->setNeedsRefresh(); } + for (auto& text : mSystemElements[mPrimary->getCursor()].textComponents) + text->resetLooping(); + for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) video->setStaticVideo(); @@ -136,6 +139,9 @@ void SystemView::update(int deltaTime) { mPrimary->update(deltaTime); + for (auto& text : mSystemElements[mPrimary->getCursor()].textComponents) + text->update(deltaTime); + for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) { if (!isScrolling()) video->update(deltaTime); @@ -217,6 +223,9 @@ std::vector SystemView::getHelpPrompts() void SystemView::onCursorChanged(const CursorState& state) { + for (auto& text : mSystemElements[mPrimary->getCursor()].textComponents) + text->resetLooping(); + const int cursor {mPrimary->getCursor()}; const int scrollVelocity {mPrimary->getScrollingVelocity()}; const ViewTransitionAnimation transitionAnim {static_cast( @@ -591,6 +600,9 @@ void SystemView::populate() bool container {false}; if (element.second.has("container")) { container = element.second.get("container"); + if (element.second.has("containerType") && + element.second.get("containerType") == "horizontal") + container = false; } else if (element.second.has("metadata") && element.second.get("metadata") == "description") { diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 486946ddb..a204b601e 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -379,6 +379,7 @@ std::map> {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"container", BOOLEAN}, + {"containerType", STRING}, {"containerVerticalSnap", BOOLEAN}, {"containerScrollSpeed", FLOAT}, {"containerStartDelay", FLOAT}, diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 57d755dc1..6197b98a2 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -10,6 +10,7 @@ #include "Log.h" #include "Settings.h" +#include "Window.h" #include "utils/StringUtil.h" TextComponent::TextComponent() @@ -32,6 +33,14 @@ TextComponent::TextComponent() , mNoTopMargin {false} , mSelectable {false} , mVerticalAutoSizing {false} + , mLoopHorizontal {false} + , mLoopScroll {false} + , mLoopSpeed {0.0f} + , mLoopSpeedMultiplier {1.0f} + , mLoopDelay {1500.0f} + , mLoopOffset1 {0} + , mLoopOffset2 {0} + , mLoopTime {0} { } @@ -62,6 +71,14 @@ TextComponent::TextComponent(const std::string& text, , mNoTopMargin {false} , mSelectable {false} , mVerticalAutoSizing {false} + , mLoopHorizontal {false} + , mLoopScroll {false} + , mLoopSpeed {0.0f} + , mLoopSpeedMultiplier {1.0f} + , mLoopDelay {1500.0f} + , mLoopOffset1 {0} + , mLoopOffset2 {0} + , mLoopTime {0} { setFont(font); setColor(color); @@ -176,75 +193,124 @@ void TextComponent::render(const glm::mat4& parentTrans) 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 = mSize.y - textSize.y; - break; - } - case ALIGN_CENTER: { - yOff = (mSize.y - textSize.y) / 2.0f; - break; - } - default: { - break; - } - } - } - else { - // If height is smaller than the font height, then always center vertically. - yOff = (mSize.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, yOff, 0.0f}); - mRenderer->setMatrix(trans); - - // 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()); + // Draw the overall textbox area. If we're inside a vertical 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); } + + if (mLoopHorizontal && mTextCache != nullptr) { + // Clip everything to be inside our bounds. + glm::vec3 dim {mSize.x, mSize.y, 0.0f}; + dim.x = (trans[0].x * dim.x + trans[3].x) - trans[3].x; + dim.y = (trans[1].y * dim.y + trans[3].y) - trans[3].y; + + const int clipRectPosX {static_cast(std::round(trans[3].x))}; + const int clipRectPosY {static_cast(std::round(trans[3].y))}; + const int clipRectSizeX {static_cast(std::round(dim.x))}; + const int clipRectSizeY {static_cast(std::round(dim.y) + 1.0f)}; + + mRenderer->pushClipRect(glm::ivec2 {clipRectPosX, clipRectPosY}, + glm::ivec2 {clipRectSizeX, clipRectSizeY}); + + float offsetX {0.0f}; + + if (mTextCache->metrics.size.x < mSize.x) { + if (mHorizontalAlignment == Alignment::ALIGN_CENTER) + offsetX = static_cast((mSize.x - mTextCache->metrics.size.x) / 2.0f); + else if (mHorizontalAlignment == Alignment::ALIGN_RIGHT) + offsetX = mSize.x - mTextCache->metrics.size.x; + } + + trans = glm::translate(trans, + glm::vec3 {offsetX - static_cast(mLoopOffset1), 0.0f, 0.0f}); + } + + auto renderFunc = [this](glm::mat4 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 = mSize.y - textSize.y; + break; + } + case ALIGN_CENTER: { + yOff = (mSize.y - textSize.y) / 2.0f; + break; + } + default: { + break; + } + } + } + else { + // If height is smaller than the font height, then always center vertically. + yOff = (mSize.y - textSize.y) / 2.0f; + } + + trans = glm::translate(trans, glm::vec3 {0.0f, yOff, 0.0f}); + mRenderer->setMatrix(trans); + + // Draw the text area, where the text is actually 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( + mLoopHorizontal ? 0.0f : (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(mLoopHorizontal ? 0.0f : + 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()); + } + }; + + renderFunc(trans); + + if (mLoopHorizontal && mTextCache != nullptr && mTextCache->metrics.size.x > mSize.x) { + // Needed to avoid flickering when returning to the start position. + if (mLoopOffset1 == 0 && mLoopOffset2 == 0) + mLoopScroll = false; + // Render again if text has moved far enough for it to repeat. + if (mLoopOffset2 < 0 || (mLoopDelay != 0.0f && mLoopScroll)) { + mLoopScroll = true; + trans = glm::translate(parentTrans * getTransform(), + glm::vec3 {static_cast(-mLoopOffset2), 0.0f, 0.0f}); + mRenderer->setMatrix(trans); + renderFunc(trans); + } + } + + if (mLoopHorizontal && mTextCache != nullptr) + mRenderer->popClipRect(); } void TextComponent::setValue(const std::string& value) @@ -254,11 +320,65 @@ void TextComponent::setValue(const std::string& value) mThemeMetadata == "genre" || mThemeMetadata == "players")) { setText(mDefaultValue); } + else if (mLoopHorizontal) { + setText(Utils::String::replace(value, "\n", "")); + } else { setText(value); } } +void TextComponent::setHorizontalLooping(bool state) +{ + resetLooping(); + mLoopHorizontal = state; + + if (mLoopHorizontal) + mLoopSpeed = + mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x * 0.247f * mLoopSpeedMultiplier; +} + +void TextComponent::update(int deltaTime) +{ + if (mLoopHorizontal && mTextCache != nullptr) { + // Don't scroll if the media viewer or screensaver is active or if text scrolling + // is disabled; + if (mWindow->isMediaViewerActive() || mWindow->isScreensaverActive() || + !mWindow->getAllowTextScrolling()) { + if (mLoopTime != 0 && !mWindow->isLaunchScreenDisplayed()) + resetLooping(); + return; + } + + assert(mLoopSpeed != 0.0f); + + mLoopOffset1 = 0; + mLoopOffset2 = 0; + + if (mTextCache->metrics.size.x > mSize.x) { + // Loop the text. + const float scrollLength {mTextCache->metrics.size.x}; + const float returnLength {mLoopSpeed * 1.5f / mLoopSpeedMultiplier}; + const float scrollTime {(scrollLength * 1000.0f) / mLoopSpeed}; + const float returnTime {(returnLength * 1000.0f) / mLoopSpeed}; + const int maxTime {static_cast(mLoopDelay + scrollTime + returnTime)}; + + mLoopTime += deltaTime; + while (mLoopTime > maxTime) + mLoopTime -= maxTime; + + mLoopOffset1 = static_cast(Utils::Math::loop(mLoopDelay, scrollTime + returnTime, + static_cast(mLoopTime), + scrollLength + returnLength)); + + if (mLoopOffset1 > (scrollLength - (mSize.x - returnLength))) + mLoopOffset2 = static_cast(mLoopOffset1 - (scrollLength + returnLength)); + else if (mLoopOffset2 < 0) + mLoopOffset2 = 0; + } + } +} + void TextComponent::onTextChanged() { mTextCache.reset(); @@ -298,7 +418,10 @@ void TextComponent::onTextChanged() const bool isMultiline {mAutoCalcExtent.y == 1 || mSize.y > lineHeight}; - if (isMultiline && !isScrollable) { + if (mLoopHorizontal) { + mTextCache = std::shared_ptr(font->buildTextCache(text, 0.0f, 0.0f, mColor)); + } + else if (isMultiline && !isScrollable) { const std::string wrappedText { font->wrapText(text, mSize.x, (mVerticalAutoSizing ? 0.0f : mSize.y - lineHeight), mLineSpacing, isMultiline)}; @@ -436,6 +559,28 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, << "\" defined as \"" << verticalAlignment << "\""; } + if (elem->has("container") && elem->get("container")) { + if (elem->has("containerType")) { + const std::string& containerType {elem->get("containerType")}; + if (containerType == "horizontal") { + if (elem->has("containerScrollSpeed")) { + mLoopSpeedMultiplier = + glm::clamp(elem->get("containerScrollSpeed"), 0.1f, 10.0f); + } + if (elem->has("containerStartDelay")) { + mLoopDelay = + glm::clamp(elem->get("containerStartDelay"), 0.0f, 10.0f) * 1000.0f; + } + mLoopHorizontal = true; + } + else if (containerType != "vertical") { + LOG(LogError) << "TextComponent: Invalid theme configuration, property " + "\"containerType\" for element \"" + << element.substr(5) << "\" defined as \"" << containerType << "\""; + } + } + } + if (properties & TEXT && elem->has("text")) setText(elem->get("text")); @@ -546,4 +691,8 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, setLineSpacing(glm::clamp(elem->get("lineSpacing"), 0.5f, 3.0f)); setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, false)); + + // We need to do this after setting the font as the loop speed is calculated from its size. + if (mLoopHorizontal) + setHorizontalLooping(true); } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index e21ada423..49863f2f2 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -86,6 +86,18 @@ public: return (mTextCache == nullptr ? 0 : mTextCache->metrics.maxGlyphHeight); } + // Horizontal looping for single-line content that is too long to fit. + void setHorizontalLooping(bool state); + + void resetLooping() + { + mLoopOffset1 = 0; + mLoopOffset2 = 0; + mLoopTime = 0; + } + + void update(int deltaTime) override; + protected: virtual void onTextChanged(); @@ -131,6 +143,15 @@ private: bool mNoTopMargin; bool mSelectable; bool mVerticalAutoSizing; + + bool mLoopHorizontal; + bool mLoopScroll; + float mLoopSpeed; + float mLoopSpeedMultiplier; + float mLoopDelay; + int mLoopOffset1; + int mLoopOffset2; + int mLoopTime; }; #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index df04e328d..cc6fdc80b 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -36,7 +36,7 @@ Font::Font(float size, const std::string& path, const bool linearMagnify) initLibrary(); // Always initialize ASCII characters. - for (unsigned int i = 32; i < 127; ++i) + for (unsigned int i {32}; i < 127; ++i) getGlyph(i); clearFaceCache(); @@ -110,7 +110,7 @@ int Font::loadGlyphs(const std::string& text) { mMaxGlyphHeight = static_cast(std::round(mFontSize)); - for (size_t i = 0; i < text.length();) { + for (size_t i {0}; i < text.length();) { unsigned int character {Utils::String::chars2Unicode(text, i)}; // Advances i. Glyph* glyph {getGlyph(character)}; @@ -207,7 +207,7 @@ TextCache* Font::buildTextCache(const std::string& text, color}; // Round vertices. - for (int i = 1; i < 5; ++i) + for (int i {1}; i < 5; ++i) vertices[i].position = glm::round(vertices[i].position); // Make duplicates of first and last vertex so this can be rendered as a triangle strip. @@ -692,7 +692,7 @@ FT_Face Font::getFaceForChar(unsigned int id) static const std::vector fallbackFonts {getFallbackFontPaths()}; // Look for the glyph in our current font and then in the fallback fonts if needed. - for (unsigned int i = 0; i < fallbackFonts.size() + 1; ++i) { + for (unsigned int i {0}; i < fallbackFonts.size() + 1; ++i) { auto fit = mFaceCache.find(i); if (fit == mFaceCache.cend()) {