// SPDX-License-Identifier: MIT // // ES-DE Frontend // CarouselComponent.h // // Carousel, usable in both the system and gamelist views. // #ifndef ES_CORE_COMPONENTS_PRIMARY_CAROUSEL_COMPONENT_H #define ES_CORE_COMPONENTS_PRIMARY_CAROUSEL_COMPONENT_H #include "Sound.h" #include "animations/LambdaAnimation.h" #include "components/IList.h" #include "components/primary/PrimaryComponent.h" #include "resources/Font.h" struct CarouselEntry { std::shared_ptr item; std::string imagePath; std::string defaultImagePath; }; template class CarouselComponent : public PrimaryComponent, protected IList { protected: using List = IList; using List::mCursor; using List::mEntries; using List::mLastCursor; using List::mScrollVelocity; using List::mSize; using List::mWindow; using GuiComponent::mDefaultZIndex; using GuiComponent::mZIndex; public: using Entry = typename IList::Entry; enum class CarouselType { HORIZONTAL, VERTICAL, VERTICAL_WHEEL, HORIZONTAL_WHEEL, NO_CAROUSEL }; enum class ItemStacking { CENTERED, ASCENDING, ASCENDING_RAISED, DESCENDING, DESCENDING_RAISED }; CarouselComponent(); void addEntry(Entry& entry, const std::shared_ptr& theme); void updateEntry(Entry& entry, const std::shared_ptr& theme); Entry& getEntry(int index) { return mEntries.at(index); } void onDemandTextureLoad() override; const CarouselType getType() { return mType; } const std::string& getDefaultCarouselImage() const { return mDefaultImagePath; } const std::string& getDefaultCarouselFolderImage() const { return mDefaultFolderImagePath; } void setDefaultImage(std::string defaultImage) { mDefaultImagePath = defaultImage; } void setDefaultFolderImage(std::string defaultImage) { mDefaultFolderImagePath = defaultImage; } bool isScrolling() const override { return List::isScrolling(); } const LetterCase getLetterCase() const override { return mLetterCase; } const LetterCase getLetterCaseAutoCollections() const override { return mLetterCaseAutoCollections; } const LetterCase getLetterCaseCustomCollections() const override { return mLetterCaseCustomCollections; } const bool getSystemNameSuffix() const override { return mSystemNameSuffix; } const LetterCase getLetterCaseSystemNameSuffix() const override { return mLetterCaseSystemNameSuffix; } void setCancelTransitionsCallback(const std::function& func) override { mCancelTransitionsCallback = func; } void setCursorChangedCallback(const std::function& func) override { mCursorChangedCallback = func; } bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; void render(const glm::mat4& parentTrans) override; void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; private: void onShowPrimary() override { mEntries.at(mCursor).data.item->resetComponent(); } void onCursorChanged(const CursorState& state) override; void onScroll() override { if (mGamelistView) NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); else NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND); } void stopScrolling() override { List::stopScrolling(); // Only finish the animation if we're in the gamelist view. if (mGamelistView) GuiComponent::finishAnimation(0); } const int getScrollingVelocity() override { return List::getScrollingVelocity(); } void clear() override { List::clear(); } const T& getSelected() const override { return List::getSelected(); } const T& getNext() const override { return List::getNext(); } const T& getPrevious() const override { return List::getPrevious(); } const T& getFirst() const override { return List::getFirst(); } const T& getLast() const override { return List::getLast(); } bool setCursor(const T& obj) override { return List::setCursor(obj); } bool remove(const T& obj) override { return List::remove(obj); } int size() const override { return List::size(); } int getCursor() override { return mCursor; } const size_t getNumEntries() override { return mEntries.size(); } const bool getFadeAbovePrimary() const override { return mFadeAbovePrimary; } enum class ImageFit { CONTAIN, FILL, COVER }; Renderer* mRenderer; std::function mCursorChangedCallback; std::function mCancelTransitionsCallback; float mEntryCamOffset; float mEntryCamTarget; int mPreviousScrollVelocity; bool mPositiveDirection; bool mTriggerJump; bool mGamelistView; CarouselType mType; std::vector mImageTypes; std::string mDefaultImagePath; std::string mDefaultFolderImagePath; std::shared_ptr mDefaultImage; float mMaxItemCount; int mItemsBeforeCenter; int mItemsAfterCenter; ItemStacking mItemStacking; glm::vec2 mSelectedItemMargins; glm::vec2 mSelectedItemOffset; glm::vec2 mItemSize; float mItemScale; float mItemRotation; glm::vec2 mItemRotationOrigin; bool mItemAxisHorizontal; float mItemAxisRotation; bool mLinearInterpolation; float mImageCornerRadius; unsigned int mImageColorShift; unsigned int mImageColorShiftEnd; bool mImageColorGradientHorizontal; unsigned int mImageSelectedColor; unsigned int mImageSelectedColorEnd; bool mImageSelectedColorGradientHorizontal; bool mHasImageSelectedColor; float mImageBrightness; float mImageSaturation; float mItemDiagonalOffset; bool mInstantItemTransitions; Alignment mItemHorizontalAlignment; Alignment mItemVerticalAlignment; Alignment mWheelHorizontalAlignment; Alignment mWheelVerticalAlignment; float mHorizontalOffset; float mVerticalOffset; bool mReflections; float mReflectionsOpacity; float mReflectionsFalloff; float mUnfocusedItemOpacity; float mUnfocusedItemSaturation; bool mHasUnfocusedItemSaturation; float mUnfocusedItemDimming; ImageFit mImagefit; glm::vec2 mImageCropPos; unsigned int mCarouselColor; unsigned int mCarouselColorEnd; bool mColorGradientHorizontal; float mTextRelativeScale; unsigned int mTextColor; unsigned int mTextBackgroundColor; unsigned int mTextSelectedColor; unsigned int mTextSelectedBackgroundColor; bool mHasTextSelectedColor; bool mTextHorizontalScrolling; float mTextHorizontalScrollSpeed; float mTextHorizontalScrollDelay; float mTextHorizontalScrollGap; std::shared_ptr mFont; LetterCase mLetterCase; LetterCase mLetterCaseAutoCollections; LetterCase mLetterCaseCustomCollections; float mLineSpacing; bool mSystemNameSuffix; LetterCase mLetterCaseSystemNameSuffix; bool mFadeAbovePrimary; }; template CarouselComponent::CarouselComponent() : IList {IList::LIST_SCROLL_STYLE_SLOW, (std::is_same_v ? ListLoopType::LIST_ALWAYS_LOOP : ListLoopType::LIST_PAUSE_AT_END_ON_JUMP)} , mRenderer {Renderer::getInstance()} , mEntryCamOffset {0.0f} , mEntryCamTarget {0.0f} , mPreviousScrollVelocity {0} , mPositiveDirection {false} , mTriggerJump {false} , mGamelistView {std::is_same_v ? true : false} , mType {CarouselType::HORIZONTAL} , mMaxItemCount {3.0f} , mItemsBeforeCenter {8} , mItemsAfterCenter {8} , mItemStacking {ItemStacking::CENTERED} , mSelectedItemMargins {0.0f, 0.0f} , mSelectedItemOffset {0.0f, 0.0f} , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.25f, Renderer::getScreenHeight() * 0.155f}} , mItemScale {1.2f} , mItemRotation {7.5f} , mItemRotationOrigin {-3.0f, 0.5f} , mItemAxisHorizontal {false} , mItemAxisRotation {0.0f} , mLinearInterpolation {false} , mImageCornerRadius {0.0f} , mImageColorShift {0xFFFFFFFF} , mImageColorShiftEnd {0xFFFFFFFF} , mImageColorGradientHorizontal {true} , mImageSelectedColor {0xFFFFFFFF} , mImageSelectedColorEnd {0xFFFFFFFF} , mImageSelectedColorGradientHorizontal {true} , mHasImageSelectedColor {false} , mImageBrightness {0.0f} , mImageSaturation {1.0f} , mItemDiagonalOffset {0.0f} , mInstantItemTransitions {false} , mItemHorizontalAlignment {ALIGN_CENTER} , mItemVerticalAlignment {ALIGN_CENTER} , mWheelHorizontalAlignment {ALIGN_CENTER} , mWheelVerticalAlignment {ALIGN_CENTER} , mHorizontalOffset {0.0f} , mVerticalOffset {0.0f} , mReflections {false} , mReflectionsOpacity {0.5f} , mReflectionsFalloff {1.0f} , mUnfocusedItemOpacity {0.5f} , mUnfocusedItemSaturation {1.0f} , mHasUnfocusedItemSaturation {false} , mUnfocusedItemDimming {1.0f} , mImagefit {ImageFit::CONTAIN} , mImageCropPos {0.5f, 0.5f} , mCarouselColor {0} , mCarouselColorEnd {0} , mColorGradientHorizontal {true} , mTextRelativeScale {1.0f} , mTextColor {0x000000FF} , mTextBackgroundColor {0xFFFFFF00} , mTextSelectedColor {0x000000FF} , mTextSelectedBackgroundColor {0xFFFFFF00} , mHasTextSelectedColor {false} , mTextHorizontalScrolling {false} , mTextHorizontalScrollSpeed {1.0f} , mTextHorizontalScrollDelay {3000.0f} , mTextHorizontalScrollGap {1.5f} , mFont {Font::get(FONT_SIZE_LARGE_FIXED)} , mLetterCase {LetterCase::NONE} , mLetterCaseAutoCollections {LetterCase::UNDEFINED} , mLetterCaseCustomCollections {LetterCase::UNDEFINED} , mLineSpacing {1.5f} , mSystemNameSuffix {true} , mLetterCaseSystemNameSuffix {LetterCase::UPPERCASE} , mFadeAbovePrimary {false} { } template void CarouselComponent::addEntry(Entry& entry, const std::shared_ptr& theme) { bool dynamic {true}; if (!mGamelistView) dynamic = false; if (entry.data.imagePath != "" && ResourceManager::getInstance().fileExists(entry.data.imagePath)) { auto item = std::make_shared(false, dynamic); item->setLinearInterpolation(mLinearInterpolation); item->setMipmapping(true); if (mImagefit == ImageFit::CONTAIN) { item->setMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } else if (mImagefit == ImageFit::FILL) { item->setResize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } else if (mImagefit == ImageFit::COVER) { item->setCropPos(mImageCropPos); item->setCroppedSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } item->setCornerRadius(mImageCornerRadius); item->setImage(entry.data.imagePath); if (mImageBrightness != 0.0) item->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) item->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) item->setColorShift(mImageColorShift); if (mImageColorShiftEnd != mImageColorShift) item->setColorShiftEnd(mImageColorShiftEnd); if (!mImageColorGradientHorizontal) item->setColorGradientHorizontal(false); item->setRotateByTargetSize(true); entry.data.item = item; } else if (entry.data.defaultImagePath != "" && ResourceManager::getInstance().fileExists(entry.data.defaultImagePath)) { if (mDefaultImage.get() == nullptr || !mGamelistView) { mDefaultImage = std::make_shared(false, dynamic); mDefaultImage->setLinearInterpolation(mLinearInterpolation); mDefaultImage->setMipmapping(true); if (mImagefit == ImageFit::CONTAIN) { mDefaultImage->setMaxSize( glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } else if (mImagefit == ImageFit::FILL) { mDefaultImage->setResize( glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } else if (mImagefit == ImageFit::COVER) { mDefaultImage->setCropPos(mImageCropPos); mDefaultImage->setCroppedSize( glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } mDefaultImage->setCornerRadius(mImageCornerRadius); mDefaultImage->setImage(entry.data.defaultImagePath); if (mImageBrightness != 0.0) mDefaultImage->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) mDefaultImage->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) mDefaultImage->setColorShift(mImageColorShift); if (mImageColorShiftEnd != mImageColorShift) mDefaultImage->setColorShiftEnd(mImageColorShiftEnd); if (!mImageColorGradientHorizontal) mDefaultImage->setColorGradientHorizontal(false); mDefaultImage->setRotateByTargetSize(true); } // For the gamelist view the default image is applied in onDemandTextureLoad(). if (!mGamelistView) entry.data.item = mDefaultImage; } else if (!mGamelistView) { entry.data.imagePath = ""; } if (!entry.data.item) { // Always add the item text as fallback in case there is no image. This is also displayed // when quick-jumping as textures are not loaded in this case. auto text = std::make_shared( entry.name, mFont, 0x000000FF, mItemHorizontalAlignment, mItemVerticalAlignment, glm::ivec2 {0, 0}, glm::vec3 {0.0f, 0.0f, 0.0f}, glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f)), 0x00000000, mLineSpacing, mTextRelativeScale, mTextHorizontalScrolling, mTextHorizontalScrollSpeed, mTextHorizontalScrollDelay, mTextHorizontalScrollGap); if (!mGamelistView) text->setValue(entry.name); text->setColor(mTextColor); text->setBackgroundColor(mTextBackgroundColor); text->setRenderBackground(true); entry.data.item = text; } // Set origin for the items based on their alignment so they line up properly. if (mItemHorizontalAlignment == ALIGN_LEFT) entry.data.item->setOrigin(0.0f, 0.5f); else if (mItemHorizontalAlignment == ALIGN_RIGHT) entry.data.item->setOrigin(1.0f, 0.5f); else entry.data.item->setOrigin(0.5f, 0.5f); if (mItemVerticalAlignment == ALIGN_TOP) entry.data.item->setOrigin(entry.data.item->getOrigin().x, 0.0f); else if (mItemVerticalAlignment == ALIGN_BOTTOM) entry.data.item->setOrigin(entry.data.item->getOrigin().x, 1.0f); else entry.data.item->setOrigin(entry.data.item->getOrigin().x, 0.5f); glm::vec2 denormalized {glm::round(mItemSize * entry.data.item->getOrigin())}; entry.data.item->setPosition(glm::vec3 {denormalized.x, denormalized.y, 0.0f}); List::add(entry); } template void CarouselComponent::updateEntry(Entry& entry, const std::shared_ptr& theme) { if (entry.data.imagePath != "") { auto item = std::make_shared(false, true); item->setLinearInterpolation(mLinearInterpolation); item->setMipmapping(true); if (mImagefit == ImageFit::CONTAIN) { item->setMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } else if (mImagefit == ImageFit::FILL) { item->setResize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } else if (mImagefit == ImageFit::COVER) { item->setCropPos(mImageCropPos); item->setCroppedSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); } item->setCornerRadius(mImageCornerRadius); item->setImage(entry.data.imagePath); if (mImageBrightness != 0.0) item->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) item->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) item->setColorShift(mImageColorShift); if (mImageColorShiftEnd != mImageColorShift) item->setColorShiftEnd(mImageColorShiftEnd); if (!mImageColorGradientHorizontal) item->setColorGradientHorizontal(false); item->setRotateByTargetSize(true); entry.data.item = item; } else { return; } // Set origin for the items based on their alignment so they line up properly. if (mItemHorizontalAlignment == ALIGN_LEFT) entry.data.item->setOrigin(0.0f, 0.5f); else if (mItemHorizontalAlignment == ALIGN_RIGHT) entry.data.item->setOrigin(1.0f, 0.5f); else entry.data.item->setOrigin(0.5f, 0.5f); if (mItemVerticalAlignment == ALIGN_TOP) entry.data.item->setOrigin(entry.data.item->getOrigin().x, 0.0f); else if (mItemVerticalAlignment == ALIGN_BOTTOM) entry.data.item->setOrigin(entry.data.item->getOrigin().x, 1.0f); else entry.data.item->setOrigin(entry.data.item->getOrigin().x, 0.5f); glm::vec2 denormalized {glm::round(mItemSize * entry.data.item->getOrigin())}; entry.data.item->setPosition(glm::vec3 {denormalized.x, denormalized.y, 0.0f}); } template void CarouselComponent::onDemandTextureLoad() { if constexpr (std::is_same_v) { if (size() == 0) return; if (mImageTypes.empty()) mImageTypes.emplace_back("marquee"); const int numEntries {size()}; const int center {getCursor()}; const bool isWheel {mType == CarouselType::VERTICAL_WHEEL || mType == CarouselType::HORIZONTAL_WHEEL}; int centerOffset {0}; int itemInclusion {0}; int itemInclusionBefore {0}; int itemInclusionAfter {0}; if (isWheel) { itemInclusion = 1; itemInclusionBefore = mItemsBeforeCenter - 1; itemInclusionAfter = mItemsAfterCenter; } else { itemInclusion = static_cast(std::ceil((mMaxItemCount + 1) / 2.0f)); itemInclusionBefore = -1; // If the carousel is offset we need to load additional textures to fully populate // the visible entries. if (mType == CarouselType::HORIZONTAL && mHorizontalOffset != 0.0f) { const float itemSpacing { ((mSize.x - (mItemSize.x * mMaxItemCount)) / mMaxItemCount) + mItemSize.x}; centerOffset = static_cast(std::ceil(mSize.x * std::fabs(mHorizontalOffset) / std::min(mItemSize.x, itemSpacing))); if (mHorizontalOffset < 0.0f) itemInclusionAfter += centerOffset; else itemInclusionBefore += centerOffset; if (mHorizontalOffset > 0.0f) centerOffset = -centerOffset; } else if (mType == CarouselType::VERTICAL && mVerticalOffset != 0.0f) { const float itemSpacing { ((mSize.y - (mItemSize.y * mMaxItemCount)) / mMaxItemCount) + mItemSize.y}; centerOffset = static_cast(std::ceil(mSize.y * std::fabs(mVerticalOffset) / std::min(mItemSize.y, itemSpacing))); if (mVerticalOffset < 0.0f) itemInclusionAfter += centerOffset; else itemInclusionBefore += centerOffset; if (mVerticalOffset > 0.0f) centerOffset = -centerOffset; } itemInclusion += 1; } for (int i {center - itemInclusion - itemInclusionBefore}; i < center + itemInclusion + itemInclusionAfter; ++i) { int cursor {i}; while (cursor < 0) cursor += numEntries; while (cursor >= numEntries) cursor -= numEntries; auto& entry = mEntries.at(cursor); if (entry.data.imagePath == "") { FileData* game {entry.object}; for (auto& imageType : mImageTypes) { if (imageType == "marquee") entry.data.imagePath = game->getMarqueePath(); else if (imageType == "cover") entry.data.imagePath = game->getCoverPath(); else if (imageType == "backcover") entry.data.imagePath = game->getBackCoverPath(); else if (imageType == "3dbox") entry.data.imagePath = game->get3DBoxPath(); else if (imageType == "physicalmedia") entry.data.imagePath = game->getPhysicalMediaPath(); else if (imageType == "screenshot") entry.data.imagePath = game->getScreenshotPath(); else if (imageType == "titlescreen") entry.data.imagePath = game->getTitleScreenPath(); else if (imageType == "miximage") entry.data.imagePath = game->getMiximagePath(); else if (imageType == "fanart") entry.data.imagePath = game->getFanArtPath(); else if (imageType == "none") // Display the game name as text. break; if (entry.data.imagePath != "") break; } if (entry.data.imagePath == "") entry.data.imagePath = entry.data.defaultImagePath; auto theme = game->getSystem()->getTheme(); updateEntry(entry, theme); } } } } template bool CarouselComponent::input(InputConfig* config, Input input) { if (input.value != 0) { switch (mType) { case CarouselType::VERTICAL: case CarouselType::VERTICAL_WHEEL: if (config->isMappedLike("up", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); List::listInput(-1); return true; } if (config->isMappedLike("down", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); List::listInput(1); return true; } break; case CarouselType::HORIZONTAL: case CarouselType::HORIZONTAL_WHEEL: default: if (config->isMappedLike("left", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); List::listInput(-1); return true; } if (config->isMappedLike("right", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); List::listInput(1); return true; } break; } if (mGamelistView) { if (config->isMappedLike("leftshoulder", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); if (mEntries.size() < 10 && getCursor() != 0) { mTriggerJump = true; return this->listFirstRow(); } else { List::listInput(-10); return true; } } if (config->isMappedLike("rightshoulder", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); if (mEntries.size() < 10 && getCursor() != static_cast(mEntries.size()) - 1) { mTriggerJump = true; return this->listLastRow(); } else { List::listInput(10); return true; } } if (config->isMappedLike("lefttrigger", input)) { if (getCursor() == 0) return true; mTriggerJump = true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); return this->listFirstRow(); } if (config->isMappedLike("righttrigger", input)) { if (getCursor() == static_cast(mEntries.size()) - 1) return true; mTriggerJump = true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); return this->listLastRow(); } } } else { if (mGamelistView) { if (config->isMappedLike("up", input) || config->isMappedLike("down", input) || config->isMappedLike("left", input) || config->isMappedLike("right", input) || config->isMappedLike("leftshoulder", input) || config->isMappedLike("rightshoulder", input) || config->isMappedLike("lefttrigger", input) || config->isMappedLike("righttrigger", input)) { if (isScrolling()) onCursorChanged(CursorState::CURSOR_STOPPED); List::listInput(0); mTriggerJump = false; } } else { if (config->isMappedLike("up", input) || config->isMappedLike("down", input) || config->isMappedLike("left", input) || config->isMappedLike("right", input)) { if (isScrolling()) onCursorChanged(CursorState::CURSOR_STOPPED); List::listInput(0); } } } return GuiComponent::input(config, input); } template void CarouselComponent::update(int deltaTime) { mEntries.at(mCursor).data.item->update(deltaTime); List::listUpdate(deltaTime); GuiComponent::update(deltaTime); } template void CarouselComponent::render(const glm::mat4& parentTrans) { const float camOffset {mInstantItemTransitions ? mEntryCamTarget : mEntryCamOffset}; int numEntries {static_cast(mEntries.size())}; if (numEntries == 0) return; glm::mat4 carouselTrans {parentTrans}; carouselTrans = glm::translate( carouselTrans, glm::vec3 {GuiComponent::mPosition.x, GuiComponent::mPosition.y, 0.0f}); carouselTrans = glm::translate(carouselTrans, glm::vec3 {GuiComponent::mOrigin.x * mSize.x * -1.0f, GuiComponent::mOrigin.y * mSize.y * -1.0f, 0.0f}); if (carouselTrans[3].x + mSize.x <= 0.0f || carouselTrans[3].y + mSize.y <= 0.0f) return; const float sizeX {carouselTrans[3].x < 0.0f ? mSize.x + carouselTrans[3].x : mSize.x}; const float sizeY {carouselTrans[3].y < 0.0f ? mSize.y + carouselTrans[3].y : mSize.y}; glm::ivec2 clipPos {static_cast(glm::clamp(std::round(carouselTrans[3].x), 0.0f, mRenderer->getScreenWidth())), static_cast(glm::clamp(std::round(carouselTrans[3].y), 0.0f, mRenderer->getScreenHeight()))}; glm::ivec2 clipDim { static_cast(std::min(std::round(sizeX), mRenderer->getScreenWidth())), static_cast(std::min(std::round(sizeY), mRenderer->getScreenHeight()))}; mRenderer->pushClipRect(clipPos, clipDim); mRenderer->setMatrix(carouselTrans); // In image debug mode, draw a green rectangle covering the entire carousel area. if (Settings::getInstance()->getBool("DebugImage")) mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x00FF0033, 0x00FF0033); // Background box behind the items. mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, mCarouselColor, mCarouselColorEnd, mColorGradientHorizontal); const bool isWheel {mType == CarouselType::VERTICAL_WHEEL || mType == CarouselType::HORIZONTAL_WHEEL}; // Draw the items. // itemSpacing will also include the size of the item itself. glm::vec2 itemSpacing {0.0f, 0.0f}; float xOff {0.0f}; float yOff {0.0f}; float scaleSize {0.0f}; if (mType == CarouselType::HORIZONTAL_WHEEL) scaleSize = mItemSize.y * mItemScale - mItemSize.y; else scaleSize = mItemSize.x * mItemScale - mItemSize.x; if (isWheel) { // Alignment of the actual carousel inside to the overall component area. if (mType == CarouselType::HORIZONTAL_WHEEL) { xOff = (mSize.x / 2.0f) - (mItemSize.y / 2.0f); if (mWheelVerticalAlignment == ALIGN_CENTER) { yOff = (mSize.y / 2.0f) + (mItemSize.x / 2.0f); if (mItemVerticalAlignment == ALIGN_TOP) yOff -= scaleSize / 2.0f; else if (mItemVerticalAlignment == ALIGN_BOTTOM) yOff += scaleSize / 2.0f; } else if (mWheelVerticalAlignment == ALIGN_TOP) { yOff = mItemSize.x - ((mItemSize.x - mItemSize.y) / 2.0f); if (mItemVerticalAlignment == ALIGN_CENTER) yOff += scaleSize / 2.0f; else if (mItemVerticalAlignment == ALIGN_BOTTOM) yOff += scaleSize; } else if (mWheelVerticalAlignment == ALIGN_BOTTOM) { yOff = mSize.y + ((mItemSize.x - mItemSize.y) / 2.0f); if (mItemVerticalAlignment == ALIGN_CENTER) yOff -= scaleSize / 2.0f; else if (mItemVerticalAlignment == ALIGN_TOP) yOff -= scaleSize / 1.0f; } } else { xOff = (mSize.x - mItemSize.x) / 2.0f; yOff = (mSize.y - mItemSize.y) / 2.0f; if (mWheelHorizontalAlignment == ALIGN_RIGHT) { xOff += mSize.x / 2.0f; if (mItemHorizontalAlignment == ALIGN_LEFT) xOff -= mItemSize.x / 2.0f + scaleSize; else if (mItemHorizontalAlignment == ALIGN_RIGHT) xOff -= mItemSize.x / 2.0f; else xOff -= mItemSize.x / 2.0f + scaleSize / 2.0f; } else if (mWheelHorizontalAlignment == ALIGN_LEFT) { xOff -= (mSize.x / 2.0f); if (mItemHorizontalAlignment == ALIGN_LEFT) xOff += mItemSize.x / 2.0f; else if (mItemHorizontalAlignment == ALIGN_RIGHT) xOff += mItemSize.x / 2.0f + scaleSize; else xOff += mItemSize.x / 2.0f + scaleSize / 2.0f; } else if (mWheelHorizontalAlignment == ALIGN_CENTER && mItemHorizontalAlignment != ALIGN_CENTER) { if (mItemHorizontalAlignment == ALIGN_RIGHT) xOff += scaleSize / 2.0f; else if (mItemHorizontalAlignment == ALIGN_LEFT) xOff -= scaleSize / 2.0f; } } } else if (mType == CarouselType::VERTICAL) { itemSpacing.y = ((mSize.y - (mItemSize.y * mMaxItemCount)) / mMaxItemCount) + mItemSize.y; yOff = (mSize.y - mItemSize.y) / 2.0f - (camOffset * itemSpacing.y); if (mItemHorizontalAlignment == ALIGN_LEFT) xOff = 0.0f; else if (mItemHorizontalAlignment == ALIGN_RIGHT) xOff = mSize.x - mItemSize.x; else xOff = (mSize.x - mItemSize.x) / 2.0f; } else { // HORIZONTAL. itemSpacing.x = ((mSize.x - (mItemSize.x * mMaxItemCount)) / mMaxItemCount) + mItemSize.x; xOff = (mSize.x - mItemSize.x) / 2.0f - (camOffset * itemSpacing.x); if (mItemVerticalAlignment == ALIGN_TOP) yOff = 0.0f; else if (mItemVerticalAlignment == ALIGN_BOTTOM) yOff = mSize.y - mItemSize.y - (mReflections ? ((mItemSize.y * mItemScale)) : 0.0f); else yOff = (mSize.y - (mItemSize.y * (mReflections ? 2.0f : 1.0f))) / 2.0f; } xOff += mSize.x * mHorizontalOffset; yOff += mSize.y * mVerticalOffset; int center {0}; int centerOffset {0}; // Needed to make sure that overlapping items are renderered correctly. if (mPositiveDirection) center = static_cast(std::floor(camOffset)); else center = static_cast(std::ceil(camOffset)); int itemInclusion {0}; int itemInclusionBefore {0}; int itemInclusionAfter {0}; if (mType == CarouselType::HORIZONTAL || mType == CarouselType::VERTICAL) { itemInclusion = static_cast(std::ceil(mMaxItemCount / 2.0f)) + 1; itemInclusionAfter = 2; // If the carousel is offset we need to calculate the center offset so the items are // rendered in the correct order (as seen when overlapping). if (mType == CarouselType::HORIZONTAL && mHorizontalOffset != 0.0f) { centerOffset = static_cast(std::ceil(mSize.x * std::fabs(mHorizontalOffset) / std::min(mItemSize.x, itemSpacing.x))); if (mHorizontalOffset < 0.0f) itemInclusionAfter += centerOffset; else itemInclusionBefore += centerOffset; if (mHorizontalOffset > 0.0f) centerOffset = -centerOffset; } else if (mType == CarouselType::VERTICAL && mVerticalOffset != 0.0f) { centerOffset = static_cast(std::ceil(mSize.y * std::fabs(mVerticalOffset) / std::min(mItemSize.y, itemSpacing.y))); if (mVerticalOffset < 0.0f) itemInclusionAfter += centerOffset; else itemInclusionBefore += centerOffset; if (mVerticalOffset > 0.0f) centerOffset = -centerOffset; } } else { // For the wheel types. itemInclusion = 1; itemInclusionBefore = mItemsBeforeCenter - 1; itemInclusionAfter = mItemsAfterCenter; } const bool singleEntry {numEntries == 1}; struct renderStruct { int index; float distance; float scale; float opacity; float saturation; float dimming; glm::mat4 trans; }; std::vector renderItems; std::vector renderItemsSorted; for (int i {center - itemInclusion - itemInclusionBefore}; i < center + itemInclusion + itemInclusionAfter; ++i) { int index {i}; while (index < 0) index += numEntries; while (index >= numEntries) index -= numEntries; float distance {i - camOffset}; if (singleEntry) distance = 0.0f; float scale {0.0f}; if (mItemScale >= 1.0f) { scale = 1.0f + ((mItemScale - 1.0f) * (1.0f - fabsf(distance))); scale = std::min(mItemScale, std::max(1.0f, scale)); scale /= mItemScale; } else { scale = 1.0f + ((1.0f - mItemScale) * (fabsf(distance) - 1.0f)); scale = std::max(mItemScale, std::min(1.0f, scale)); } glm::vec2 selectedItemMargins {0.0f, 0.0f}; if (mSelectedItemMargins != glm::vec2 {0.0f, 0.0f}) { if (i < camOffset) { if (mType == CarouselType::HORIZONTAL) selectedItemMargins.x = -mSelectedItemMargins.x; else selectedItemMargins.y = -mSelectedItemMargins.x; } else if (i > camOffset) { if (mType == CarouselType::HORIZONTAL) selectedItemMargins.x = mSelectedItemMargins.y; else selectedItemMargins.y = mSelectedItemMargins.y; } if (std::fabs(distance) < 1.0f) selectedItemMargins *= std::fabs(distance); } float itemHorizontalOffset {0.0f}; float itemVerticallOffset {0.0f}; if ((mSelectedItemOffset.x != 0.0f || mSelectedItemOffset.y != 0.0f) && std::fabs(distance) < 1.0f) { itemHorizontalOffset = (1.0f - std::fabs(distance)) * mSelectedItemOffset.x; itemVerticallOffset = (1.0f - std::fabs(distance)) * mSelectedItemOffset.y; } glm::mat4 itemTrans {carouselTrans}; if (singleEntry) itemTrans = glm::translate(carouselTrans, glm::vec3 {xOff, yOff, 0.0f}); else itemTrans = glm::translate( itemTrans, glm::vec3 {(i * itemSpacing.x) + xOff + itemHorizontalOffset + selectedItemMargins.x, (i * itemSpacing.y) + yOff + itemVerticallOffset + selectedItemMargins.y, 0.0f}); if (mType == CarouselType::HORIZONTAL_WHEEL) itemTrans = glm::rotate(itemTrans, glm::radians(-90.0f), glm::vec3 {0.0f, 0.0f, 1.0f}); float opacity {0.0f}; float saturation {0.0f}; float dimming {0.0f}; if (distance == 0.0f || mUnfocusedItemOpacity == 1.0f) { opacity = 1.0f; } else if (fabsf(distance) >= 1.0f) { opacity = mUnfocusedItemOpacity; } else { const float maxDiff {1.0f - mUnfocusedItemOpacity}; opacity = mUnfocusedItemOpacity + (maxDiff - (maxDiff * fabsf(distance))); } if (mHasUnfocusedItemSaturation) { if (distance == 0.0f) { saturation = mImageSaturation; } else if (fabsf(distance) >= 1.0f) { saturation = mUnfocusedItemSaturation; } else { const float maxDiff {mImageSaturation - mUnfocusedItemSaturation}; saturation = mUnfocusedItemSaturation + (maxDiff - (maxDiff * fabsf(distance))); } } if (distance == 0.0f || mUnfocusedItemDimming == 1.0f) { dimming = 1.0f; } else if (fabsf(distance) >= 1.0f) { dimming = mUnfocusedItemDimming; } else { const float maxDiff {1.0f - mUnfocusedItemDimming}; dimming = mUnfocusedItemDimming + (maxDiff - (maxDiff * fabsf(distance))); } renderStruct renderItem; renderItem.index = index; renderItem.distance = distance; renderItem.scale = scale; renderItem.opacity = opacity; renderItem.saturation = saturation; renderItem.dimming = dimming; renderItem.trans = itemTrans; renderItems.emplace_back(renderItem); if (singleEntry) break; } int belowCenter {static_cast(std::round((renderItems.size() - centerOffset - 1) / 2))}; if (renderItems.size() == 1) { renderItemsSorted.emplace_back(renderItems.front()); } else if (!isWheel && mItemStacking != ItemStacking::CENTERED) { if (mItemStacking == ItemStacking::ASCENDING) { renderItemsSorted.insert(renderItemsSorted.begin(), std::make_move_iterator(renderItems.begin()), std::make_move_iterator(renderItems.end())); } else if (mItemStacking == ItemStacking::ASCENDING_RAISED) { for (size_t i {0}; i < renderItems.size(); ++i) { if (i == static_cast(belowCenter)) continue; renderItemsSorted.emplace_back(std::move(renderItems[i])); } renderItemsSorted.emplace_back(std::move(renderItems[belowCenter])); } else if (mItemStacking == ItemStacking::DESCENDING) { for (size_t i {renderItems.size()}; i > 0; --i) renderItemsSorted.emplace_back(std::move(renderItems[i - 1])); } else if (mItemStacking == ItemStacking::DESCENDING_RAISED) { for (size_t i {renderItems.size()}; i > 0; --i) { if (i - 1 == static_cast(belowCenter)) continue; renderItemsSorted.emplace_back(std::move(renderItems[i - 1])); } renderItemsSorted.emplace_back(std::move(renderItems[belowCenter])); } } else { // Make sure that overlapping items are rendered in the correct order. size_t zeroDistanceEntry {0}; for (int i = 0; i < belowCenter; ++i) renderItemsSorted.emplace_back(renderItems[i]); for (int i = static_cast(renderItems.size()) - 1; i > belowCenter - 1; --i) { if (isWheel && (mPositiveDirection ? std::ceil(renderItems[i].distance) : std::floor(renderItems[i].distance)) == 0) { zeroDistanceEntry = i; continue; } renderItemsSorted.emplace_back(renderItems[i]); } if (isWheel) renderItemsSorted.emplace_back(renderItems[zeroDistanceEntry]); } for (auto& renderItem : renderItemsSorted) { const std::shared_ptr& comp {mEntries.at(renderItem.index).data.item}; if (comp == nullptr) continue; if (isWheel) { glm::mat4 positionCalc {renderItem.trans}; const float xOffTrans {-mItemRotationOrigin.x * mItemSize.x}; const float yOffTrans {mItemAxisHorizontal ? 0.0f : -mItemRotationOrigin.y * mItemSize.y}; // Transform to offset point. positionCalc = glm::translate(positionCalc, glm::vec3 {xOffTrans * -1.0f, yOffTrans * -1.0f, 0.0f}); // Apply rotation transform. positionCalc = glm::rotate(positionCalc, glm::radians(mItemRotation * renderItem.distance), glm::vec3 {0.0f, 0.0f, 1.0f}); // Transform back to original point. positionCalc = glm::translate(positionCalc, glm::vec3 {xOffTrans, yOffTrans, 0.0f}); if (mItemAxisHorizontal) { // Only keep position and discard the rotation data. renderItem.trans[3].x = positionCalc[3].x; renderItem.trans[3].y = positionCalc[3].y; if (mType == CarouselType::HORIZONTAL_WHEEL) { // For horizontal wheels we need to rotate all items 90 degrees around their // own axis. const float xOffTransRotate {-(mItemSize.x / 2.0f)}; const float yOffTransRotate {-(mItemSize.y / 2.0f)}; renderItem.trans = glm::translate(renderItem.trans, glm::vec3 {xOffTransRotate * -1.0f, yOffTransRotate * -1.0f, 0.0f}); renderItem.trans = glm::rotate(renderItem.trans, glm::radians(90.0f), glm::vec3 {0.0f, 0.0f, 1.0f}); renderItem.trans = glm::translate( renderItem.trans, glm::vec3 {xOffTransRotate, yOffTransRotate, 0.0f}); } } else if (mType == CarouselType::HORIZONTAL_WHEEL) { renderItem.trans = positionCalc; renderItem.trans = positionCalc; const float xOffTransRotate {-(mItemSize.x / 2.0f)}; const float yOffTransRotate {-(mItemSize.y / 2.0f)}; renderItem.trans = glm::translate(renderItem.trans, glm::vec3 {xOffTransRotate * -1.0f, yOffTransRotate * -1.0f, 0.0f}); renderItem.trans = glm::rotate(renderItem.trans, glm::radians(90.0f), glm::vec3 {0.0f, 0.0f, 1.0f}); renderItem.trans = glm::translate( renderItem.trans, glm::vec3 {xOffTransRotate, yOffTransRotate, 0.0f}); } else { renderItem.trans = positionCalc; renderItem.trans = positionCalc; } } else if (mItemAxisRotation != 0.0f) { // Rotate items around their own axis. const float xOffTransRotate {-(mItemSize.x / 2.0f)}; const float yOffTransRotate {-(mItemSize.y / 2.0f)}; renderItem.trans = glm::translate(renderItem.trans, glm::vec3 {xOffTransRotate * -1.0f, yOffTransRotate * -1.0f, 0.0f}); renderItem.trans = glm::rotate(renderItem.trans, glm::radians(mItemAxisRotation), glm::vec3 {0.0f, 0.0f, 1.0f}); renderItem.trans = glm::translate(renderItem.trans, glm::vec3 {xOffTransRotate, yOffTransRotate, 0.0f}); } float metadataOpacity {1.0f}; if constexpr (std::is_same_v) { // If a game is marked as hidden, lower the opacity a lot. // If a game is marked to not be counted, lower the opacity a moderate amount. if (mEntries.at(renderItem.index).object->getHidden()) metadataOpacity = 0.4f; else if (!mEntries.at(renderItem.index).object->getCountAsGame()) metadataOpacity = 0.7f; } const glm::vec3 origPos {comp->getPosition()}; if (mItemDiagonalOffset != 0.0f) { if (mType == CarouselType::HORIZONTAL) comp->setPosition( origPos.x, origPos.y - (mItemDiagonalOffset * renderItem.distance), origPos.z); else comp->setPosition(origPos.x - (mItemDiagonalOffset * renderItem.distance), origPos.y, origPos.z); } comp->setScale(renderItem.scale); comp->setOpacity(renderItem.opacity * metadataOpacity); if (mHasUnfocusedItemSaturation) comp->setSaturation(renderItem.saturation); if (mUnfocusedItemDimming != 1.0f) comp->setDimming(renderItem.dimming); if (renderItem.index == mCursor && std::abs(renderItem.distance) < 1.0f && (mHasImageSelectedColor || mHasTextSelectedColor)) { if (mHasTextSelectedColor && mEntries.at(renderItem.index).data.imagePath == "" && mEntries.at(renderItem.index).data.defaultImagePath == "") { comp->setColor(mTextSelectedColor); if (mTextSelectedBackgroundColor != mTextBackgroundColor) comp->setBackgroundColor(mTextSelectedBackgroundColor); comp->render(renderItem.trans); comp->setColor(mTextColor); if (mTextSelectedBackgroundColor != mTextBackgroundColor) comp->setBackgroundColor(mTextBackgroundColor); } else if (mHasImageSelectedColor) { comp->setColorShift(mImageSelectedColor); if (mImageSelectedColorEnd != mImageSelectedColor) comp->setColorShiftEnd(mImageSelectedColorEnd); if (mImageSelectedColorGradientHorizontal != mImageColorGradientHorizontal) comp->setColorGradientHorizontal(mImageSelectedColorGradientHorizontal); comp->render(renderItem.trans); if (mImageSelectedColorGradientHorizontal != mImageColorGradientHorizontal) comp->setColorGradientHorizontal(mImageColorGradientHorizontal); comp->setColorShift(mImageColorShift); if (mImageColorShiftEnd != mImageColorShift) comp->setColorShiftEnd(mImageColorShiftEnd); } else { comp->render(renderItem.trans); } } else { comp->render(renderItem.trans); } if (mItemDiagonalOffset != 0.0f) comp->setPosition(origPos); // TODO: Rewrite to use "real" reflections instead of this hack. // Don't attempt to add reflections for text entries. if (mReflections && (mEntries.at(renderItem.index).data.imagePath != "" || mEntries.at(renderItem.index).data.defaultImagePath != "")) { glm::mat4 reflectionTrans {glm::translate( renderItem.trans, glm::vec3 {0.0f, comp->getSize().y * renderItem.scale, 0.0f})}; float falloff {glm::clamp(mReflectionsFalloff, 0.0f, 1.0f)}; falloff = mReflectionsOpacity * (1.0f - falloff); comp->setOpacity(comp->getOpacity() * mReflectionsOpacity); if (mHasUnfocusedItemSaturation) comp->setSaturation(renderItem.saturation); if (mUnfocusedItemDimming != 1.0f) comp->setDimming(renderItem.dimming); if (mReflectionsFalloff > 0.0f) comp->setReflectionsFalloff(comp->getSize().y / mReflectionsFalloff); comp->setFlipY(true); if (renderItem.index == mCursor && mHasImageSelectedColor) { comp->setColorShift(mImageSelectedColorGradientHorizontal ? mImageSelectedColor : mImageSelectedColorEnd); if (mImageSelectedColorEnd != mImageSelectedColor) comp->setColorShiftEnd(mImageSelectedColorGradientHorizontal ? mImageSelectedColorEnd : mImageSelectedColor); if (mImageSelectedColorGradientHorizontal != mImageColorGradientHorizontal) comp->setColorGradientHorizontal(mImageSelectedColorGradientHorizontal); comp->render(reflectionTrans); if (mImageSelectedColorGradientHorizontal != mImageColorGradientHorizontal) comp->setColorGradientHorizontal(mImageColorGradientHorizontal); comp->setColorShift(mImageColorShift); if (mImageColorShiftEnd != mImageColorShift) comp->setColorShiftEnd(mImageColorShiftEnd); } else { if ((mImageColorShift != 0xFFFFFFFF || mImageColorShiftEnd != 0xFFFFFFFF) && !mImageColorGradientHorizontal) { // We need to reverse the color shift if a vertical gradient is applied. comp->setColorShift(mImageColorShiftEnd); comp->setColorShiftEnd(mImageColorShift); comp->render(reflectionTrans); comp->setColorShift(mImageColorShift); comp->setColorShiftEnd(mImageColorShiftEnd); } else { comp->render(reflectionTrans); } } comp->setFlipY(false); comp->setReflectionsFalloff(0.0f); } if (singleEntry) break; } mRenderer->popClipRect(); } template void CarouselComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { mSize.x = Renderer::getScreenWidth(); mSize.y = Renderer::getScreenHeight() * 0.23240f; GuiComponent::mPosition.x = 0.0f; GuiComponent::mPosition.y = Renderer::getScreenHeight() * 0.38378f; mCarouselColor = 0xFFFFFFD8; mCarouselColorEnd = 0xFFFFFFD8; mZIndex = mDefaultZIndex; using namespace ThemeFlags; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "carousel")}; if (!elem) return; if (elem->has("type")) { std::string type {elem->get("type")}; if (type == "horizontal") { mType = CarouselType::HORIZONTAL; } else if (type == "horizontalWheel") { mType = CarouselType::HORIZONTAL_WHEEL; } else if (type == "vertical") { mType = CarouselType::VERTICAL; } else if (type == "verticalWheel") { mType = CarouselType::VERTICAL_WHEEL; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property \"type\" " "for element \"" << element.substr(9) << "\" defined as \"" << type << "\""; mType = CarouselType::HORIZONTAL; } } if (mGamelistView && properties && elem->has("imageType")) { const std::vector supportedImageTypes { "marquee", "cover", "backcover", "3dbox", "physicalmedia", "screenshot", "titlescreen", "miximage", "fanart", "none"}; std::string imageTypesString {elem->get("imageType")}; for (auto& character : imageTypesString) { if (std::isspace(character)) character = ','; } imageTypesString = Utils::String::replace(imageTypesString, ",,", ","); mImageTypes = Utils::String::delimitedStringToVector(imageTypesString, ","); // Only allow two imageType entries due to performance reasons. if (mImageTypes.size() > 2) mImageTypes.erase(mImageTypes.begin() + 2, mImageTypes.end()); if (mImageTypes.empty()) { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property \"imageType\" " "for element \"" << element.substr(9) << "\" contains no values"; } for (std::string& type : mImageTypes) { if (std::find(supportedImageTypes.cbegin(), supportedImageTypes.cend(), type) == supportedImageTypes.cend()) { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property \"imageType\" " "for element \"" << element.substr(9) << "\" defined as \"" << type << "\""; mImageTypes.clear(); break; } } if (mImageTypes.size() == 2 && mImageTypes.front() == mImageTypes.back()) { LOG(LogError) << "CarouselComponent: Invalid theme configuration, property \"imageType\" " "for element \"" << element.substr(9) << "\" contains duplicate values"; mImageTypes.clear(); } } if (elem->has("color")) { mCarouselColor = elem->get("color"); mCarouselColorEnd = mCarouselColor; } if (elem->has("colorEnd")) mCarouselColorEnd = elem->get("colorEnd"); if (elem->has("gradientType")) { const std::string& gradientType {elem->get("gradientType")}; if (gradientType == "horizontal") { mColorGradientHorizontal = true; } else if (gradientType == "vertical") { mColorGradientHorizontal = false; } else { mColorGradientHorizontal = true; LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"gradientType\" for element \"" << element.substr(9) << "\" defined as \"" << gradientType << "\""; } } mLinearInterpolation = true; if (elem->has("maxItemCount")) { mMaxItemCount = glm::clamp(elem->get("maxItemCount"), 0.5f, 30.0f); if (mType == CarouselType::HORIZONTAL_WHEEL || mType == CarouselType::VERTICAL_WHEEL) { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"maxItemCount\" for element \"" << element.substr(9) << "\" not applicable to the " << (mType == CarouselType::HORIZONTAL_WHEEL ? "\"horizontalWheel\"" : "\"verticalWheel\"") << " type"; } } if (elem->has("itemsBeforeCenter")) mItemsBeforeCenter = glm::clamp(static_cast(elem->get("itemsBeforeCenter")), 0, 20); if (elem->has("itemsAfterCenter")) mItemsAfterCenter = glm::clamp(static_cast(elem->get("itemsAfterCenter")), 0, 20); if (mType == CarouselType::HORIZONTAL || mType == CarouselType::VERTICAL) { if (elem->has("selectedItemMargins")) { const glm::vec2 selectedItemMargins { glm::clamp(elem->get("selectedItemMargins"), -1.0f, 1.0f)}; if (mType == CarouselType::HORIZONTAL) mSelectedItemMargins = selectedItemMargins * Renderer::getScreenWidth(); else mSelectedItemMargins = selectedItemMargins * Renderer::getScreenHeight(); } if (elem->has("selectedItemOffset")) { const glm::vec2 selectedItemOffset { glm::clamp(elem->get("selectedItemOffset"), -1.0f, 1.0f)}; if (mType == CarouselType::HORIZONTAL) mSelectedItemOffset = selectedItemOffset * Renderer::getScreenWidth(); else mSelectedItemOffset = selectedItemOffset * Renderer::getScreenHeight(); } } if (elem->has("itemSize")) { const glm::vec2 itemSize {glm::clamp(elem->get("itemSize"), 0.05f, 1.0f)}; mItemSize = itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); } if (elem->has("itemStacking")) { const std::string& itemStacking {elem->get("itemStacking")}; if (itemStacking == "ascending") { mItemStacking = ItemStacking::ASCENDING; } else if (itemStacking == "ascendingRaised") { mItemStacking = ItemStacking::ASCENDING_RAISED; } else if (itemStacking == "descending") { mItemStacking = ItemStacking::DESCENDING; } else if (itemStacking == "descendingRaised") { mItemStacking = ItemStacking::DESCENDING_RAISED; } else if (itemStacking == "centered") { mItemStacking = ItemStacking::CENTERED; } else { mItemStacking = ItemStacking::CENTERED; LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"itemStacking\" for element \"" << element.substr(9) << "\" defined as \"" << itemStacking << "\""; } } if (elem->has("itemScale")) mItemScale = glm::clamp(elem->get("itemScale"), 0.2f, 3.0f); if (elem->has("imageFit")) { const std::string& imageFit {elem->get("imageFit")}; if (imageFit == "contain") { mImagefit = ImageFit::CONTAIN; } else if (imageFit == "fill") { mImagefit = ImageFit::FILL; } else if (imageFit == "cover") { mImagefit = ImageFit::COVER; if (elem->has("imageCropPos")) mImageCropPos = glm::clamp(elem->get("imageCropPos"), 0.0f, 1.0f); } else { mImagefit = ImageFit::CONTAIN; LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"imageFit\" for element \"" << element.substr(9) << "\" defined as \"" << imageFit << "\""; } } if (elem->has("imageCornerRadius")) mImageCornerRadius = glm::clamp(elem->get("imageCornerRadius"), 0.0f, 0.5f) * (mItemScale >= 1.0f ? mItemScale : 1.0f) * mRenderer->getScreenWidth(); mImageSelectedColor = mImageColorShift; mImageSelectedColorEnd = mImageColorShiftEnd; if (elem->has("imageSelectedColor")) { mImageSelectedColor = elem->get("imageSelectedColor"); mImageSelectedColorEnd = mImageSelectedColor; mHasImageSelectedColor = true; } if (elem->has("imageSelectedColorEnd")) { mImageSelectedColorEnd = elem->get("imageSelectedColorEnd"); mHasImageSelectedColor = true; } if (elem->has("imageSelectedGradientType")) { const std::string& gradientType {elem->get("imageSelectedGradientType")}; if (gradientType == "horizontal") { mImageSelectedColorGradientHorizontal = true; } else if (gradientType == "vertical") { mImageSelectedColorGradientHorizontal = false; } else { mImageSelectedColorGradientHorizontal = true; LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"imageSelectedGradientType\" for element \"" << element.substr(9) << "\" defined as \"" << gradientType << "\""; } } if (elem->has("imageBrightness")) mImageBrightness = glm::clamp(elem->get("imageBrightness"), -2.0f, 2.0f); if (elem->has("imageSaturation")) mImageSaturation = glm::clamp(elem->get("imageSaturation"), 0.0f, 1.0f); if (elem->has("itemDiagonalOffset") && (mType == CarouselType::HORIZONTAL || mType == CarouselType::VERTICAL)) { const float diagonalOffset { glm::clamp(elem->get("itemDiagonalOffset"), -0.5f, 0.5f)}; if (mType == CarouselType::HORIZONTAL) mItemDiagonalOffset = diagonalOffset * Renderer::getScreenHeight(); else mItemDiagonalOffset = diagonalOffset * Renderer::getScreenWidth(); } if (elem->has("imageInterpolation")) { const std::string& imageInterpolation {elem->get("imageInterpolation")}; if (imageInterpolation == "linear") { mLinearInterpolation = true; } else if (imageInterpolation == "nearest") { mLinearInterpolation = false; } else { mLinearInterpolation = true; LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"imageInterpolation\" for element \"" << element.substr(9) << "\" defined as \"" << imageInterpolation << "\""; } } if (elem->has("itemTransitions")) { const std::string& itemTransitions {elem->get("itemTransitions")}; if (itemTransitions == "animate") { mInstantItemTransitions = false; } else if (itemTransitions == "instant") { mInstantItemTransitions = true; } else { mInstantItemTransitions = false; LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"itemTransitions\" for element \"" << element.substr(9) << "\" defined as \"" << itemTransitions << "\""; } } if (elem->has("itemRotation")) mItemRotation = elem->get("itemRotation"); if (elem->has("itemRotationOrigin")) mItemRotationOrigin = elem->get("itemRotationOrigin"); mItemAxisHorizontal = (elem->has("itemAxisHorizontal") && elem->get("itemAxisHorizontal")); if (elem->has("itemAxisRotation")) mItemAxisRotation = elem->get("itemAxisRotation"); if (elem->has("imageColor")) { mImageColorShift = elem->get("imageColor"); mImageColorShiftEnd = mImageColorShift; } if (elem->has("imageColorEnd")) mImageColorShiftEnd = elem->get("imageColorEnd"); if (elem->has("imageGradientType")) { const std::string& gradientType {elem->get("imageGradientType")}; if (gradientType == "horizontal") { mImageColorGradientHorizontal = true; } else if (gradientType == "vertical") { mImageColorGradientHorizontal = false; } else { mImageColorGradientHorizontal = true; LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"imageGradientType\" for element \"" << element.substr(9) << "\" defined as \"" << gradientType << "\""; } } if (elem->has("itemHorizontalAlignment") && mType != CarouselType::HORIZONTAL && mType != CarouselType::HORIZONTAL_WHEEL) { const std::string& alignment {elem->get("itemHorizontalAlignment")}; if (alignment == "left") { mItemHorizontalAlignment = ALIGN_LEFT; } else if (alignment == "right") { mItemHorizontalAlignment = ALIGN_RIGHT; } else if (alignment == "center") { mItemHorizontalAlignment = ALIGN_CENTER; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"itemHorizontalAlignment\" for element \"" << element.substr(9) << "\" defined as \"" << alignment << "\""; mItemHorizontalAlignment = ALIGN_CENTER; } } if (elem->has("itemVerticalAlignment") && mType != CarouselType::VERTICAL) { const std::string& alignment {elem->get("itemVerticalAlignment")}; if (alignment == "top") { mItemVerticalAlignment = ALIGN_TOP; } else if (alignment == "bottom") { mItemVerticalAlignment = ALIGN_BOTTOM; } else if (alignment == "center") { mItemVerticalAlignment = ALIGN_CENTER; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"itemVerticalAlignment\" for element \"" << element.substr(9) << "\" defined as \"" << alignment << "\""; mItemVerticalAlignment = ALIGN_CENTER; } } if (elem->has("wheelHorizontalAlignment") && mType == CarouselType::VERTICAL_WHEEL) { const std::string& alignment {elem->get("wheelHorizontalAlignment")}; if (alignment == "left") { mWheelHorizontalAlignment = ALIGN_LEFT; } else if (alignment == "right") { mWheelHorizontalAlignment = ALIGN_RIGHT; } else if (alignment == "center") { mWheelHorizontalAlignment = ALIGN_CENTER; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"wheelHorizontalAlignment\" for element \"" << element.substr(9) << "\" defined as \"" << alignment << "\""; mWheelHorizontalAlignment = ALIGN_CENTER; } } if (elem->has("wheelVerticalAlignment") && mType == CarouselType::HORIZONTAL_WHEEL) { const std::string& alignment {elem->get("wheelVerticalAlignment")}; if (alignment == "top") { mWheelVerticalAlignment = ALIGN_TOP; } else if (alignment == "bottom") { mWheelVerticalAlignment = ALIGN_BOTTOM; } else if (alignment == "center") { mWheelVerticalAlignment = ALIGN_CENTER; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"wheelVerticalAlignment\" for element \"" << element.substr(9) << "\" defined as \"" << alignment << "\""; mWheelVerticalAlignment = ALIGN_CENTER; } } if (elem->has("horizontalOffset")) mHorizontalOffset = glm::clamp(elem->get("horizontalOffset"), -1.0f, 1.0f); if (elem->has("verticalOffset")) mVerticalOffset = glm::clamp(elem->get("verticalOffset"), -1.0f, 1.0f); if (elem->has("reflections") && elem->get("reflections")) { if (mType == CarouselType::HORIZONTAL) { mReflections = elem->get("reflections"); } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"reflections\" for element \"" << element.substr(9) << "\" only supported for horizontal carousel type"; } } if (elem->has("reflectionsOpacity")) mReflectionsOpacity = glm::clamp(elem->get("reflectionsOpacity"), 0.1f, 1.0f); if (elem->has("reflectionsFalloff")) mReflectionsFalloff = glm::clamp(elem->get("reflectionsFalloff"), 0.0f, 10.0f); if (elem->has("unfocusedItemOpacity")) mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); if (elem->has("unfocusedItemSaturation")) { mUnfocusedItemSaturation = glm::clamp(elem->get("unfocusedItemSaturation"), 0.0f, 1.0f); mHasUnfocusedItemSaturation = true; } if (elem->has("unfocusedItemDimming")) mUnfocusedItemDimming = glm::clamp(elem->get("unfocusedItemDimming"), 0.0f, 1.0f); if (elem->has("fastScrolling") && elem->get("fastScrolling")) List::mTierList = IList::LIST_SCROLL_STYLE_MEDIUM; // Ccale the font size with the itemScale property value. mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, (mItemScale >= 1.0f ? mItemScale : 1.0f)); if (elem->has("textRelativeScale")) mTextRelativeScale = glm::clamp(elem->get("textRelativeScale"), 0.2f, 1.0f); if (elem->has("textColor")) mTextColor = elem->get("textColor"); if (elem->has("textBackgroundColor")) mTextBackgroundColor = elem->get("textBackgroundColor"); mTextSelectedColor = mTextColor; mTextSelectedBackgroundColor = mTextBackgroundColor; if (elem->has("textSelectedColor")) { mTextSelectedColor = elem->get("textSelectedColor"); mHasTextSelectedColor = true; } if (elem->has("textSelectedBackgroundColor")) { mTextSelectedBackgroundColor = elem->get("textSelectedBackgroundColor"); mHasTextSelectedColor = true; } if (elem->has("textHorizontalScrolling")) mTextHorizontalScrolling = elem->get("textHorizontalScrolling"); if (elem->has("textHorizontalScrollSpeed")) { mTextHorizontalScrollSpeed = glm::clamp(elem->get("textHorizontalScrollSpeed"), 0.1f, 10.0f); } if (elem->has("textHorizontalScrollDelay")) { mTextHorizontalScrollDelay = glm::clamp(elem->get("textHorizontalScrollDelay"), 0.0f, 10.0f) * 1000.0f; } if (elem->has("textHorizontalScrollGap")) { mTextHorizontalScrollGap = glm::clamp(elem->get("textHorizontalScrollGap"), 0.1f, 5.0f); } if (elem->has("lineSpacing")) mLineSpacing = glm::clamp(elem->get("lineSpacing"), 0.5f, 3.0f); if (elem->has("letterCase")) { const std::string& letterCase {elem->get("letterCase")}; if (letterCase == "uppercase") { mLetterCase = LetterCase::UPPERCASE; } else if (letterCase == "lowercase") { mLetterCase = LetterCase::LOWERCASE; } else if (letterCase == "capitalize") { mLetterCase = LetterCase::CAPITALIZE; } else if (letterCase != "none") { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"letterCase\" for element \"" << element.substr(9) << "\" defined as \"" << letterCase << "\""; } } if (elem->has("letterCaseAutoCollections")) { const std::string& letterCase {elem->get("letterCaseAutoCollections")}; if (letterCase == "uppercase") { mLetterCaseAutoCollections = LetterCase::UPPERCASE; } else if (letterCase == "lowercase") { mLetterCaseAutoCollections = LetterCase::LOWERCASE; } else if (letterCase == "capitalize") { mLetterCaseAutoCollections = LetterCase::CAPITALIZE; } else if (letterCase == "none") { mLetterCaseAutoCollections = LetterCase::NONE; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"letterCaseAutoCollections\" for element \"" << element.substr(9) << "\" defined as \"" << letterCase << "\""; } } if (elem->has("letterCaseCustomCollections")) { const std::string& letterCase {elem->get("letterCaseCustomCollections")}; if (letterCase == "uppercase") { mLetterCaseCustomCollections = LetterCase::UPPERCASE; } else if (letterCase == "lowercase") { mLetterCaseCustomCollections = LetterCase::LOWERCASE; } else if (letterCase == "capitalize") { mLetterCaseCustomCollections = LetterCase::CAPITALIZE; } else if (letterCase == "none") { mLetterCaseCustomCollections = LetterCase::NONE; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"letterCaseCustomCollections\" for element \"" << element.substr(9) << "\" defined as \"" << letterCase << "\""; } } if (mGamelistView && elem->has("systemNameSuffix")) mSystemNameSuffix = elem->get("systemNameSuffix"); if (mGamelistView && properties & LETTER_CASE && elem->has("letterCaseSystemNameSuffix")) { const std::string& letterCase {elem->get("letterCaseSystemNameSuffix")}; if (letterCase == "uppercase") { mLetterCaseSystemNameSuffix = LetterCase::UPPERCASE; } else if (letterCase == "lowercase") { mLetterCaseSystemNameSuffix = LetterCase::LOWERCASE; } else if (letterCase == "capitalize") { mLetterCaseSystemNameSuffix = LetterCase::CAPITALIZE; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " "\"letterCaseSystemNameSuffix\" for element \"" << element.substr(9) << "\" defined as \"" << letterCase << "\""; } } if (elem->has("fadeAbovePrimary")) mFadeAbovePrimary = elem->get("fadeAbovePrimary"); GuiComponent::applyTheme(theme, view, element, ALL); mSize.x = glm::clamp(mSize.x, mRenderer->getScreenWidth() * 0.05f, mRenderer->getScreenWidth() * 2.0f); mSize.y = glm::clamp(mSize.y, mRenderer->getScreenHeight() * 0.05f, mRenderer->getScreenHeight() * 2.0f); } template void CarouselComponent::onCursorChanged(const CursorState& state) { if (mEntries.size() > static_cast(mLastCursor)) mEntries.at(mLastCursor).data.item->resetComponent(); float startPos {mEntryCamOffset}; float posMax {static_cast(mEntries.size())}; float target {static_cast(mCursor)}; // Find the shortest path to the target. float endPos {target}; // Directly. if (mPreviousScrollVelocity > 0 && mScrollVelocity == 0 && mEntryCamOffset > posMax - 1.0f) startPos = 0.0f; // If quick jumping to the start or end of the list using the trigger button when in // the gamelist view, then always animate in the requested direction. if (!mTriggerJump) { float dist {fabsf(endPos - startPos)}; if (fabsf(target + posMax - startPos - mScrollVelocity) < dist) endPos = target + posMax; // Loop around the end (0 -> max). if (fabsf(target - posMax - startPos - mScrollVelocity) < dist) endPos = target - posMax; // Loop around the start (max - 1 -> -1). } // Make sure there are no reverse jumps between items. bool changedDirection {false}; if (mPreviousScrollVelocity != 0 && mPreviousScrollVelocity != mScrollVelocity) changedDirection = true; if (!changedDirection && mScrollVelocity > 0 && endPos < startPos) endPos = endPos + posMax; if (!changedDirection && mScrollVelocity < 0 && endPos > startPos) endPos = endPos - posMax; if (mScrollVelocity != 0) mPreviousScrollVelocity = mScrollVelocity; // Needed to make sure that overlapping items are renderered correctly. if (startPos > endPos) mPositiveDirection = true; else mPositiveDirection = false; mEntryCamTarget = endPos; float animTime {400.0f}; float timeDiff {1.0f}; // If startPos is inbetween two positions then reduce the time slightly as the distance will // be shorter meaning the animation would play for too long if not compensated for. if (mScrollVelocity == 1) timeDiff = endPos - startPos; else if (mScrollVelocity == -1) timeDiff = startPos - endPos; if (timeDiff != 1.0f) animTime = glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 200.0f, animTime); Animation* anim {new LambdaAnimation( [this, startPos, endPos, posMax](float t) { // Non-linear interpolation. t = 1.0f - (1.0f - t) * (1.0f - t); float f {(endPos * t) + (startPos * (1.0f - t))}; if (f < 0) f += posMax; if (f >= posMax) f -= posMax; mEntryCamOffset = f; }, static_cast(animTime))}; GuiComponent::setAnimation(anim, 0, nullptr, false, 0); if (mCursorChangedCallback) mCursorChangedCallback(state); } #endif // ES_CORE_COMPONENTS_PRIMARY_CAROUSEL_COMPONENT_H