From 48111ce5e4ec145d4589b1d6806de0b429f5feb5 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 12 Nov 2022 14:08:53 +0100 Subject: [PATCH] Added basic GridComponent functionality and integration. --- es-app/src/views/GamelistBase.cpp | 49 +- es-app/src/views/GamelistBase.h | 2 + es-app/src/views/GamelistView.cpp | 65 ++- es-app/src/views/SystemView.cpp | 91 ++-- es-app/src/views/SystemView.h | 1 + es-core/src/ThemeData.cpp | 34 ++ es-core/src/components/IList.h | 6 + .../components/primary/CarouselComponent.h | 5 +- .../src/components/primary/GridComponent.h | 485 +++++++++++++++++- .../src/components/primary/PrimaryComponent.h | 1 + .../components/primary/TextListComponent.h | 4 +- 11 files changed, 675 insertions(+), 68 deletions(-) diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index b99e08bf2..d03e4bf99 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -568,15 +568,19 @@ void GamelistBase::populateList(const std::vector& files, FileData* f auto theme = mRoot->getSystem()->getTheme(); std::string name; - std::string carouselItemType; - std::string carouselDefaultItem; + std::string defaultItem; if (mCarousel != nullptr) { - carouselItemType = mCarousel->getItemType(); - carouselDefaultItem = mCarousel->getDefaultItem(); - if (!ResourceManager::getInstance().fileExists(carouselDefaultItem)) - carouselDefaultItem = ""; + defaultItem = mCarousel->getDefaultItem(); + if (!ResourceManager::getInstance().fileExists(defaultItem)) + defaultItem = ""; } + else if (mGrid != nullptr) { + defaultItem = mGrid->getDefaultItem(); + if (!ResourceManager::getInstance().fileExists(defaultItem)) + defaultItem = ""; + } + if (files.size() > 0) { for (auto it = files.cbegin(); it != files.cend(); ++it) { @@ -593,8 +597,6 @@ void GamelistBase::populateList(const std::vector& files, FileData* f } if (mCarousel != nullptr) { - assert(carouselItemType != ""); - CarouselComponent::Entry carouselEntry; carouselEntry.name = (*it)->getName(); carouselEntry.object = *it; @@ -606,13 +608,29 @@ void GamelistBase::populateList(const std::vector& files, FileData* f else if (letterCase == LetterCase::CAPITALIZED) carouselEntry.name = Utils::String::toCapitalized(carouselEntry.name); - if (carouselDefaultItem != "") - carouselEntry.data.defaultItemPath = carouselDefaultItem; + if (defaultItem != "") + carouselEntry.data.defaultItemPath = defaultItem; mCarousel->addEntry(carouselEntry, theme); } + else if (mGrid != nullptr) { + GridComponent::Entry gridEntry; + gridEntry.name = (*it)->getName(); + gridEntry.object = *it; - if (mTextList != nullptr) { + if (letterCase == LetterCase::UPPERCASE) + gridEntry.name = Utils::String::toUpper(gridEntry.name); + else if (letterCase == LetterCase::LOWERCASE) + gridEntry.name = Utils::String::toLower(gridEntry.name); + else if (letterCase == LetterCase::CAPITALIZED) + gridEntry.name = Utils::String::toCapitalized(gridEntry.name); + + if (defaultItem != "") + gridEntry.data.defaultItemPath = defaultItem; + + mGrid->addEntry(gridEntry, theme); + } + else if (mTextList != nullptr) { TextListComponent::Entry textListEntry; std::string indicators {mTextList->getIndicators()}; std::string collectionIndicators {mTextList->getCollectionIndicators()}; @@ -717,13 +735,20 @@ void GamelistBase::addPlaceholder(FileData* firstEntry) textListEntry.data.entryType = TextListEntryType::SECONDARY; mTextList->addEntry(textListEntry); } - if (mCarousel != nullptr) { + else if (mCarousel != nullptr) { CarouselComponent::Entry carouselEntry; carouselEntry.name = placeholder->getName(); letterCaseFunc(carouselEntry.name); carouselEntry.object = placeholder; mCarousel->addEntry(carouselEntry, mRoot->getSystem()->getTheme()); } + else if (mGrid != nullptr) { + GridComponent::Entry gridEntry; + gridEntry.name = placeholder->getName(); + letterCaseFunc(gridEntry.name); + gridEntry.object = placeholder; + mGrid->addEntry(gridEntry, mRoot->getSystem()->getTheme()); + } } void GamelistBase::generateFirstLetterIndex(const std::vector& files) diff --git a/es-app/src/views/GamelistBase.h b/es-app/src/views/GamelistBase.h index 046a2d478..59b188c29 100644 --- a/es-app/src/views/GamelistBase.h +++ b/es-app/src/views/GamelistBase.h @@ -23,6 +23,7 @@ #include "components/TextComponent.h" #include "components/VideoFFmpegComponent.h" #include "components/primary/CarouselComponent.h" +#include "components/primary/GridComponent.h" #include "components/primary/TextListComponent.h" #include @@ -90,6 +91,7 @@ protected: FileData* mRoot; std::unique_ptr> mCarousel; + std::unique_ptr> mGrid; std::unique_ptr> mTextList; PrimaryComponent* mPrimary; diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index 76a3f0582..a529cac68 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -114,15 +114,24 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) if (mTheme->hasView("gamelist")) { for (auto& element : mTheme->getViewElements("gamelist").elements) { - if (element.second.type == "textlist" || element.second.type == "carousel") { - if (element.second.type == "carousel" && mTextList != nullptr) { + if (element.second.type == "carousel" || element.second.type == "grid" || + element.second.type == "textlist") { + if (element.second.type == "carousel" && + (mGrid != nullptr || mTextList != nullptr)) { LOG(LogWarning) << "SystemView::populate(): Multiple primary components " - << "defined, skipping configuration entry"; + << "defined, skipping carousel configuration entry"; continue; } - if (element.second.type == "textlist" && mCarousel != nullptr) { + if (element.second.type == "grid" && + (mCarousel != nullptr || mTextList != nullptr)) { LOG(LogWarning) << "SystemView::populate(): Multiple primary components " - << "defined, skipping configuration entry"; + << "defined, skipping grid configuration entry"; + continue; + } + if (element.second.type == "textlist" && + (mCarousel != nullptr || mGrid != nullptr)) { + LOG(LogWarning) << "SystemView::populate(): Multiple primary components " + << "defined, skipping textlist configuration entry"; continue; } } @@ -154,10 +163,11 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) mCarousel->setItemType(itemType); } else { - LOG(LogWarning) - << "GamelistView::onThemeChanged(): Invalid theme configuration, " - " property defined as \"" - << itemType << "\""; + LOG(LogWarning) << "GamelistView::onThemeChanged(): Invalid theme " + "configuration, carousel property \"itemType\" " + "for element \"" + << element.first.substr(9) << "\" defined as \"" + << itemType << "\""; mCarousel->setItemType("marquee"); } } @@ -174,6 +184,40 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) mPrimary->applyTheme(theme, "gamelist", element.first, ALL); addChild(mPrimary); } + if (element.second.type == "grid") { + if (mGrid == nullptr) { + mGrid = std::make_unique>(); + if (element.second.has("itemType")) { + const std::string itemType {element.second.get("itemType")}; + if (itemType == "marquee" || itemType == "cover" || + itemType == "backcover" || itemType == "3dbox" || + itemType == "physicalmedia" || itemType == "screenshot" || + itemType == "titlescreen" || itemType == "miximage" || + itemType == "fanart" || itemType == "none") { + mGrid->setItemType(itemType); + } + else { + LOG(LogWarning) << "GamelistView::onThemeChanged(): Invalid theme " + "configuration, grid property \"itemType\" " + "for element \"" + << element.first.substr(5) << "\" defined as \"" + << itemType << "\""; + mGrid->setItemType("marquee"); + } + } + else { + mGrid->setItemType("marquee"); + } + if (element.second.has("defaultItem")) + mGrid->setDefaultItem(element.second.get("defaultItem")); + mPrimary = mGrid.get(); + } + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { updateView(state); }); + mPrimary->setDefaultZIndex(50.0f); + mPrimary->applyTheme(theme, "gamelist", element.first, ALL); + addChild(mPrimary); + } if (element.second.type == "image") { // If this is the startup system, then forceload the images to avoid texture pop-in. if (isStartupSystem) @@ -331,6 +375,9 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) mCarousel->getType() == CarouselComponent::CarouselType::HORIZONTAL_WHEEL) mLeftRightAvailable = false; } + else if (mGrid != nullptr) { + mLeftRightAvailable = false; + } for (auto& video : mStaticVideoComponents) { if (video->hasStaticVideo()) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 8b6830d5e..850d45c0e 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -145,9 +145,6 @@ bool SystemView::input(InputConfig* config, Input input) void SystemView::update(int deltaTime) { - if (!mPrimary->isAnimationPlaying(0)) - mMaxFade = false; - mPrimary->update(deltaTime); for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) { @@ -295,19 +292,24 @@ void SystemView::onCursorChanged(const CursorState& state) Animation* anim; + float animTime {380.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 (scrollVelocity == 1) + timeDiff = endPos - startPos; + else if (scrollVelocity == -1) + timeDiff = startPos - endPos; + + if (timeDiff != 1.0f) + animTime = + glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 200.0f, animTime); + if (transitionStyle == "fade") { float startFade {mFadeOpacity}; anim = new LambdaAnimation( [this, startFade, startPos, endPos, posMax](float t) { - t -= 1; - float f {glm::mix(startPos, endPos, t * t * t + 1.0f)}; - if (f < 0.0f) - f += posMax; - if (f >= posMax) - f -= posMax; - - t += 1; - if (t < 0.3f) mFadeOpacity = glm::mix(0.0f, 1.0f, glm::clamp(t / 0.2f + startFade, 0.0f, 1.0f)); @@ -319,7 +321,7 @@ void SystemView::onCursorChanged(const CursorState& state) if (t > 0.5f) mCamOffset = endPos; - if (t >= 0.7f && t != 1.0f) + if (mNavigated && t >= 0.7f && t != 1.0f) mMaxFade = true; // Update the game count when the entire animation has been completed. @@ -328,15 +330,16 @@ void SystemView::onCursorChanged(const CursorState& state) updateGameCount(); } }, - 500); + static_cast(animTime * 1.3f)); } else if (transitionStyle == "slide") { mUpdatedGameCount = false; anim = new LambdaAnimation( [this, startPos, endPos, posMax](float t) { - t -= 1; - float f {glm::mix(startPos, endPos, t * t * t + 1.0f)}; - if (f < 0.0f) + // 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; @@ -362,23 +365,13 @@ void SystemView::onCursorChanged(const CursorState& state) updateGameCount(); } }, - 500); + static_cast(animTime)); } else { // Instant. updateGameCount(); anim = new LambdaAnimation( - [this, startPos, endPos, posMax](float t) { - t -= 1; - float f {glm::mix(startPos, endPos, t * t * t + 1.0f)}; - if (f < 0.0f) - f += posMax; - if (f >= posMax) - f -= posMax; - - mCamOffset = endPos; - }, - 500); + [this, startPos, endPos, posMax](float t) { mCamOffset = endPos; }, animTime); } setAnimation(anim, 0, nullptr, false, 0); @@ -458,17 +451,27 @@ void SystemView::populate() ThemeFlags::ALL); elements.gameSelectors.back()->setNeedsRefresh(); } - if (element.second.type == "textlist" || element.second.type == "carousel") { - if (element.second.type == "carousel" && mTextList != nullptr) { + if (element.second.type == "carousel" || element.second.type == "grid" || + element.second.type == "textlist") { + if (element.second.type == "carousel" && + (mGrid != nullptr || mTextList != nullptr)) { LOG(LogWarning) << "SystemView::populate(): Multiple primary components " - << "defined, skipping configuration entry"; + << "defined, skipping carousel configuration entry"; continue; } - if (element.second.type == "textlist" && mCarousel != nullptr) { + if (element.second.type == "grid" && + (mCarousel != nullptr || mTextList != nullptr)) { LOG(LogWarning) << "SystemView::populate(): Multiple primary components " - << "defined, skipping configuration entry"; + << "defined, skipping grid configuration entry"; + continue; + } + if (element.second.type == "textlist" && + (mCarousel != nullptr || mGrid != nullptr)) { + LOG(LogWarning) + << "SystemView::populate(): Multiple primary components " + << "defined, skipping textlist configuration entry"; continue; } if (element.second.type == "carousel" && mCarousel == nullptr) { @@ -476,6 +479,11 @@ void SystemView::populate() mPrimary = mCarousel.get(); mPrimaryType = PrimaryType::CAROUSEL; } + else if (element.second.type == "grid" && mGrid == nullptr) { + mGrid = std::make_unique>(); + mPrimary = mGrid.get(); + mPrimaryType = PrimaryType::GRID; + } else if (element.second.type == "textlist" && mTextList == nullptr) { mTextList = std::make_unique>(); mPrimary = mTextList.get(); @@ -497,7 +505,7 @@ void SystemView::populate() anim->setPauseAnimation(true); } }); - if (mCarousel != nullptr) { + if (mCarousel != nullptr || mGrid != nullptr) { if (element.second.has("staticItem")) itemPath = element.second.get("staticItem"); if (element.second.has("defaultItem")) @@ -675,6 +683,15 @@ void SystemView::populate() entry.data.defaultItemPath = defaultItemPath; mCarousel->addEntry(entry, theme); } + else if (mGrid != nullptr) { + GridComponent::Entry entry; + entry.name = it->getFullName(); + letterCaseFunc(entry.name); + entry.object = it; + entry.data.itemPath = itemPath; + entry.data.defaultItemPath = defaultItemPath; + mGrid->addEntry(entry, theme); + } else if (mTextList != nullptr) { TextListComponent::Entry entry; entry.name = it->getFullName(); @@ -1268,6 +1285,10 @@ void SystemView::renderElements(const glm::mat4& parentTrans, bool abovePrimary) elementTrans, glm::round(glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f})); } + else if (mGrid != nullptr) { + elementTrans = glm::translate( + elementTrans, glm::round(glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f})); + } else if (mTextList != nullptr) { elementTrans = glm::translate( elementTrans, glm::round(glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f})); diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 26a15442d..6040fca27 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -126,6 +126,7 @@ private: Renderer* mRenderer; std::unique_ptr> mCarousel; + std::unique_ptr> mGrid; std::unique_ptr> mTextList; std::unique_ptr mLegacySystemInfo; std::vector mSystemElements; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 26aae7104..06f54ca12 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -322,6 +322,40 @@ std::map> {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}, {"legacyZIndexMode", STRING}}}, // For backward compatibility with legacy themes. + {"grid", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"columns", UNSIGNED_INTEGER}, + {"staticItem", PATH}, + {"itemType", STRING}, + {"defaultItem", PATH}, + {"itemSize", NORMALIZED_PAIR}, + {"itemScale", FLOAT}, + {"itemSpacing", NORMALIZED_PAIR}, + {"itemTransitions", STRING}, + {"itemHorizontalAlignment", STRING}, + {"itemVerticalAlignment", STRING}, + {"horizontalMargins", NORMALIZED_PAIR}, + {"verticalMargins", NORMALIZED_PAIR}, + {"horizontalOffset", FLOAT}, + {"verticalOffset", FLOAT}, + {"unfocusedItemOpacity", FLOAT}, + {"edgeScaleInwards", BOOLEAN}, + {"color", COLOR}, + {"colorEnd", COLOR}, + {"gradientType", STRING}, + {"text", STRING}, + {"textColor", COLOR}, + {"textBackgroundColor", COLOR}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"letterCase", STRING}, + {"letterCaseCollections", STRING}, + {"letterCaseGroupedCollections", STRING}, + {"lineSpacing", FLOAT}, + {"fadeAbovePrimary", BOOLEAN}, + {"zIndex", FLOAT}}}, {"textlist", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index e8300ee6d..888a66042 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -73,6 +73,7 @@ protected: const ScrollTierList& mTierList; const ListLoopType mLoopType; int mCursor; + int mLastCursor; int mScrollTier; int mScrollVelocity; int mScrollTierAccumulator; @@ -88,6 +89,7 @@ public: , mTierList {tierList} , mLoopType {loopType} , mCursor {0} + , mLastCursor {0} , mScrollTier {0} , mScrollVelocity {0} , mScrollTierAccumulator {0} @@ -213,6 +215,7 @@ protected: bool listFirstRow() { + mLastCursor = mCursor; mCursor = 0; onCursorChanged(CursorState::CURSOR_STOPPED); onScroll(); @@ -221,6 +224,7 @@ protected: bool listLastRow() { + mLastCursor = mCursor; mCursor = static_cast(mEntries.size()) - 1; onCursorChanged(CursorState::CURSOR_STOPPED); onScroll(); @@ -323,6 +327,8 @@ protected: if (mScrollVelocity == 0 || size() < 2) return; + mLastCursor = mCursor; + int cursor {mCursor + amt}; int absAmt {amt < 0 ? -amt : amt}; diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index ba0b13817..373649929 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -1327,7 +1327,7 @@ template void CarouselComponent::onCursorChanged(const CursorSta mPositiveDirection = false; mEntryCamTarget = endPos; - float animTime {380}; + float animTime {380.0f}; float timeDiff {1.0f}; // If startPos is inbetween two positions then reduce the time slightly as the distance will @@ -1338,7 +1338,8 @@ template void CarouselComponent::onCursorChanged(const CursorSta timeDiff = startPos - endPos; if (timeDiff != 1.0f) - animTime = glm::clamp(std::fabs(glm::mix(0.0f, 380.0f, timeDiff * 1.5f)), 200.0f, 380.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) { diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 770a86d9a..972188f53 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -6,8 +6,8 @@ // Grid, usable in both the system and gamelist views. // -#ifndef ES_CORE_COMPONENTS_GRID_COMPONENT_H -#define ES_CORE_COMPONENTS_GRID_COMPONENT_H +#ifndef ES_CORE_COMPONENTS_PRIMARY_GRID_COMPONENT_H +#define ES_CORE_COMPONENTS_PRIMARY_GRID_COMPONENT_H #include "components/IList.h" #include "components/primary/PrimaryComponent.h" @@ -26,10 +26,19 @@ class GridComponent : public PrimaryComponent, protected IList protected: using List::mCursor; using List::mEntries; + using List::mLastCursor; + using List::mScrollVelocity; + using List::mSize; public: + using Entry = typename IList::Entry; + GridComponent(); + void addEntry(Entry& entry, const std::shared_ptr& theme); + void updateEntry(Entry& entry, const std::shared_ptr& theme); + void onDemandTextureLoad() override; + void setCancelTransitionsCallback(const std::function& func) override { mCancelTransitionsCallback = func; @@ -42,29 +51,489 @@ public: const size_t getNumEntries() override { return mEntries.size(); } const bool getFadeAbovePrimary() const override { return mFadeAbovePrimary; } const LetterCase getLetterCase() const override { return mLetterCase; } - virtual const LetterCase getLetterCaseCollections() const = 0; - virtual const LetterCase getLetterCaseGroupedCollections() const = 0; + const LetterCase getLetterCaseCollections() const override { return mLetterCaseCollections; } + const LetterCase getLetterCaseGroupedCollections() const override + { + return mLetterCaseGroupedCollections; + } + const std::string& getItemType() { return mItemType; } + void setItemType(std::string itemType) { mItemType = itemType; } + const std::string& getDefaultItem() { return mDefaultItem; } + void setDefaultItem(std::string defaultItem) { mDefaultItem = defaultItem; } + 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 calculateLayout(); + + void onCursorChanged(const CursorState& state) override; + bool isScrolling() const override { return List::isScrolling(); } + void stopScrolling() override { List::stopScrolling(); } + 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(); } + Renderer* mRenderer; std::function mCancelTransitionsCallback; std::function mCursorChangedCallback; - bool mFadeAbovePrimary; + std::string mItemType; + std::string mDefaultItem; + float mEntryOffset; + float mEntryCamTarget; + float mTransitionFactor; + std::shared_ptr mFont; + + unsigned int mColumns; + glm::vec2 mItemSize; + float mItemScale; + glm::vec2 mItemSpacing; + bool mInstantItemTransitions; + float mUnfocusedItemOpacity; + unsigned int mTextColor; + unsigned int mTextBackgroundColor; LetterCase mLetterCase; LetterCase mLetterCaseCollections; LetterCase mLetterCaseGroupedCollections; + float mLineSpacing; + bool mFadeAbovePrimary; + int mPreviousScrollVelocity; + bool mPositiveDirection; + bool mGamelistView; + bool mLayoutValid; + bool mRowJump; }; template GridComponent::GridComponent() - : IList {} + : IList {LIST_SCROLL_STYLE_SLOW, ListLoopType::LIST_PAUSE_AT_END} , mRenderer {Renderer::getInstance()} - , mFadeAbovePrimary {false} + , mEntryOffset {0.0f} + , mEntryCamTarget {0.0f} + , mTransitionFactor {1.0f} + , mFont {Font::get(FONT_SIZE_LARGE)} + , mColumns {5} + , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.15f, + Renderer::getScreenHeight() * 0.25f}} + , mItemScale {1.2f} + , mItemSpacing {glm::vec2 {Renderer::getScreenWidth() * 0.02f, + Renderer::getScreenHeight() * 0.02f}} + , mInstantItemTransitions {false} + , mUnfocusedItemOpacity {1.0f} + , mTextColor {0x000000FF} + , mTextBackgroundColor {0xFFFFFF00} , mLetterCase {LetterCase::NONE} , mLetterCaseCollections {LetterCase::NONE} , mLetterCaseGroupedCollections {LetterCase::NONE} + , mLineSpacing {1.5f} + , mFadeAbovePrimary {false} + , mPreviousScrollVelocity {0} + , mPositiveDirection {false} + , mGamelistView {std::is_same_v ? true : false} + , mLayoutValid {false} + , mRowJump {false} { } -#endif // ES_CORE_COMPONENTS_GRID_COMPONENT_H +template +void GridComponent::addEntry(Entry& entry, const std::shared_ptr& theme) +{ + bool dynamic {true}; + + if (!mGamelistView) + dynamic = false; + + if (entry.data.itemPath != "" && + ResourceManager::getInstance().fileExists(entry.data.itemPath)) { + auto item = std::make_shared(false, dynamic); + item->setLinearInterpolation(true); + item->setMipmapping(true); + item->setMaxSize(mItemSize); + item->setImage(entry.data.itemPath); + item->applyTheme(theme, "system", "", ThemeFlags::ALL); + item->setOrigin(0.5f, 0.5f); + item->setRotateByTargetSize(true); + entry.data.item = item; + } + else if (entry.data.defaultItemPath != "" && + ResourceManager::getInstance().fileExists(entry.data.defaultItemPath)) { + auto defaultItem = std::make_shared(false, dynamic); + defaultItem->setLinearInterpolation(true); + defaultItem->setMipmapping(true); + defaultItem->setMaxSize(mItemSize); + defaultItem->setImage(entry.data.defaultItemPath); + defaultItem->applyTheme(theme, "system", "", ThemeFlags::ALL); + defaultItem->setOrigin(0.5f, 0.5f); + defaultItem->setRotateByTargetSize(true); + entry.data.item = defaultItem; + } + + if (!entry.data.item) { + // If no item image is present, add item text as fallback. + auto text = std::make_shared( + entry.name, mFont, 0x000000FF, Alignment::ALIGN_CENTER, Alignment::ALIGN_CENTER, + glm::vec3 {0.0f, 0.0f, 0.0f}, mItemSize, 0x00000000); + text->setOrigin(0.5f, 0.5f); + text->setLineSpacing(mLineSpacing); + if (!mGamelistView) + text->setValue(entry.name); + text->setColor(mTextColor); + text->setBackgroundColor(mTextBackgroundColor); + text->setRenderBackground(true); + + entry.data.item = text; + } + + List::add(entry); +} + +template +void GridComponent::updateEntry(Entry& entry, const std::shared_ptr& theme) +{ + if (entry.data.itemPath != "") { + auto item = std::make_shared(false, true); + item->setLinearInterpolation(true); + item->setMipmapping(true); + item->setMaxSize(mItemSize); + item->setImage(entry.data.itemPath); + item->applyTheme(theme, "system", "", ThemeFlags::ALL); + item->setOrigin(0.5f, 0.5f); + item->setRotateByTargetSize(true); + entry.data.item = item; + } + else { + return; + } +} + +template void GridComponent::onDemandTextureLoad() +{ + if constexpr (std::is_same_v) { + const int numEntries {static_cast(mEntries.size())}; + + // TODO: Currently loads every item every time. + for (int i {0}; i < size(); ++i) { + int cursor {i}; + + while (cursor < 0) + cursor += numEntries; + while (cursor >= numEntries) + cursor -= numEntries; + + auto& entry = mEntries.at(cursor); + + if (entry.data.itemPath == "") { + FileData* game {entry.object}; + + if (mItemType == "" || mItemType == "marquee") + entry.data.itemPath = game->getMarqueePath(); + else if (mItemType == "cover") + entry.data.itemPath = game->getCoverPath(); + else if (mItemType == "backcover") + entry.data.itemPath = game->getBackCoverPath(); + else if (mItemType == "3dbox") + entry.data.itemPath = game->get3DBoxPath(); + else if (mItemType == "physicalmedia") + entry.data.itemPath = game->getPhysicalMediaPath(); + else if (mItemType == "screenshot") + entry.data.itemPath = game->getScreenshotPath(); + else if (mItemType == "titlescreen") + entry.data.itemPath = game->getTitleScreenPath(); + else if (mItemType == "miximage") + entry.data.itemPath = game->getMiximagePath(); + else if (mItemType == "fanart") + entry.data.itemPath = game->getFanArtPath(); + else if (mItemType == "none") // Display the game name as text. + return; + + auto theme = game->getSystem()->getTheme(); + updateEntry(entry, theme); + } + } + } +} + +template bool GridComponent::input(InputConfig* config, Input input) +{ + if (size() > 0) { + if (input.value != 0) { + mRowJump = false; + + 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; + } + if (config->isMappedLike("up", input)) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + mRowJump = true; + List::listInput(-mColumns); + return true; + } + if (config->isMappedLike("down", input)) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + mRowJump = true; + List::listInput(mColumns); + return true; + } + if (config->isMappedLike("lefttrigger", input)) { + if (getCursor() == 0) + return true; + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + return this->listFirstRow(); + } + if (config->isMappedLike("righttrigger", input)) { + if (getCursor() == static_cast(mEntries.size()) - 1) + return true; + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + return this->listLastRow(); + } + } + else { + if (config->isMappedLike("left", input) || config->isMappedLike("right", input) || + config->isMappedLike("up", input) || config->isMappedLike("down", input) || + config->isMappedLike("lefttrigger", input) || + config->isMappedLike("righttrigger", input)) { + if constexpr (std::is_same_v) { + if (isScrolling()) + onCursorChanged(CursorState::CURSOR_STOPPED); + List::listInput(0); + } + else { + if (isScrolling()) + onCursorChanged(CursorState::CURSOR_STOPPED); + List::listInput(0); + } + } + } + } + + return GuiComponent::input(config, input); +} + +template void GridComponent::update(int deltaTime) +{ + if (!mLayoutValid) + calculateLayout(); + + List::listUpdate(deltaTime); + GuiComponent::update(deltaTime); +} + +template void GridComponent::render(const glm::mat4& parentTrans) +{ + int numEntries {static_cast(mEntries.size())}; + if (numEntries == 0) + return; + + glm::mat4 trans {parentTrans * List::getTransform()}; + mRenderer->setMatrix(trans); + + // In image debug mode, draw a green rectangle covering the entire grid area. + if (Settings::getInstance()->getBool("DebugImage")) + mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x00FF0033, 0x00FF0033); + + for (size_t i {0}; i < mEntries.size(); ++i) { + float opacity {mUnfocusedItemOpacity}; + float scale {1.0f}; + + if (i == static_cast(mCursor)) { + scale = glm::mix(1.0f, mItemScale, mTransitionFactor); + opacity = 1.0f - glm::mix(mUnfocusedItemOpacity, 0.0f, mTransitionFactor); + } + else if (i == static_cast(mLastCursor)) { + scale = glm::mix(mItemScale, 1.0f, mTransitionFactor); + opacity = glm::mix(1.0f, mUnfocusedItemOpacity, mTransitionFactor); + } + + mEntries.at(i).data.item->setScale(scale); + mEntries.at(i).data.item->setOpacity(opacity); + mEntries.at(i).data.item->render(trans); + mEntries.at(i).data.item->setScale(1.0f); + mEntries.at(i).data.item->setOpacity(1.0f); + } + + GuiComponent::renderChildren(trans); +} + +template +void GridComponent::applyTheme(const std::shared_ptr& theme, + const std::string& view, + const std::string& element, + unsigned int properties) +{ + GuiComponent::applyTheme(theme, view, element, properties); + using namespace ThemeFlags; + const ThemeData::ThemeElement* elem {theme->getElement(view, element, "grid")}; + + if (!elem) + return; + + if (elem->has("columns")) + mColumns = glm::clamp(elem->get("columns"), 0u, 100u); + + 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("itemScale")) + mItemScale = glm::clamp(elem->get("itemScale"), 0.5f, 2.0f); + + if (elem->has("itemTransitions")) { + const std::string& itemTransitions {elem->get("itemTransitions")}; + if (itemTransitions == "scale") { + mInstantItemTransitions = false; + } + else if (itemTransitions == "instant") { + mInstantItemTransitions = true; + } + else { + mInstantItemTransitions = false; + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"itemTransitions\" for element \"" + << element.substr(5) << "\" defined as \"" << itemTransitions << "\""; + } + } + + if (elem->has("itemSpacing")) { + const glm::vec2 itemSpacing {glm::clamp(elem->get("itemSpacing"), 0.0f, 0.1f)}; + mItemSpacing = + itemSpacing * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + } + + if (elem->has("unfocusedItemOpacity")) + mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); +} + +template void GridComponent::onCursorChanged(const CursorState& state) +{ + float startPos {mEntryOffset}; + 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 && mEntryOffset > posMax - 1.0f) + startPos = 0.0f; + + float dist {std::fabs(endPos - startPos)}; + + if (std::fabs(target + posMax - startPos - mScrollVelocity) < dist) + endPos = target + posMax; // Loop around the end (0 -> max). + if (std::fabs(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 {250.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)), 180.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; + + mEntryOffset = f; + + if (mInstantItemTransitions) { + mTransitionFactor = 1.0f; + } + else { + // Linear interpolation. + mTransitionFactor = t; + // Non-linear interpolation doesn't seem to be a good match for this component. + // mTransitionFactor = {(1.0f * t) + (0.0f * (1.0f - t))}; + } + }, + static_cast(animTime))}; + + GuiComponent::setAnimation(anim, 0, nullptr, false, 0); + + if (mCursorChangedCallback) + mCursorChangedCallback(state); +} + +template void GridComponent::calculateLayout() +{ + assert(!mEntries.empty()); + + unsigned int columnCount {0}; + unsigned int rowCount {0}; + + for (auto& entry : mEntries) { + entry.data.item->setPosition(glm::vec3 { + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + mItemSpacing.x * columnCount, + (mItemSize.y * rowCount) + (mItemSize.y * 0.5f) + mItemSpacing.y * rowCount, 0.0f}); + if (columnCount == mColumns - 1) { + ++rowCount; + columnCount = 0; + continue; + } + + ++columnCount; + } + + mLayoutValid = true; +} + +#endif // ES_CORE_COMPONENTS_PRIMARY_GRID_COMPONENT_H diff --git a/es-core/src/components/primary/PrimaryComponent.h b/es-core/src/components/primary/PrimaryComponent.h index 03ad7c7a9..7d28a0c92 100644 --- a/es-core/src/components/primary/PrimaryComponent.h +++ b/es-core/src/components/primary/PrimaryComponent.h @@ -14,6 +14,7 @@ template class PrimaryComponent : public virtual GuiComponent public: enum class PrimaryType { CAROUSEL, + GRID, TEXTLIST }; diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index c64f8c614..cadfc08e7 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -705,7 +705,7 @@ template void TextListComponent::onCursorChanged(const CursorSta float posMax {static_cast(mEntries.size())}; float endPos {static_cast(mCursor)}; - float animTime {380}; + float animTime {380.0f}; float timeDiff {1.0f}; // If startPos is inbetween two positions then reduce the time slightly as the distance will @@ -717,7 +717,7 @@ template void TextListComponent::onCursorChanged(const CursorSta if (timeDiff != 1.0f) animTime = - glm::clamp(std::fabs(glm::mix(0.0f, 380.0f, timeDiff * 1.5f)), 200.0f, 380.0f); + 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) {