// SPDX-License-Identifier: MIT // // ES-DE Frontend // ScrollableContainer.cpp // // Component containing scrollable information, used for the game // description text in the scraper and gamelist views. // #include "components/ScrollableContainer.h" #include "Window.h" #include "animations/LambdaAnimation.h" #include "renderers/Renderer.h" #include "resources/Font.h" ScrollableContainer::ScrollableContainer() : mRenderer {Renderer::getInstance()} , mScrollPos {0.0f, 0.0f} , mScrollDir {0.0f, 0.0f} , mAdjustedHeight {0.0f} , mClipSpacing {0.0f} , mAutoScrollDelay {0} , mAutoScrollSpeed {0} , mAutoScrollAccumulator {0} , mAutoScrollResetAccumulator {0} , mAdjustedAutoScrollSpeed {0} , mVerticalSnap {true} , mUpdatedSize {false} { // Set the modifier to get equivalent scrolling speed regardless of screen resolution. mResolutionModifier = mRenderer->getScreenResolutionModifier(); mAutoScrollResetDelayConstant = AUTO_SCROLL_RESET_DELAY; mAutoScrollDelayConstant = AUTO_SCROLL_DELAY; mAutoScrollSpeedConstant = AUTO_SCROLL_SPEED; } void ScrollableContainer::setAutoScroll(bool autoScroll) { if (autoScroll) { mScrollDir = glm::vec2 {0.0f, 1.0f}; mAutoScrollDelay = static_cast(mAutoScrollDelayConstant); resetComponent(); } else { mScrollDir = glm::vec2 {}; mAutoScrollDelay = 0; mAutoScrollSpeed = 0; mAutoScrollAccumulator = 0; } } void ScrollableContainer::setScrollParameters(float autoScrollDelayConstant, float autoScrollResetDelayConstant, float autoScrollSpeedConstant) { mAutoScrollResetDelayConstant = glm::clamp(autoScrollResetDelayConstant, 1000.0f, 10000.0f); mAutoScrollDelayConstant = glm::clamp(autoScrollDelayConstant, 1000.0f, 10000.0f); mAutoScrollSpeedConstant = AUTO_SCROLL_SPEED / glm::clamp(autoScrollSpeedConstant, 0.1f, 10.0f); } void ScrollableContainer::resetComponent() { mScrollPos = glm::vec2 {0.0f, 0.0f}; mAutoScrollResetAccumulator = 0; mAutoScrollAccumulator = -mAutoScrollDelay + mAutoScrollSpeed; mAtEnd = false; mUpdatedSize = false; // This applies to the actual TextComponent that is getting displayed. mChildren.front()->setAutoCalcExtent(glm::ivec2 {0, 1}); // This is needed to resize to the designated area when the background image gets invalidated. if (!mChildren.empty()) { float combinedHeight {0.0f}; const float cacheGlyphHeight { mChildren.front()->getTextCache() == nullptr ? 0.0f : static_cast(mChildren.front()->getTextCache()->metrics.maxGlyphHeight)}; 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)}; if (numLines == 0) numLines = 1; mAdjustedHeight = std::ceil(numLines * combinedHeight); } else { mAdjustedHeight = mSize.y; } } } } void ScrollableContainer::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { using namespace ThemeFlags; GuiComponent::applyTheme(theme, view, element, properties); const ThemeData::ThemeElement* elem {theme->getElement(view, element, "text")}; if (!elem) return; if (elem->has("containerVerticalSnap")) mVerticalSnap = elem->get("containerVerticalSnap"); if (elem->has("containerScrollSpeed")) { mAutoScrollSpeedConstant = AUTO_SCROLL_SPEED / glm::clamp(elem->get("containerScrollSpeed"), 0.1f, 10.0f); } if (elem->has("containerStartDelay")) { mAutoScrollDelayConstant = glm::clamp(elem->get("containerStartDelay"), 0.0f, 10.0f) * 1000.0f; } if (elem->has("containerResetDelay")) { mAutoScrollResetDelayConstant = glm::clamp(elem->get("containerResetDelay"), 0.0f, 20.0f) * 1000.0f; } } void ScrollableContainer::update(int deltaTime) { if (!isVisible() || mSize == glm::vec2 {0.0f, 0.0f}) return; const glm::vec2 contentSize {glm::round(mChildren.front()->getSize())}; float rowModifier {1.0f}; const float lineSpacing {mChildren.front()->getLineSpacing()}; float combinedHeight {0.0f}; const float cacheGlyphHeight { mChildren.front()->getTextCache() == nullptr ? 0.0f : static_cast(mChildren.front()->getTextCache()->metrics.maxGlyphHeight)}; 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) { const float minimumSpacing {mChildren.front()->getFont()->getHeight(1.2f)}; const float currentSpacing {mChildren.front()->getFont()->getHeight(lineSpacing)}; mClipSpacing = std::round((currentSpacing - minimumSpacing) / 2.0f); } // Resize container to font height boundary to avoid rendering a fraction of the last line. if (!mUpdatedSize) { if (mVerticalSnap) { float numLines {std::floor(mSize.y / combinedHeight)}; if (numLines == 0) numLines = 1; mAdjustedHeight = std::ceil(numLines * combinedHeight); } else { mAdjustedHeight = mSize.y; } mUpdatedSize = true; } // 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 (mScrollPos != glm::vec2 {0.0f, 0.0f} && !mWindow->isLaunchScreenDisplayed()) resetComponent(); return; } if (!mAdjustedAutoScrollSpeed) { float fontSize {static_cast(mChildren.front()->getFont()->getSize())}; float width {contentSize.x / (fontSize * 1.3f)}; // Keep speed adjustments within reason. float speedModifier {glm::clamp(width, 10.0f, 40.0f)}; speedModifier *= mAutoScrollSpeedConstant; speedModifier /= mResolutionModifier; mAdjustedAutoScrollSpeed = static_cast(speedModifier); } // If there are less than 8 lines of text, accelerate the scrolling further. const float lines {mAdjustedHeight / combinedHeight}; if (lines < 8.0f) rowModifier = lines / 8.0f; if (mAdjustedAutoScrollSpeed < 0) mAdjustedAutoScrollSpeed = 1; if (mAdjustedAutoScrollSpeed != 0) { mAutoScrollAccumulator += deltaTime; while (mAutoScrollAccumulator >= static_cast(rowModifier * static_cast(mAdjustedAutoScrollSpeed))) { if (!mAtEnd && contentSize.y > mAdjustedHeight) mScrollPos += mScrollDir; mAutoScrollAccumulator -= static_cast(rowModifier * static_cast(mAdjustedAutoScrollSpeed)); } } // Clip scrolling within bounds. if (mScrollPos.x < 0.0f) mScrollPos.x = 0.0f; if (mScrollPos.y < 0.0f) mScrollPos.y = 0.0f; if (mScrollPos.x + std::round(mSize.x) > contentSize.x) { mScrollPos.x = contentSize.x - std::round(mSize.x); mAtEnd = true; } if (contentSize.y < mAdjustedHeight) mScrollPos.y = 0.0f; else if (mScrollPos.y + mAdjustedHeight > contentSize.y) mAtEnd = true; if (mAtEnd) { mAutoScrollResetAccumulator += deltaTime; if (mAutoScrollResetAccumulator >= static_cast(mAutoScrollResetDelayConstant)) { // Fade in the text as it resets to the start position. float maxOpacity {static_cast(mChildren.front()->getColor() & 0x000000FF) / 255.0f}; auto func = [this, maxOpacity](float t) { unsigned int color {mChildren.front()->getColor()}; float opacity {glm::mix(0.0f, maxOpacity, t)}; color = (color & 0xFFFFFF00) + static_cast(opacity * 255.0f); this->mChildren.front()->setColor(color); mScrollPos = glm::vec2 {}; mAutoScrollResetAccumulator = 0; mAutoScrollAccumulator = -mAutoScrollDelay + mAutoScrollSpeed; mAtEnd = false; }; this->setAnimation(new LambdaAnimation(func, 300), 0, nullptr, false); } } GuiComponent::update(deltaTime); } void ScrollableContainer::render(const glm::mat4& parentTrans) { if (!isVisible() || mThemeOpacity == 0.0f || mChildren.front()->getValue() == "") return; glm::mat4 trans {parentTrans * getTransform()}; glm::ivec2 clipPos {static_cast(trans[3].x), static_cast(trans[3].y)}; glm::ivec2 clipDim {mSize.x, mAdjustedHeight}; // By effectively clipping the upper and lower boundaries of the container we mostly avoid // scrolling outside the vertical starting and ending positions. clipPos.y += static_cast(mClipSpacing); clipDim.y -= static_cast(mClipSpacing); mRenderer->pushClipRect(clipPos, clipDim); trans = glm::translate(trans, -glm::vec3 {mScrollPos.x, mScrollPos.y, 0.0f}); mRenderer->setMatrix(trans); if (Settings::getInstance()->getBool("DebugText")) mRenderer->drawRect(0.0f, mScrollPos.y, mSize.x, mAdjustedHeight, 0x0000FF33, 0x0000FF33); GuiComponent::renderChildren(trans); mRenderer->popClipRect(); }