// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // 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(Window* window) : GuiComponent{window} , mScrollPos{0.0f, 0.0f} , mScrollDir{0.0f, 0.0f} , mClipSpacing{0.0f} , mAutoScrollDelay{0} , mAutoScrollSpeed{0} , mAutoScrollAccumulator{0} , mAutoScrollResetAccumulator{0} , mAdjustedAutoScrollSpeed{0} , mUpdatedSize{false} { // Set the modifier to get equivalent scrolling speed regardless of screen resolution. mResolutionModifier = Renderer::getScreenHeightModifier(); 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); reset(); } 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::reset() { mScrollPos = glm::vec2{}; mAutoScrollResetAccumulator = 0; mAutoScrollAccumulator = -mAutoScrollDelay + mAutoScrollSpeed; mAtEnd = false; } void ScrollableContainer::update(int deltaTime) { // 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{} && !mWindow->isLaunchScreenDisplayed()) reset(); return; } const glm::vec2 contentSize{mChildren.front()->getSize()}; float rowModifier{1.0f}; float lineSpacing{mChildren.front()->getLineSpacing()}; float combinedHeight{mChildren.front()->getFont()->getHeight(lineSpacing)}; // 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 && contentSize.y > mSize.y) { float numLines{mSize.y / combinedHeight}; mSize.y = floorf(numLines) * combinedHeight; mUpdatedSize = true; } else if (mUpdatedSize) { // If there are less than 8 lines of text, accelerate the scrolling further. float lines{mSize.y / combinedHeight}; if (lines < 8.0f) rowModifier = lines / 8.0f; } 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 (mAdjustedAutoScrollSpeed < 0) mAdjustedAutoScrollSpeed = 1; if (mAdjustedAutoScrollSpeed != 0) { mAutoScrollAccumulator += deltaTime; while (mAutoScrollAccumulator >= static_cast(rowModifier * static_cast(mAdjustedAutoScrollSpeed))) { if (contentSize.y > mSize.y) 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 + getSize().x > contentSize.x) { mScrollPos.x = contentSize.x - getSize().x; mAtEnd = true; } if (contentSize.y < getSize().y) { mScrollPos.y = 0.0f; } else if (mScrollPos.y + getSize().y > contentSize.y) { mScrollPos.y = contentSize.y - getSize().y; mAtEnd = true; } if (mAtEnd) { mAutoScrollResetAccumulator += deltaTime; if (mAutoScrollResetAccumulator >= static_cast(mAutoScrollResetDelayConstant)) { // Fade in the text as it resets to the start position. auto func = [this](float t) { this->setOpacity(static_cast(glm::mix(0.0f, 1.0f, t) * 255)); 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()) return; glm::mat4 trans{parentTrans * getTransform()}; glm::ivec2 clipPos{static_cast(trans[3].x), static_cast(trans[3].y)}; glm::vec3 dimScaled{}; dimScaled.x = std::fabs(trans[3].x + mSize.x); dimScaled.y = std::fabs(trans[3].y + mSize.y); glm::ivec2 clipDim{static_cast(ceilf(dimScaled.x - trans[3].x)), static_cast(ceilf(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. clipPos.y += static_cast(mClipSpacing); clipDim.y -= static_cast(mClipSpacing); Renderer::pushClipRect(clipPos, clipDim); trans = glm::translate(trans, -glm::vec3{mScrollPos.x, mScrollPos.y, 0.0f}); Renderer::setMatrix(trans); GuiComponent::renderChildren(trans); Renderer::popClipRect(); }