From 3a1c9d41ce40ca5826711cde521587eab3bc1624 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Thu, 24 Mar 2022 23:05:23 +0100 Subject: [PATCH] Fully generalized SystemView and GamelistView and rewrote CarouselComponent into a template class. Also cleaned up some code and fixed an issue where navigation sounds would not play when using the shoulder buttons. --- es-app/CMakeLists.txt | 8 - es-app/src/Screensaver.cpp | 10 +- es-app/src/components/CarouselComponent.h | 96 ---- es-app/src/components/TextListComponent.cpp | 9 - es-app/src/guis/GuiMenu.cpp | 9 +- es-app/src/guis/GuiScraperSearch.cpp | 2 +- es-app/src/views/GamelistBase.cpp | 164 ++++--- es-app/src/views/GamelistBase.h | 24 +- es-app/src/views/GamelistLegacy.h | 42 +- es-app/src/views/GamelistView.cpp | 96 +++- es-app/src/views/GamelistView.h | 6 +- es-app/src/views/SystemView.cpp | 283 ++++++++---- es-app/src/views/SystemView.h | 77 ++-- es-app/src/views/ViewController.cpp | 37 +- es-core/CMakeLists.txt | 9 +- es-core/src/Window.cpp | 2 +- es-core/src/components/ComponentList.cpp | 4 +- .../src/components/GameSelectorComponent.h | 1 + es-core/src/components/IList.h | 147 +++--- .../components/primary/CarouselComponent.h | 337 +++++++++++--- .../src/components/primary/PrimaryComponent.h | 50 ++ .../components/primary}/TextListComponent.h | 430 ++++++++++-------- 22 files changed, 1135 insertions(+), 708 deletions(-) delete mode 100644 es-app/src/components/CarouselComponent.h delete mode 100644 es-app/src/components/TextListComponent.cpp rename es-app/src/components/CarouselComponent.cpp => es-core/src/components/primary/CarouselComponent.h (62%) create mode 100644 es-core/src/components/primary/PrimaryComponent.h rename {es-app/src/components => es-core/src/components/primary}/TextListComponent.h (72%) diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 159fc44c8..7018fa207 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -25,10 +25,6 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.h ${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.h - # Primary GUI components - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/CarouselComponent.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.h - # GUIs ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h @@ -76,10 +72,6 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.cpp - # Primary GUI components - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/CarouselComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.cpp - # GUIs ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp diff --git a/es-app/src/Screensaver.cpp b/es-app/src/Screensaver.cpp index 4b13d762f..e5270df50 100644 --- a/es-app/src/Screensaver.cpp +++ b/es-app/src/Screensaver.cpp @@ -215,9 +215,10 @@ void Screensaver::launchGame() // Launching game ViewController::getInstance()->triggerGameLaunch(mCurrentGame); ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem()); - GamelistView* view = - ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get(); + GamelistView* view { + ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get()}; view->setCursor(mCurrentGame); + view->stopListScrolling(); ViewController::getInstance()->cancelViewTransitions(); ViewController::getInstance()->pauseViewVideos(); } @@ -228,9 +229,10 @@ void Screensaver::goToGame() if (mCurrentGame != nullptr) { // Go to the game in the gamelist view, but don't launch it. ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem()); - GamelistView* view = - ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get(); + GamelistView* view { + ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get()}; view->setCursor(mCurrentGame); + view->stopListScrolling(); ViewController::getInstance()->cancelViewTransitions(); } } diff --git a/es-app/src/components/CarouselComponent.h b/es-app/src/components/CarouselComponent.h deleted file mode 100644 index e3d336fdc..000000000 --- a/es-app/src/components/CarouselComponent.h +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// EmulationStation Desktop Edition -// CarouselComponent.h -// -// Carousel. -// - -#include "GuiComponent.h" -#include "Sound.h" -#include "components/IList.h" -#include "components/ImageComponent.h" -#include "components/TextComponent.h" -#include "resources/Font.h" - -class SystemData; - -#ifndef ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H -#define ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H - -struct CarouselElement { - std::shared_ptr logo; - std::string logoPath; - std::string defaultLogoPath; -}; - -class CarouselComponent : public IList -{ -public: - CarouselComponent(); - void addEntry(const std::shared_ptr& theme, Entry& entry, bool legacyMode); - Entry& getEntry(int index) { return mEntries.at(index); } - - enum CarouselType { - HORIZONTAL, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). - VERTICAL, - VERTICAL_WHEEL, - HORIZONTAL_WHEEL - }; - - int getCursor() { return mCursor; } - const CarouselType getType() { return mType; } - size_t getNumEntries() { return mEntries.size(); } - - void setCursorChangedCallback(const std::function& func) - { - mCursorChangedCallback = func; - } - void setCancelTransitionsCallback(const std::function& func) - { - mCancelTransitionsCallback = 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; - -protected: - void onCursorChanged(const CursorState& state) override; - void onScroll() override - { - NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND); - } - -private: - Renderer* mRenderer; - std::function mCursorChangedCallback; - std::function mCancelTransitionsCallback; - - float mCamOffset; - int mPreviousScrollVelocity; - - CarouselType mType; - std::shared_ptr mFont; - unsigned int mTextColor; - unsigned int mTextBackgroundColor; - std::string mText; - float mLineSpacing; - Alignment mLogoHorizontalAlignment; - Alignment mLogoVerticalAlignment; - float mMaxLogoCount; - glm::vec2 mLogoSize; - float mLogoScale; - float mLogoRotation; - glm::vec2 mLogoRotationOrigin; - unsigned int mCarouselColor; - unsigned int mCarouselColorEnd; - bool mColorGradientHorizontal; -}; - -#endif // ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H diff --git a/es-app/src/components/TextListComponent.cpp b/es-app/src/components/TextListComponent.cpp deleted file mode 100644 index 9ff694dd4..000000000 --- a/es-app/src/components/TextListComponent.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// EmulationStation Desktop Edition -// TextListComponent.cpp -// -// Text list used for displaying and navigating the gamelist views. -// - -#include "components/TextListComponent.h" diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 69e9e082e..4e67dfff4 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -121,17 +121,18 @@ void GuiMenu::openUIOptions() Scripting::fireEvent("theme-changed", theme_set->getSelected(), Settings::getInstance()->getString("ThemeSet")); Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - CollectionSystemsManager::getInstance()->updateSystemsList(); mWindow->setChangedThemeSet(); // This is required so that the custom collection system does not disappear // if the user is editing a custom collection when switching theme sets. - if (CollectionSystemsManager::getInstance()->isEditing()) { + if (CollectionSystemsManager::getInstance()->isEditing()) CollectionSystemsManager::getInstance()->exitEditMode(); - s->setNeedsCollectionsUpdate(); - } + // TODO: Eliminate this extra reload or only execute it when switching from + // a legacy theme to a non-legacy theme. + ViewController::getInstance()->reloadAll(); s->setNeedsSaving(); s->setNeedsReloading(); s->setNeedsGoToStart(); + s->setNeedsCollectionsUpdate(); s->setInvalidateCachedBackground(); } }); diff --git a/es-app/src/guis/GuiScraperSearch.cpp b/es-app/src/guis/GuiScraperSearch.cpp index 3ce973a41..d8fda64fc 100644 --- a/es-app/src/guis/GuiScraperSearch.cpp +++ b/es-app/src/guis/GuiScraperSearch.cpp @@ -130,7 +130,7 @@ GuiScraperSearch::GuiScraperSearch(SearchType type, unsigned int scrapeCount) // Result list. mResultList = std::make_shared(); mResultList->setCursorChangedCallback([this](CursorState state) { - if (state == CURSOR_STOPPED) + if (state == CursorState::CURSOR_STOPPED) updateInfoPane(); }); diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index 93bca1fe6..47a4b73ce 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -16,6 +16,7 @@ GamelistBase::GamelistBase(FileData* root) : mRoot {root} + , mPrimary {nullptr} , mRandomGame {nullptr} , mLastUpdated {nullptr} , mGameCount {0} @@ -25,33 +26,22 @@ GamelistBase::GamelistBase(FileData* root) , mIsFiltered {false} , mIsFolder {false} , mVideoPlaying {false} + , mLeftRightAvailable {true} { setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); - - mList.setSize(mSize.x, mSize.y * 0.8f); - mList.setPosition(0.0f, mSize.y * 0.2f); - mList.setDefaultZIndex(20.0f); - addChild(&mList); - - populateList(root->getChildrenListToDisplay(), root); -} - -GamelistBase::~GamelistBase() -{ - // } void GamelistBase::setCursor(FileData* cursor) { - if (!mList.setCursor(cursor) && (!cursor->isPlaceHolder())) { + if (!mPrimary->setCursor(cursor) && (!cursor->isPlaceHolder())) { populateList(cursor->getParent()->getChildrenListToDisplay(), cursor->getParent()); - mList.setCursor(cursor); + mPrimary->setCursor(cursor); // Update our cursor stack in case our cursor just got set to some folder // we weren't in before. if (mCursorStack.empty() || mCursorStack.top() != cursor->getParent()) { std::stack tmp; - FileData* ptr = cursor->getParent(); + FileData* ptr {cursor->getParent()}; while (ptr && ptr != mRoot) { tmp.push(ptr); @@ -72,7 +62,7 @@ bool GamelistBase::input(InputConfig* config, Input input) { if (input.value != 0) { if (config->isMappedTo("a", input)) { - FileData* cursor = getCursor(); + FileData* cursor {getCursor()}; if (cursor->getType() == GAME) { pauseViewVideos(); ViewController::getInstance()->cancelViewTransitions(); @@ -87,7 +77,7 @@ bool GamelistBase::input(InputConfig* config, Input input) mCursorStack.push(cursor); populateList(cursor->getChildrenListToDisplay(), cursor); - FileData* newCursor = nullptr; + FileData* newCursor {nullptr}; std::vector listEntries = cursor->getChildrenListToDisplay(); // Check if there is an entry in the cursor stack history matching any entry // in the currect folder. If so, select that entry. @@ -105,6 +95,7 @@ bool GamelistBase::input(InputConfig* config, Input input) if (!newCursor) newCursor = getCursor(); setCursor(newCursor); + stopListScrolling(); if (mRoot->getSystem()->getThemeFolder() == "custom-collections") updateHelpPrompts(); } @@ -124,6 +115,7 @@ bool GamelistBase::input(InputConfig* config, Input input) populateList(mCursorStack.top()->getParent()->getChildrenListToDisplay(), mCursorStack.top()->getParent()); setCursor(mCursorStack.top()); + stopListScrolling(); if (mCursorStack.size() > 0) mCursorStack.pop(); if (mRoot->getSystem()->getThemeFolder() == "custom-collections") @@ -173,7 +165,7 @@ bool GamelistBase::input(InputConfig* config, Input input) } } else if (config->isMappedLike(getQuickSystemSelectRightButton(), input)) { - if (Settings::getInstance()->getBool("QuickSystemSelect") && + if (mLeftRightAvailable && Settings::getInstance()->getBool("QuickSystemSelect") && SystemData::sSystemVector.size() > 1) { muteViewVideos(); onFocusLost(); @@ -183,7 +175,7 @@ bool GamelistBase::input(InputConfig* config, Input input) } } else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input)) { - if (Settings::getInstance()->getBool("QuickSystemSelect") && + if (mLeftRightAvailable && Settings::getInstance()->getBool("QuickSystemSelect") && SystemData::sSystemVector.size() > 1) { muteViewVideos(); onFocusLost(); @@ -199,7 +191,7 @@ bool GamelistBase::input(InputConfig* config, Input input) stopListScrolling(); // Jump to a random game. NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); - FileData* randomGame = getCursor()->getSystem()->getRandomGame(getCursor()); + FileData* randomGame {getCursor()->getSystem()->getRandomGame(getCursor())}; if (randomGame) setCursor(randomGame); return true; @@ -214,8 +206,8 @@ bool GamelistBase::input(InputConfig* config, Input input) NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND); // If there is already an mCursorStackHistory entry for the collection, then // remove it so we don't get multiple entries. - std::vector listEntries = - mRandomGame->getSystem()->getRootFolder()->getChildrenListToDisplay(); + std::vector listEntries { + mRandomGame->getSystem()->getRootFolder()->getChildrenListToDisplay()}; for (auto it = mCursorStackHistory.begin(); it != mCursorStackHistory.end(); ++it) { if (std::find(listEntries.begin(), listEntries.end(), *it) != listEntries.end()) { @@ -224,6 +216,7 @@ bool GamelistBase::input(InputConfig* config, Input input) } } setCursor(mRandomGame); + stopListScrolling(); updateHelpPrompts(); } else { @@ -258,13 +251,13 @@ bool GamelistBase::input(InputConfig* config, Input input) // When marking or unmarking a game as favorite, don't jump to the new position // it gets after the gamelist sorting. Instead retain the cursor position in the // list using the logic below. - FileData* entryToUpdate = getCursor(); - SystemData* system = getCursor()->getSystem(); + FileData* entryToUpdate {getCursor()}; + SystemData* system {getCursor()->getSystem()}; bool favoritesSorting; - bool removedLastFavorite = false; - bool selectLastEntry = false; - bool isEditing = CollectionSystemsManager::getInstance()->isEditing(); - bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop"); + bool removedLastFavorite {false}; + bool selectLastEntry {false}; + bool isEditing {CollectionSystemsManager::getInstance()->isEditing()}; + bool foldersOnTop {Settings::getInstance()->getBool("FoldersOnTop")}; // If the current list only contains folders, then treat it as if the folders // are not sorted on top, this way the logic should work exactly as for mixed // lists or files-only lists. @@ -412,8 +405,8 @@ bool GamelistBase::input(InputConfig* config, Input input) // As the toggling of the game destroyed this object, we need to get the view // from ViewController instead of using the reference that existed before the // destruction. Otherwise we get random crashes. - GamelistView* view = - ViewController::getInstance()->getGamelistView(system).get(); + GamelistView* view { + ViewController::getInstance()->getGamelistView(system).get()}; // Jump to the first entry in the gamelist if the last favorite was unmarked. if (foldersOnTop && removedLastFavorite && !entryToUpdate->getSystem()->isCustomCollection()) { @@ -428,7 +421,7 @@ bool GamelistBase::input(InputConfig* config, Input input) setCursor(getFirstEntry()); view->setCursor(view->getFirstEntry()); } - else if (selectLastEntry && mList.size() > 0) { + else if (selectLastEntry && mPrimary->size() > 0) { setCursor(getLastEntry()); view->setCursor(view->getLastEntry()); } @@ -498,45 +491,65 @@ void GamelistBase::populateList(const std::vector& files, FileData* f favoriteStar = Settings::getInstance()->getBool("FavoritesStar"); } - mList.clear(); + if (mPrimary != nullptr) + mPrimary->clear(); + + auto theme = mRoot->getSystem()->getTheme(); + std::string name; + unsigned int color {0}; if (files.size() > 0) { for (auto it = files.cbegin(); it != files.cend(); ++it) { - if (!mFirstGameEntry && (*it)->getType() == GAME) - mFirstGameEntry = (*it); - // Add a leading tick mark icon to the game name if it's part of the custom collection - // currently being edited. - if (isEditing && (*it)->getType() == GAME) { - if (CollectionSystemsManager::getInstance()->inCustomCollection(editingCollection, - (*it))) { - if (Settings::getInstance()->getBool("SpecialCharsASCII")) - inCollectionPrefix = "! "; - else - inCollectionPrefix = ViewController::TICKMARK_CHAR + " "; - } - else { - inCollectionPrefix = ""; - } + if (mCarousel != nullptr) { + CarouselComponent::Entry carouselEntry; + carouselEntry.name = (*it)->getName(); + carouselEntry.object = *it; + carouselEntry.data.logoPath = (*it)->getMarqueePath(); + mCarousel->addEntry(carouselEntry, theme); } - if ((*it)->getFavorite() && favoriteStar && - mRoot->getSystem()->getName() != "favorites") { - if (Settings::getInstance()->getBool("SpecialCharsASCII")) - mList.add(inCollectionPrefix + "* " + (*it)->getName(), *it, - ((*it)->getType() == FOLDER)); - else - mList.add(inCollectionPrefix + ViewController::FAVORITE_CHAR + " " + - (*it)->getName(), - *it, ((*it)->getType() == FOLDER)); - } - else if ((*it)->getType() == FOLDER && mRoot->getSystem()->getName() != "collections") { - if (Settings::getInstance()->getBool("SpecialCharsASCII")) - mList.add("# " + (*it)->getName(), *it, true); - else - mList.add(ViewController::FOLDER_CHAR + " " + (*it)->getName(), *it, true); - } - else { - mList.add(inCollectionPrefix + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (mTextList != nullptr) { + TextListComponent::Entry textListEntry; + if (!mFirstGameEntry && (*it)->getType() == GAME) + mFirstGameEntry = (*it); + // Add a leading tick mark icon to the game name if it's part of the custom + // collection currently being edited. + if (isEditing && (*it)->getType() == GAME) { + if (CollectionSystemsManager::getInstance()->inCustomCollection( + editingCollection, (*it))) { + if (Settings::getInstance()->getBool("SpecialCharsASCII")) + inCollectionPrefix = "! "; + else + inCollectionPrefix = ViewController::TICKMARK_CHAR + " "; + } + else { + inCollectionPrefix = ""; + } + } + + if ((*it)->getFavorite() && favoriteStar && + mRoot->getSystem()->getName() != "favorites") { + if (Settings::getInstance()->getBool("SpecialCharsASCII")) + name = inCollectionPrefix + "* " + (*it)->getName(); + else + name = inCollectionPrefix + ViewController::FAVORITE_CHAR + " " + + (*it)->getName(); + } + else if ((*it)->getType() == FOLDER && + mRoot->getSystem()->getName() != "collections") { + if (Settings::getInstance()->getBool("SpecialCharsASCII")) + name = "# " + (*it)->getName(); + else + name = ViewController::FOLDER_CHAR + " " + (*it)->getName(); + } + else { + name = inCollectionPrefix + (*it)->getName(); + } + color = (*it)->getType() == FOLDER; + textListEntry.name = name; + textListEntry.object = *it; + textListEntry.data.colorId = color; + mTextList->addEntry(textListEntry); } } } @@ -551,14 +564,26 @@ void GamelistBase::populateList(const std::vector& files, FileData* f void GamelistBase::addPlaceholder(FileData* firstEntry) { // Empty list, add a placeholder. - FileData* placeholder; + FileData* placeholder {nullptr}; if (firstEntry && firstEntry->getSystem()->isGroupedCustomCollection()) placeholder = firstEntry->getSystem()->getPlaceholder(); else placeholder = this->mRoot->getSystem()->getPlaceholder(); - mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER)); + if (mTextList != nullptr) { + TextListComponent::Entry textListEntry; + textListEntry.name = placeholder->getName(); + textListEntry.object = placeholder; + textListEntry.data.colorId = 1; + mTextList->addEntry(textListEntry); + } + if (mCarousel != nullptr) { + CarouselComponent::Entry carouselEntry; + carouselEntry.name = placeholder->getName(); + carouselEntry.object = placeholder; + mCarousel->addEntry(carouselEntry, mRoot->getSystem()->getTheme()); + } } void GamelistBase::generateFirstLetterIndex(const std::vector& files) @@ -679,9 +704,10 @@ void GamelistBase::remove(FileData* game, bool deleteFile) setCursor(siblings.at(gamePos - 1)); } } - mList.remove(game); - if (mList.size() == 0) + mPrimary->remove(game); + + if (mPrimary->size() == 0) addPlaceholder(nullptr); // If a game has been deleted, immediately remove the entry from gamelist.xml diff --git a/es-app/src/views/GamelistBase.h b/es-app/src/views/GamelistBase.h index 8064d7dfe..c9341c3f5 100644 --- a/es-app/src/views/GamelistBase.h +++ b/es-app/src/views/GamelistBase.h @@ -21,23 +21,24 @@ #include "components/RatingComponent.h" #include "components/ScrollableContainer.h" #include "components/TextComponent.h" -#include "components/TextListComponent.h" #include "components/VideoFFmpegComponent.h" +#include "components/primary/CarouselComponent.h" +#include "components/primary/TextListComponent.h" #include class GamelistBase : public GuiComponent { public: - FileData* getCursor() { return mList.getSelected(); } + FileData* getCursor() { return mPrimary->getSelected(); } void setCursor(FileData*); bool input(InputConfig* config, Input input) override; - FileData* getNextEntry() { return mList.getNext(); } - FileData* getPreviousEntry() { return mList.getPrevious(); } - FileData* getFirstEntry() { return mList.getFirst(); } - FileData* getLastEntry() { return mList.getLast(); } + FileData* getNextEntry() { return mPrimary->getNext(); } + FileData* getPreviousEntry() { return mPrimary->getPrevious(); } + FileData* getFirstEntry() { return mPrimary->getFirst(); } + FileData* getLastEntry() { return mPrimary->getLast(); } FileData* getFirstGameEntry() { return mFirstGameEntry; } // These functions are used to retain the folder cursor history, for instance @@ -58,9 +59,10 @@ public: const std::vector& getFirstLetterIndex() { return mFirstLetterIndex; } + void stopListScrolling() override { mPrimary->stopScrolling(); } + protected: GamelistBase(FileData* root); - ~GamelistBase(); // Called when a FileData* is added, has its metadata changed, or is removed. virtual void onFileChanged(FileData* file, bool reloadGamelist) = 0; @@ -72,14 +74,15 @@ protected: virtual void launch(FileData* game) = 0; - bool isListScrolling() override { return mList.isScrolling(); } - void stopListScrolling() override { mList.stopScrolling(); } + bool isListScrolling() override { return mPrimary->isScrolling(); } std::string getQuickSystemSelectRightButton() { return "right"; } std::string getQuickSystemSelectLeftButton() { return "left"; } FileData* mRoot; - TextListComponent mList; + std::unique_ptr> mCarousel; + std::unique_ptr> mTextList; + PrimaryComponent* mPrimary; // Points to the first game in the list, i.e. the first entry which is of the type "GAME". FileData* mFirstGameEntry; @@ -100,6 +103,7 @@ protected: bool mIsFiltered; bool mIsFolder; bool mVideoPlaying; + bool mLeftRightAvailable; private: }; diff --git a/es-app/src/views/GamelistLegacy.h b/es-app/src/views/GamelistLegacy.h index 04a740d19..6ffbc1fdf 100644 --- a/es-app/src/views/GamelistLegacy.h +++ b/es-app/src/views/GamelistLegacy.h @@ -67,7 +67,7 @@ void GamelistView::legacyPopulateFields() mImageComponents.back()->setThemeMetadata("image_md_image"); mImageComponents.back()->setOrigin(0.5f, 0.5f); mImageComponents.back()->setPosition(mSize.x * 0.25f, - mList.getPosition().y + mSize.y * 0.2125f); + mPrimary->getPosition().y + mSize.y * 0.2125f); mImageComponents.back()->setMaxSize(mSize.x * (0.50f - 2.0f * padding), mSize.y * 0.4f); mImageComponents.back()->setDefaultZIndex(30.0f); mImageComponents.back()->setScrollFadeIn(true); @@ -79,7 +79,7 @@ void GamelistView::legacyPopulateFields() mVideoComponents.back()->setThemeMetadata("video_md_video"); mVideoComponents.back()->setOrigin(0.5f, 0.5f); mVideoComponents.back()->setPosition(mSize.x * 0.25f, - mList.getPosition().y + mSize.y * 0.2125f); + mPrimary->getPosition().y + mSize.y * 0.2125f); mVideoComponents.back()->setSize(mSize.x * (0.5f - 2.0f * padding), mSize.y * 0.4f); mVideoComponents.back()->setDefaultZIndex(30.0f); mVideoComponents.back()->setScrollFadeIn(true); @@ -87,10 +87,11 @@ void GamelistView::legacyPopulateFields() addChild(mVideoComponents.back().get()); } - mList.setPosition(mSize.x * (0.50f + padding), mList.getPosition().y); - mList.setSize(mSize.x * (0.50f - padding), mList.getSize().y); - mList.setAlignment(TextListComponent::ALIGN_LEFT); - mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); + mPrimary->setPosition(mSize.x * (0.50f + padding), mPrimary->getPosition().y); + mPrimary->setSize(mSize.x * (0.50f - padding), mPrimary->getSize().y); + mPrimary->setAlignment(TextListComponent::PrimaryAlignment::ALIGN_LEFT); + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { legacyUpdateInfoPanel(state); }); // Metadata labels + values. mTextComponents.push_back(std::make_unique()); @@ -210,6 +211,11 @@ void GamelistView::legacyPopulateFields() void GamelistView::legacyOnThemeChanged(const std::shared_ptr& theme) { + if (mTextList == nullptr) { + mTextList = std::make_unique>(); + mPrimary = mTextList.get(); + } + legacyPopulateFields(); using namespace ThemeFlags; @@ -230,7 +236,11 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr& theme) for (auto extra : mThemeExtras) addChild(extra); - mList.applyTheme(theme, getName(), "textlist_gamelist", ALL); + mPrimary->setSize(mSize.x, mSize.y * 0.8f); + mPrimary->setPosition(0.0f, mSize.y * 0.2f); + mPrimary->setDefaultZIndex(50.0f); + mPrimary->applyTheme(theme, getName(), "textlist_gamelist", ALL); + addChild(mPrimary); mImageComponents[LegacyImage::MD_THUMBNAIL]->applyTheme( theme, getName(), mImageComponents[LegacyImage::MD_THUMBNAIL]->getThemeMetadata(), ALL); @@ -306,19 +316,22 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr& theme) container->setVisible(false); } + populateList(mRoot->getChildrenListToDisplay(), mRoot); sortChildren(); mHelpStyle.applyTheme(mTheme, getName()); } -void GamelistView::legacyUpdateInfoPanel() +void GamelistView::legacyUpdateInfoPanel(const CursorState& state) { - FileData* file {(mList.size() == 0 || mList.isScrolling()) ? nullptr : mList.getSelected()}; + FileData* file {(mPrimary->size() > 0 && state == CursorState::CURSOR_STOPPED) ? + mPrimary->getSelected() : + nullptr}; // If the game data has already been rendered to the info panel, then skip it this time. if (file == mLastUpdated) return; - if (!mList.isScrolling()) + if (state == CursorState::CURSOR_STOPPED) mLastUpdated = file; bool hideMetaDataFields {false}; @@ -340,7 +353,7 @@ void GamelistView::legacyUpdateInfoPanel() // If we're scrolling, hide the metadata fields if the last game had this options set, // or if we're in the grouped custom collection view. - if (mList.isScrolling()) { + if (mPrimary->isScrolling()) { if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") || (mLastUpdated->getSystem()->isCustomCollection() && mLastUpdated->getPath() == mLastUpdated->getSystem()->getName())) @@ -566,10 +579,6 @@ void GamelistView::legacyUpdateInfoPanel() for (auto it = comps.cbegin(); it != comps.cend(); ++it) { GuiComponent* comp {*it}; - if (!fadingOut && !comp->isAnimationPlaying(0)) { - comp->setOpacity(1.0f); - continue; - } // An animation is playing, then animate if reverse != fadingOut. // An animation is not playing, then animate if opacity != our target opacity. if ((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || @@ -578,6 +587,9 @@ void GamelistView::legacyUpdateInfoPanel() comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); } } + + if (state == CursorState::CURSOR_SCROLLING) + mLastUpdated = nullptr; } void GamelistView::legacyUpdate(int deltaTime) diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index ebd585a93..3cd7b81e4 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -26,13 +26,6 @@ GamelistView::GamelistView(FileData* root) if (mLegacyMode) return; - - const float padding {0.01f}; - - mList.setPosition(mSize.x * (0.50f + padding), mList.getPosition().y); - mList.setSize(mSize.x * (0.50f - padding), mList.getSize().y); - mList.setAlignment(TextListComponent::ALIGN_LEFT); - mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); } GamelistView::~GamelistView() @@ -79,9 +72,11 @@ void GamelistView::onShow() GuiComponent::onShow(); if (mLegacyMode) - legacyUpdateInfoPanel(); + legacyUpdateInfoPanel(CursorState::CURSOR_STOPPED); else - updateInfoPanel(); + updateInfoPanel(CursorState::CURSOR_STOPPED); + + mPrimary->finishAnimation(0); } void GamelistView::onTransition() @@ -111,6 +106,44 @@ 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) { + LOG(LogWarning) << "SystemView::populate(): Multiple primary components " + << "defined, skipping configuration entry"; + continue; + } + if (element.second.type == "textlist" && mCarousel != nullptr) { + LOG(LogWarning) << "SystemView::populate(): Multiple primary components " + << "defined, skipping configuration entry"; + continue; + } + } + if (element.second.type == "textlist") { + if (mTextList == nullptr) { + mTextList = std::make_unique>(); + mPrimary = mTextList.get(); + } + mPrimary->setPosition(0.2f, mSize.y * 0.2f); + mPrimary->setSize(mSize.x * 0.7f, mSize.y * 0.6f); + mPrimary->setAlignment(TextListComponent::PrimaryAlignment::ALIGN_LEFT); + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { updateInfoPanel(state); }); + mPrimary->setDefaultZIndex(50.0f); + mPrimary->setZIndex(50.0f); + mPrimary->applyTheme(theme, "gamelist", element.first, ALL); + addChild(mPrimary); + } + if (element.second.type == "carousel") { + if (mCarousel == nullptr) { + mCarousel = std::make_unique>(); + mPrimary = mCarousel.get(); + } + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { updateInfoPanel(state); }); + mPrimary->setDefaultZIndex(50.0f); + mPrimary->applyTheme(theme, "gamelist", element.first, ALL); + addChild(mPrimary); + } if (element.second.type == "image") { mImageComponents.push_back(std::make_unique()); mImageComponents.back()->setDefaultZIndex(30.0f); @@ -211,11 +244,31 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) addChild(mRatingComponents.back().get()); } } + + if (mPrimary == nullptr) { + mTextList = std::make_unique>(); + mPrimary = mTextList.get(); + mPrimary->setPosition(0.2f, mSize.y * 0.2f); + mPrimary->setSize(mSize.x * 0.7f, mSize.y * 0.6f); + mPrimary->setAlignment(TextListComponent::PrimaryAlignment::ALIGN_LEFT); + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { updateInfoPanel(state); }); + mPrimary->setDefaultZIndex(50.0f); + mPrimary->setZIndex(50.0f); + mPrimary->applyTheme(theme, "gamelist", "", ALL); + addChild(mPrimary); + } + mHelpStyle.applyTheme(mTheme, "gamelist"); + populateList(mRoot->getChildrenListToDisplay(), mRoot); } - mList.setDefaultZIndex(50.0f); - mList.applyTheme(theme, "gamelist", "textlist_gamelist", ALL); + // Disable quick system select if the primary component uses the left and right buttons. + if (mCarousel != nullptr) { + if (mCarousel->getType() == CarouselComponent::CarouselType::HORIZONTAL || + mCarousel->getType() == CarouselComponent::CarouselType::HORIZONTAL_WHEEL) + mLeftRightAvailable = false; + } sortChildren(); } @@ -259,7 +312,7 @@ std::vector GamelistView::getHelpPrompts() std::vector prompts; if (Settings::getInstance()->getBool("QuickSystemSelect") && - SystemData::sSystemVector.size() > 1) + SystemData::sSystemVector.size() > 1 && mLeftRightAvailable) prompts.push_back(HelpPrompt("left/right", "system")); if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && mCursorStack.empty() && @@ -301,20 +354,22 @@ std::vector GamelistView::getHelpPrompts() return prompts; } -void GamelistView::updateInfoPanel() +void GamelistView::updateInfoPanel(const CursorState& state) { if (mLegacyMode) { - legacyUpdateInfoPanel(); + legacyUpdateInfoPanel(state); return; } - FileData* file {(mList.size() == 0 || mList.isScrolling()) ? nullptr : mList.getSelected()}; + FileData* file {(mPrimary->size() > 0 && state == CursorState::CURSOR_STOPPED) ? + mPrimary->getSelected() : + nullptr}; // If the game data has already been rendered to the info panel, then skip it this time. if (file == mLastUpdated) return; - if (!mList.isScrolling()) + if (state == CursorState::CURSOR_STOPPED) mLastUpdated = file; bool hideMetaDataFields {false}; @@ -336,7 +391,7 @@ void GamelistView::updateInfoPanel() // If we're scrolling, hide the metadata fields if the last game had this options set, // or if we're in the grouped custom collection view. - if (mList.isScrolling()) { + if (state == CursorState::CURSOR_SCROLLING) { if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") || (mLastUpdated->getSystem()->isCustomCollection() && mLastUpdated->getPath() == mLastUpdated->getSystem()->getName())) @@ -686,10 +741,6 @@ void GamelistView::updateInfoPanel() for (auto it = comps.cbegin(); it != comps.cend(); ++it) { GuiComponent* comp {*it}; - if (!fadingOut && !comp->isAnimationPlaying(0)) { - comp->setOpacity(1.0f); - continue; - } // An animation is playing, then animate if reverse != fadingOut. // An animation is not playing, then animate if opacity != our target opacity. if ((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || @@ -698,6 +749,9 @@ void GamelistView::updateInfoPanel() comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); } } + + if (state == CursorState::CURSOR_SCROLLING) + mLastUpdated = nullptr; } void GamelistView::setGameImage(FileData* file, GuiComponent* comp) diff --git a/es-app/src/views/GamelistView.h b/es-app/src/views/GamelistView.h index dd99375f5..dd81fd584 100644 --- a/es-app/src/views/GamelistView.h +++ b/es-app/src/views/GamelistView.h @@ -25,7 +25,7 @@ public: void onShow() override; void onTransition() override; - void preloadGamelist() { updateInfoPanel(); } + void preloadGamelist() { updateInfoPanel(CursorState::CURSOR_STOPPED); } void launch(FileData* game) override { ViewController::getInstance()->triggerGameLaunch(game); } std::string getName() const @@ -91,13 +91,13 @@ public: std::vector getHelpPrompts() override; private: - void updateInfoPanel(); + void updateInfoPanel(const CursorState& state); void setGameImage(FileData* file, GuiComponent* comp); // Legacy (backward compatibility) functions. void legacyPopulateFields(); void legacyOnThemeChanged(const std::shared_ptr& theme); - void legacyUpdateInfoPanel(); + void legacyUpdateInfoPanel(const CursorState& state); void legacyUpdate(int deltaTime); void legacyInitMDLabels(); void legacyInitMDValues(); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 4c29629bc..746eb5ec6 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -23,6 +23,8 @@ SystemView::SystemView() : mRenderer {Renderer::getInstance()} + , mPrimary {nullptr} + , mPrimaryType {PrimaryType::CAROUSEL} , mCamOffset {0.0f} , mFadeOpacity {0.0f} , mPreviousScrollVelocity {0} @@ -34,20 +36,6 @@ SystemView::SystemView() , mFadeTransitions {false} { setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); - - mCarousel = std::make_unique(); - mCarousel->setCursorChangedCallback([&](const CursorState& state) { onCursorChanged(state); }); - mCarousel->setCancelTransitionsCallback([&] { - ViewController::getInstance()->cancelViewTransitions(); - mNavigated = true; - if (mSystemElements.size() > 1) { - for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) - anim->setPauseAnimation(true); - for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) - anim->setPauseAnimation(true); - } - }); - populate(); } @@ -65,29 +53,29 @@ SystemView::~SystemView() void SystemView::onTransition() { - for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) + for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents) anim->setPauseAnimation(true); - for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) + for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents) anim->setPauseAnimation(true); } void SystemView::goToSystem(SystemData* system, bool animate) { - mCarousel->setCursor(system); + mPrimary->setCursor(system); - for (auto& selector : mSystemElements[mCarousel->getCursor()].gameSelectors) { + for (auto& selector : mSystemElements[mPrimary->getCursor()].gameSelectors) { if (selector->getGameSelection() == GameSelectorComponent::GameSelection::RANDOM) selector->setNeedsRefresh(); } - for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) + for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) video->setStaticVideo(); - for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) + for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents) anim->resetFileAnimation(); - for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) + for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents) anim->resetFileAnimation(); updateGameSelectors(); @@ -111,9 +99,9 @@ bool SystemView::input(InputConfig* config, Input input) } if (config->isMappedTo("a", input)) { - mCarousel->stopScrolling(); + mPrimary->stopScrolling(); pauseViewVideos(); - ViewController::getInstance()->goToGamelist(mCarousel->getSelected()); + ViewController::getInstance()->goToGamelist(mPrimary->getSelected()); NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND); return true; } @@ -122,7 +110,7 @@ bool SystemView::input(InputConfig* config, Input input) config->isMappedTo("rightthumbstickclick", input))) { // Get a random system and jump to it. NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND); - mCarousel->setCursor(SystemData::getRandomSystem(mCarousel->getSelected())); + mPrimary->setCursor(SystemData::getRandomSystem(mPrimary->getSelected())); return true; } @@ -138,23 +126,23 @@ bool SystemView::input(InputConfig* config, Input input) } } - return mCarousel->input(config, input); + return mPrimary->input(config, input); } void SystemView::update(int deltaTime) { - if (!mCarousel->isAnimationPlaying(0)) + if (!mPrimary->isAnimationPlaying(0)) mMaxFade = false; - mCarousel->update(deltaTime); + mPrimary->update(deltaTime); - for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) + for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) video->update(deltaTime); - for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) + for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents) anim->update(deltaTime); - for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) + for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents) anim->update(deltaTime); GuiComponent::update(deltaTime); @@ -162,7 +150,7 @@ void SystemView::update(int deltaTime) void SystemView::render(const glm::mat4& parentTrans) { - if (mCarousel->getNumEntries() == 0) + if (mPrimary->getNumEntries() == 0) return; // Nothing to render. bool fade {false}; @@ -174,7 +162,7 @@ void SystemView::render(const glm::mat4& parentTrans) renderElements(parentTrans, false); glm::mat4 trans {getTransform() * parentTrans}; - mCarousel->render(trans); + mPrimary->render(trans); // For legacy themes the carousel is always rendered on top of all other elements. if (!mLegacyMode && !fade) @@ -191,11 +179,16 @@ void SystemView::onThemeChanged(const std::shared_ptr& /*theme*/) std::vector SystemView::getHelpPrompts() { std::vector prompts; - if (mCarousel->getType() == CarouselComponent::VERTICAL || - mCarousel->getType() == CarouselComponent::VERTICAL_WHEEL) + if (mCarousel != nullptr) { + if (mCarousel->getType() == CarouselComponent::CarouselType::VERTICAL || + mCarousel->getType() == CarouselComponent::CarouselType::VERTICAL_WHEEL) + prompts.push_back(HelpPrompt("up/down", "choose")); + else + prompts.push_back(HelpPrompt("left/right", "choose")); + } + else if (mTextList != nullptr) { prompts.push_back(HelpPrompt("up/down", "choose")); - else - prompts.push_back(HelpPrompt("left/right", "choose")); + } prompts.push_back(HelpPrompt("a", "select")); @@ -209,9 +202,9 @@ std::vector SystemView::getHelpPrompts() return prompts; } -void SystemView::onCursorChanged(const CursorState& /*state*/) +void SystemView::onCursorChanged(const CursorState& state) { - int cursor {mCarousel->getCursor()}; + int cursor {mPrimary->getCursor()}; for (auto& selector : mSystemElements[cursor].gameSelectors) { if (selector->getGameSelection() == GameSelectorComponent::GameSelection::RANDOM) @@ -221,34 +214,41 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) for (auto& video : mSystemElements[cursor].videoComponents) video->setStaticVideo(); - for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) + for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents) anim->resetFileAnimation(); - for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) + for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents) anim->resetFileAnimation(); updateGameSelectors(); startViewVideos(); updateHelpPrompts(); - int scrollVelocity {mCarousel->getScrollingVelocity()}; + int scrollVelocity {mPrimary->getScrollingVelocity()}; float startPos {mCamOffset}; - float posMax {static_cast(mCarousel->getNumEntries())}; + float posMax {static_cast(mPrimary->getNumEntries())}; float target {static_cast(cursor)}; + float endPos {target}; - // Find the shortest path to the target. - float endPos {target}; // Directly. - float dist {fabs(endPos - startPos)}; + if (mPreviousScrollVelocity > 0 && scrollVelocity == 0 && mCamOffset > posMax - 1.0f) + startPos = 0.0f; - if (fabs(target + posMax - startPos - scrollVelocity) < dist) - endPos = target + posMax; // Loop around the end (0 -> max). - if (fabs(target - posMax - startPos - scrollVelocity) < dist) - endPos = target - posMax; // Loop around the start (max - 1 -> -1). + if (mPrimaryType == PrimaryType::CAROUSEL) { + // Find the shortest path to the target. + float dist {fabs(endPos - startPos)}; + + if (fabs(target + posMax - startPos - scrollVelocity) < dist) + endPos = target + posMax; // Loop around the end (0 -> max). + if (fabs(target - posMax - startPos - scrollVelocity) < dist) + endPos = target - posMax; // Loop around the start (max - 1 -> -1). + } // Make sure transitions do not animate in reverse. bool changedDirection {false}; - if (mPreviousScrollVelocity != 0 && mPreviousScrollVelocity != scrollVelocity) - changedDirection = true; + if (mPreviousScrollVelocity != 0 && mPreviousScrollVelocity != scrollVelocity) { + if (scrollVelocity > 0 && startPos + scrollVelocity < posMax) + changedDirection = true; + } if (!changedDirection && scrollVelocity > 0 && endPos < startPos) endPos = endPos + posMax; @@ -374,8 +374,27 @@ void SystemView::populate() std::string logoPath; std::string defaultLogoPath; - if (mLegacyMode && mViewNeedsReload) + if (mLegacyMode && mViewNeedsReload) { + if (mCarousel == nullptr) { + mCarousel = std::make_unique>(); + mPrimary = mCarousel.get(); + mPrimary->setDefaultZIndex(50.0f); + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { onCursorChanged(state); }); + mPrimary->setCancelTransitionsCallback([&] { + ViewController::getInstance()->cancelViewTransitions(); + mNavigated = true; + if (mSystemElements.size() > 1) { + for (auto& anim : + mSystemElements[mPrimary->getCursor()].lottieAnimComponents) + anim->setPauseAnimation(true); + for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents) + anim->setPauseAnimation(true); + } + }); + } legacyApplyTheme(theme); + } if (mLegacyMode) { SystemViewElements elements; @@ -404,12 +423,51 @@ void SystemView::populate() ThemeFlags::ALL); elements.gameSelectors.back()->setNeedsRefresh(); } - if (element.second.type == "carousel") { - mCarousel->applyTheme(theme, "system", element.first, ThemeFlags::ALL); - if (element.second.has("logo")) - logoPath = element.second.get("logo"); - if (element.second.has("defaultLogo")) - defaultLogoPath = element.second.get("defaultLogo"); + if (element.second.type == "textlist" || element.second.type == "carousel") { + if (element.second.type == "carousel" && mTextList != nullptr) { + LOG(LogWarning) + << "SystemView::populate(): Multiple primary components " + << "defined, skipping configuration entry"; + continue; + } + if (element.second.type == "textlist" && mCarousel != nullptr) { + LOG(LogWarning) + << "SystemView::populate(): Multiple primary components " + << "defined, skipping configuration entry"; + continue; + } + if (element.second.type == "carousel" && mCarousel == nullptr) { + mCarousel = std::make_unique>(); + mPrimary = mCarousel.get(); + mPrimaryType = PrimaryType::CAROUSEL; + } + else if (element.second.type == "textlist" && mTextList == nullptr) { + mTextList = std::make_unique>(); + mPrimary = mTextList.get(); + mPrimaryType = PrimaryType::TEXTLIST; + } + mPrimary->setDefaultZIndex(50.0f); + mPrimary->applyTheme(theme, "system", element.first, ThemeFlags::ALL); + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { onCursorChanged(state); }); + mPrimary->setCancelTransitionsCallback([&] { + ViewController::getInstance()->cancelViewTransitions(); + mNavigated = true; + if (mSystemElements.size() > 1) { + for (auto& anim : + mSystemElements[mPrimary->getCursor()].lottieAnimComponents) + anim->setPauseAnimation(true); + for (auto& anim : + mSystemElements[mPrimary->getCursor()].GIFAnimComponents) + anim->setPauseAnimation(true); + } + }); + if (mCarousel != nullptr) { + if (element.second.has("logo")) + logoPath = element.second.get("logo"); + if (element.second.has("defaultLogo")) + defaultLogoPath = element.second.get("defaultLogo"); + } } else if (element.second.type == "image") { elements.imageComponents.emplace_back(std::make_unique()); @@ -500,10 +558,6 @@ void SystemView::populate() } } } - else { - // Apply default carousel configuration. - mCarousel->applyTheme(theme, "system", "", ThemeFlags::ALL); - } std::stable_sort( elements.children.begin(), elements.children.end(), @@ -523,13 +577,41 @@ void SystemView::populate() mSystemElements.back().helpStyle.applyTheme(theme, "system"); } - CarouselComponent::Entry entry; - entry.name = it->getName(); - entry.object = it; - entry.data.logoPath = logoPath; - entry.data.defaultLogoPath = defaultLogoPath; + if (mPrimary == nullptr) { + mCarousel = std::make_unique>(); + mPrimary = mCarousel.get(); + mPrimaryType = PrimaryType::CAROUSEL; + mPrimary->setDefaultZIndex(50.0f); + mPrimary->applyTheme(theme, "system", "", ThemeFlags::ALL); + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { onCursorChanged(state); }); + mPrimary->setCancelTransitionsCallback([&] { + ViewController::getInstance()->cancelViewTransitions(); + mNavigated = true; + if (mSystemElements.size() > 1) { + for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents) + anim->setPauseAnimation(true); + for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents) + anim->setPauseAnimation(true); + } + }); + } - mCarousel->addEntry(theme, entry, mLegacyMode); + if (mCarousel != nullptr) { + CarouselComponent::Entry entry; + entry.name = it->getName(); + entry.object = it; + entry.data.logoPath = logoPath; + entry.data.defaultLogoPath = defaultLogoPath; + mCarousel->addEntry(entry, theme); + } + if (mTextList != nullptr) { + TextListComponent::Entry entry; + entry.name = it->getFullName(); + entry.object = it; + entry.data.colorId = 0; + mTextList->addEntry(entry); + } } for (auto& elements : mSystemElements) { @@ -545,7 +627,7 @@ void SystemView::populate() } } - if (mCarousel->getNumEntries() == 0) { + if (mPrimary->getNumEntries() == 0) { // Something is wrong, there is not a single system to show, check if UI mode is not full. if (!UIModeController::getInstance()->isUIModeFull()) { Settings::getInstance()->setString("UIMode", "full"); @@ -562,21 +644,21 @@ void SystemView::populate() void SystemView::updateGameCount() { std::pair gameCount = - mCarousel->getSelected()->getDisplayedGameCount(); + mPrimary->getSelected()->getDisplayedGameCount(); std::stringstream ss; std::stringstream ssGames; std::stringstream ssFavorites; bool games {false}; - if (!mCarousel->getSelected()->isGameSystem()) { + if (!mPrimary->getSelected()->isGameSystem()) { ss << "Configuration"; } - else if (mCarousel->getSelected()->isCollection() && - (mCarousel->getSelected()->getName() == "favorites")) { + else if (mPrimary->getSelected()->isCollection() && + (mPrimary->getSelected()->getName() == "favorites")) { ss << gameCount.first << " Game" << (gameCount.first == 1 ? " " : "s"); } - else if (mCarousel->getSelected()->isCollection() && - (mCarousel->getSelected()->getName() == "recent")) { + else if (mPrimary->getSelected()->isCollection() && + (mPrimary->getSelected()->getName() == "recent")) { // The "recent" gamelist has probably been trimmed after sorting, so we'll cap it at // its maximum limit of 50 games. ss << (gameCount.first > 50 ? 50 : gameCount.first) << " Game" @@ -594,7 +676,7 @@ void SystemView::updateGameCount() mLegacySystemInfo->setText(ss.str()); } else { - for (auto& gameCount : mSystemElements[mCarousel->getCursor()].gameCountComponents) { + for (auto& gameCount : mSystemElements[mPrimary->getCursor()].gameCountComponents) { if (gameCount->getThemeSystemdata() == "gamecount") { gameCount->setValue(ss.str()); } @@ -619,7 +701,7 @@ void SystemView::updateGameSelectors() if (mLegacyMode) return; - int cursor {mCarousel->getCursor()}; + int cursor {mPrimary->getCursor()}; if (mSystemElements[cursor].gameSelectors.size() == 0) return; @@ -1052,10 +1134,13 @@ void SystemView::legacyApplyTheme(const std::shared_ptr& theme) else mViewNeedsReload = true; - mCarousel->applyTheme(theme, "system", "carousel_systemcarousel", ThemeFlags::ALL); + if (mCarousel != nullptr) + mPrimary->applyTheme(theme, "system", "carousel_systemcarousel", ThemeFlags::ALL); + else if (mTextList != nullptr) + mPrimary->applyTheme(theme, "system", "textlist_gamelist", ThemeFlags::ALL); mLegacySystemInfo->setSize(mSize.x, mLegacySystemInfo->getFont()->getLetterHeight() * 2.2f); - mLegacySystemInfo->setPosition(0.0f, mCarousel->getPosition().y + mCarousel->getSize().y); + mLegacySystemInfo->setPosition(0.0f, mPrimary->getPosition().y + mPrimary->getSize().y); mLegacySystemInfo->setBackgroundColor(0xDDDDDDD8); mLegacySystemInfo->setRenderBackground(true); mLegacySystemInfo->setFont( @@ -1076,33 +1161,41 @@ void SystemView::renderElements(const glm::mat4& parentTrans, bool abovePrimary) { glm::mat4 trans {getTransform() * parentTrans}; - const float primaryZIndex {mCarousel->getZIndex()}; + const float primaryZIndex {mPrimary->getZIndex()}; - int renderLeft {static_cast(mCamOffset)}; - int renderRight {static_cast(mCamOffset)}; + int renderBefore {static_cast(mCamOffset)}; + int renderAfter {static_cast(mCamOffset)}; // If we're transitioning then also render the previous and next systems. - if (mCarousel->isAnimationPlaying(0)) { - renderLeft -= 1; - renderRight += 1; + if (mPrimary->isAnimationPlaying(0)) { + renderBefore -= 1; + renderAfter += 1; } - for (int i = renderLeft; i <= renderRight; ++i) { + for (int i = renderBefore; i <= renderAfter; ++i) { int index {i}; while (index < 0) - index += static_cast(mCarousel->getNumEntries()); - while (index >= static_cast(mCarousel->getNumEntries())) - index -= static_cast(mCarousel->getNumEntries()); + index += static_cast(mPrimary->getNumEntries()); + while (index >= static_cast(mPrimary->getNumEntries())) + index -= static_cast(mPrimary->getNumEntries()); - if (mCarousel->isAnimationPlaying(0) || index == mCarousel->getCursor()) { + if (mPrimary->isAnimationPlaying(0) || index == mPrimary->getCursor()) { glm::mat4 elementTrans {trans}; - if (mCarousel->getType() == CarouselComponent::HORIZONTAL || - mCarousel->getType() == CarouselComponent::HORIZONTAL_WHEEL) - elementTrans = glm::translate(elementTrans, - glm::vec3 {(i - mCamOffset) * mSize.x, 0.0f, 0.0f}); - else + if (mCarousel != nullptr) { + if (mCarousel->getType() == + CarouselComponent::CarouselType::HORIZONTAL || + mCarousel->getType() == + CarouselComponent::CarouselType::HORIZONTAL_WHEEL) + elementTrans = glm::translate( + elementTrans, glm::vec3 {(i - mCamOffset) * mSize.x, 0.0f, 0.0f}); + else + elementTrans = glm::translate( + elementTrans, glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f}); + } + else if (mTextList != nullptr) { elementTrans = glm::translate(elementTrans, glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f}); + } mRenderer->pushClipRect( glm::ivec2 {static_cast(glm::round(elementTrans[3].x)), diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 143f06b20..a64eff544 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -13,42 +13,26 @@ #include "GuiComponent.h" #include "Sound.h" #include "SystemData.h" -#include "components/CarouselComponent.h" #include "components/DateTimeComponent.h" #include "components/GIFAnimComponent.h" #include "components/GameSelectorComponent.h" #include "components/LottieAnimComponent.h" #include "components/RatingComponent.h" #include "components/TextComponent.h" -#include "components/TextListComponent.h" #include "components/VideoFFmpegComponent.h" +#include "components/primary/CarouselComponent.h" +#include "components/primary/TextListComponent.h" #include "resources/Font.h" #include class SystemData; -struct SystemViewElements { - HelpStyle helpStyle; - std::string name; - std::string fullName; - std::vector> gameSelectors; - std::vector legacyExtras; - std::vector children; - - std::vector> imageComponents; - std::vector> videoComponents; - std::vector> lottieAnimComponents; - std::vector> GIFAnimComponents; - std::vector> gameCountComponents; - std::vector> textComponents; - std::vector> dateTimeComponents; - std::vector> ratingComponents; -}; - class SystemView : public GuiComponent { public: + using PrimaryType = PrimaryComponent::PrimaryType; + SystemView(); ~SystemView(); @@ -59,47 +43,49 @@ public: void update(int deltaTime) override; void render(const glm::mat4& parentTrans) override; - bool isScrolling() { return mCarousel->isScrolling(); } - void stopScrolling() { mCarousel->stopScrolling(); } - bool isSystemAnimationPlaying(unsigned char slot) - { - return mCarousel->isAnimationPlaying(slot); - } + bool isScrolling() { return mPrimary->isScrolling(); } + void stopScrolling() { mPrimary->stopScrolling(); } + bool isSystemAnimationPlaying(unsigned char slot) { return mPrimary->isAnimationPlaying(slot); } void finishSystemAnimation(unsigned char slot) { finishAnimation(slot); - mCarousel->finishAnimation(slot); + mPrimary->finishAnimation(slot); } - CarouselComponent::CarouselType getCarouselType() { return mCarousel->getType(); } - SystemData* getFirstSystem() { return mCarousel->getFirst(); } + PrimaryComponent::PrimaryType getPrimaryType() { return mPrimaryType; } + CarouselComponent::CarouselType getCarouselType() + { + return (mCarousel != nullptr) ? mCarousel->getType() : + CarouselComponent::CarouselType::NO_CAROUSEL; + } + SystemData* getFirstSystem() { return mPrimary->getFirst(); } void startViewVideos() override { - for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) + for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) video->startVideoPlayer(); } void stopViewVideos() override { - for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) + for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) video->stopVideoPlayer(); } void pauseViewVideos() override { - for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) { + for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) { video->pauseVideoPlayer(); } } void muteViewVideos() override { - for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) + for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) video->muteVideoPlayer(); } void onThemeChanged(const std::shared_ptr& theme); std::vector getHelpPrompts() override; - HelpStyle getHelpStyle() override { return mSystemElements[mCarousel->getCursor()].helpStyle; } + HelpStyle getHelpStyle() override { return mSystemElements[mPrimary->getCursor()].helpStyle; } protected: void onCursorChanged(const CursorState& state); @@ -111,10 +97,31 @@ private: void legacyApplyTheme(const std::shared_ptr& theme); void renderElements(const glm::mat4& parentTrans, bool abovePrimary); + struct SystemViewElements { + HelpStyle helpStyle; + std::string name; + std::string fullName; + std::vector> gameSelectors; + std::vector legacyExtras; + std::vector children; + + std::vector> imageComponents; + std::vector> videoComponents; + std::vector> lottieAnimComponents; + std::vector> GIFAnimComponents; + std::vector> gameCountComponents; + std::vector> textComponents; + std::vector> dateTimeComponents; + std::vector> ratingComponents; + }; + Renderer* mRenderer; - std::unique_ptr mCarousel; + std::unique_ptr> mCarousel; + std::unique_ptr> mTextList; std::unique_ptr mLegacySystemInfo; std::vector mSystemElements; + PrimaryComponent* mPrimary; + PrimaryType mPrimaryType; float mCamOffset; float mFadeOpacity; diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index d44d84f92..afcb58979 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -355,19 +355,33 @@ void ViewController::goToSystemView(SystemData* system, bool playTransition) if (applicationStartup) { mCamera = glm::translate(mCamera, -mCurrentView->getPosition()); if (Settings::getInstance()->getString("TransitionStyle") == "slide") { - if (getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL || - getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL_WHEEL) + if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) { + if (getSystemListView()->getCarouselType() == + CarouselComponent::CarouselType::HORIZONTAL || + getSystemListView()->getCarouselType() == + CarouselComponent::CarouselType::HORIZONTAL_WHEEL) + mCamera[3].y += Renderer::getScreenHeight(); + else + mCamera[3].x -= Renderer::getScreenWidth(); + } + else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST) { mCamera[3].y += Renderer::getScreenHeight(); - else - mCamera[3].x -= Renderer::getScreenWidth(); + } updateHelpPrompts(); } else if (Settings::getInstance()->getString("TransitionStyle") == "fade") { - if (getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL || - getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL_WHEEL) + if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) { + if (getSystemListView()->getCarouselType() == + CarouselComponent::CarouselType::HORIZONTAL || + getSystemListView()->getCarouselType() == + CarouselComponent::CarouselType::HORIZONTAL_WHEEL) + mCamera[3].y += Renderer::getScreenHeight(); + else + mCamera[3].x += Renderer::getScreenWidth(); + } + else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST) { mCamera[3].y += Renderer::getScreenHeight(); - else - mCamera[3].x += Renderer::getScreenWidth(); + } } else { updateHelpPrompts(); @@ -415,9 +429,9 @@ void ViewController::goToPrevGamelist() void ViewController::goToGamelist(SystemData* system) { - bool wrapFirstToLast = false; - bool wrapLastToFirst = false; - bool slideTransitions = false; + bool wrapFirstToLast {false}; + bool wrapLastToFirst {false}; + bool slideTransitions {false}; if (mCurrentView != nullptr) mCurrentView->onTransition(); @@ -521,6 +535,7 @@ void ViewController::goToGamelist(SystemData* system) } mCurrentView = getGamelistView(system); + mCurrentView->finishAnimation(0); // Application startup animation, if starting in a gamelist rather than in the system view. if (mState.viewing == NOTHING) { diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 9f5470f06..4da839bbc 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -31,7 +31,12 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/LambdaAnimation.h ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/MoveCameraAnimation.h - # GUI components + # Primary GUI components + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/CarouselComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/PrimaryComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/TextListComponent.h + + # Secondary GUI components ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgeComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.h @@ -111,7 +116,7 @@ set(CORE_SOURCES # Animations ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/AnimationController.cpp - # GUI components + # Secondary GUI components ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgeComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 712af2bfb..56960d15c 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -240,7 +240,7 @@ void Window::input(InputConfig* config, Input input) // up. So scale it to full size so it won't be stuck at a smaller size when returning // from the submenu. mTopScale = 1.0f; - GuiComponent* menu = mGuiStack.back(); + GuiComponent* menu {mGuiStack.back()}; glm::vec2 menuCenter {menu->getCenter()}; menu->setOrigin(0.5f, 0.5f); menu->setPosition(menuCenter.x, menuCenter.y, 0.0f); diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index 56d3c4634..ad30a1117 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -13,7 +13,7 @@ #define TOTAL_HORIZONTAL_PADDING_PX 20.0f ComponentList::ComponentList() - : IList {LIST_SCROLL_STYLE_SLOW, LIST_NEVER_LOOP} + : IList {LIST_SCROLL_STYLE_SLOW, ListLoopType::LIST_NEVER_LOOP} , mRenderer {Renderer::getInstance()} , mFocused {false} , mSetupCompleted {false} @@ -53,7 +53,7 @@ void ComponentList::addRow(const ComponentListRow& row, bool setCursorHere) if (setCursorHere) { mCursor = static_cast(mEntries.size()) - 1; - onCursorChanged(CURSOR_STOPPED); + onCursorChanged(CursorState::CURSOR_STOPPED); } } diff --git a/es-core/src/components/GameSelectorComponent.h b/es-core/src/components/GameSelectorComponent.h index 7c4a06b78..783aff0e8 100644 --- a/es-core/src/components/GameSelectorComponent.h +++ b/es-core/src/components/GameSelectorComponent.h @@ -11,6 +11,7 @@ #include "GuiComponent.h" #include "Log.h" +#include "Settings.h" #include "ThemeData.h" class GameSelectorComponent : public GuiComponent diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 80c25af51..a4e9b2a22 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -13,14 +13,15 @@ #include "components/ImageComponent.h" #include "utils/StringUtil.h" -enum CursorState { +enum class CursorState { CURSOR_STOPPED, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). CURSOR_SCROLLING }; -enum ListLoopType { +enum class ListLoopType { LIST_ALWAYS_LOOP, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). LIST_PAUSE_AT_END, + LIST_PAUSE_AT_END_ON_JUMP, LIST_NEVER_LOOP }; @@ -57,7 +58,7 @@ const ScrollTierList LIST_SCROLL_STYLE_SLOW = { }; // clang-format on -template class IList : public GuiComponent +template class IList : public virtual GuiComponent { public: struct Entry { @@ -67,6 +68,10 @@ public: }; protected: + Window* mWindow; + std::vector mEntries; + const ScrollTierList& mTierList; + const ListLoopType mLoopType; int mCursor; int mScrollTier; int mScrollVelocity; @@ -76,32 +81,23 @@ protected: float mTitleOverlayOpacity; unsigned int mTitleOverlayColor; - const ScrollTierList& mTierList; - const ListLoopType mLoopType; - - std::vector mEntries; - Window* mWindow; - public: IList(const ScrollTierList& tierList = LIST_SCROLL_STYLE_QUICK, - const ListLoopType& loopType = LIST_PAUSE_AT_END) - : mTierList {tierList} + const ListLoopType& loopType = ListLoopType::LIST_PAUSE_AT_END) + : mWindow {Window::getInstance()} + , mTierList {tierList} , mLoopType {loopType} - , mWindow {Window::getInstance()} + , mCursor {0} + , mScrollTier {0} + , mScrollVelocity {0} + , mScrollTierAccumulator {0} + , mScrollCursorAccumulator {0} + , mTitleOverlayOpacity {0.0f} + , mTitleOverlayColor {0xFFFFFF00} { - mCursor = 0; - mScrollTier = 0; - mScrollVelocity = 0; - mScrollTierAccumulator = 0; - mScrollCursorAccumulator = 0; - - mTitleOverlayOpacity = 0.0f; - mTitleOverlayColor = 0xFFFFFF00; } - bool isScrolling() const { return (mScrollVelocity != 0 && mScrollTier > 0); } - - int getScrollingVelocity() { return mScrollVelocity; } + const bool isScrolling() const { return (mScrollVelocity != 0 && mScrollTier > 0); } void stopScrolling() { @@ -109,15 +105,17 @@ public: listInput(0); if (mScrollVelocity == 0) - onCursorChanged(CURSOR_STOPPED); + onCursorChanged(CursorState::CURSOR_STOPPED); } + const int getScrollingVelocity() const { return mScrollVelocity; } + void clear() { mEntries.clear(); mCursor = 0; listInput(0); - onCursorChanged(CURSOR_STOPPED); + onCursorChanged(CursorState::CURSOR_STOPPED); } const std::string& getSelectedName() @@ -166,16 +164,15 @@ public: { assert(it != mEntries.cend()); mCursor = it - mEntries.cbegin(); - onCursorChanged(CURSOR_STOPPED); + onCursorChanged(CursorState::CURSOR_STOPPED); } - // Returns true if successful (select is in our list), false if not. bool setCursor(const UserData& obj) { for (auto it = mEntries.cbegin(); it != mEntries.cend(); ++it) { if ((*it).object == obj) { mCursor = static_cast(it - mEntries.cbegin()); - onCursorChanged(CURSOR_STOPPED); + onCursorChanged(CursorState::CURSOR_STOPPED); return true; } } @@ -205,7 +202,7 @@ protected: { if (mCursor > 0 && it - mEntries.cbegin() <= mCursor) { --mCursor; - onCursorChanged(CURSOR_STOPPED); + onCursorChanged(CursorState::CURSOR_STOPPED); } mEntries.erase(it); @@ -214,7 +211,7 @@ protected: bool listFirstRow() { mCursor = 0; - onCursorChanged(CURSOR_STOPPED); + onCursorChanged(CursorState::CURSOR_STOPPED); onScroll(); return true; } @@ -222,7 +219,7 @@ protected: bool listLastRow() { mCursor = static_cast(mEntries.size()) - 1; - onCursorChanged(CURSOR_STOPPED); + onCursorChanged(CursorState::CURSOR_STOPPED); onScroll(); return true; } @@ -257,7 +254,7 @@ protected: // We delay scrolling until after scroll tier has updated so isScrolling() returns // accurately during onCursorChanged callbacks. We don't just do scroll tier first // because it would not catch the scrollDelay == tier length case. - int scrollCount = 0; + int scrollCount {0}; while (mScrollCursorAccumulator >= mTierList.tiers[mScrollTier].scrollDelay) { mScrollCursorAccumulator -= mTierList.tiers[mScrollTier].scrollDelay; ++scrollCount; @@ -275,45 +272,47 @@ protected: scroll(mScrollVelocity); } - void listRenderTitleOverlay(const glm::mat4& /*trans*/) + void listRenderTitleOverlay(const glm::mat4&) { - if (!Settings::getInstance()->getBool("ListScrollOverlay")) - return; + if constexpr (std::is_same_v) { + if (!Settings::getInstance()->getBool("ListScrollOverlay")) + return; - if (size() == 0 || mTitleOverlayOpacity == 0.0f) { - mWindow->renderListScrollOverlay(0.0f, ""); - return; - } + if (size() == 0 || mTitleOverlayOpacity == 0.0f) { + mWindow->renderListScrollOverlay(0.0f, ""); + return; + } - std::string titleIndex; - bool favoritesSorting; + std::string titleIndex; + bool favoritesSorting; - if (getSelected()->getSystem()->isCustomCollection()) - favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom"); - else - favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); + if (getSelected()->getSystem()->isCustomCollection()) + favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom"); + else + favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); - if (favoritesSorting && getSelected()->getFavorite()) { + if (favoritesSorting && getSelected()->getFavorite()) { #if defined(_MSC_VER) // MSVC compiler. - titleIndex = Utils::String::wideStringToString(L"\uF005"); + titleIndex = Utils::String::wideStringToString(L"\uF005"); #else - titleIndex = "\uF005"; + titleIndex = "\uF005"; #endif - } - else { - titleIndex = getSelected()->getName(); - if (titleIndex.size()) { - titleIndex[0] = toupper(titleIndex[0]); - if (titleIndex.size() > 1) { - titleIndex = titleIndex.substr(0, 2); - titleIndex[1] = tolower(titleIndex[1]); + } + else { + titleIndex = getSelected()->getName(); + if (titleIndex.size()) { + titleIndex[0] = toupper(titleIndex[0]); + if (titleIndex.size() > 1) { + titleIndex = titleIndex.substr(0, 2); + titleIndex[1] = tolower(titleIndex[1]); + } } } - } - // The actual rendering takes place in Window to make sure that the overlay is placed on - // top of all GUI elements but below the info popups and GPU statistics overlay. - mWindow->renderListScrollOverlay(mTitleOverlayOpacity, titleIndex); + // The actual rendering takes place in Window to make sure that the overlay is placed on + // top of all GUI elements but below the info popups and GPU statistics overlay. + mWindow->renderListScrollOverlay(mTitleOverlayOpacity, titleIndex); + } } void scroll(int amt) @@ -321,14 +320,23 @@ protected: if (mScrollVelocity == 0 || size() < 2) return; - int cursor = mCursor + amt; - int absAmt = amt < 0 ? -amt : amt; + int cursor {mCursor + amt}; + int absAmt {amt < 0 ? -amt : amt}; - // Stop at the end if we've been holding down the button for a long time or - // we're scrolling faster than one item at a time (e.g. page up/down). - // Otherwise, loop around. - if ((mLoopType == LIST_PAUSE_AT_END && (mScrollTier > 0 || absAmt > 1)) || - mLoopType == LIST_NEVER_LOOP) { + bool stopScroll {false}; + + // Depending on the loop type we'll either pause at the ends if holding a navigation + // button, or we'll only stop if it's a quick jump key (should or trigger button) that + // is held, or we never loop. + if (mLoopType == ListLoopType::LIST_PAUSE_AT_END && (mScrollTier > 0 || absAmt > 1)) + stopScroll = true; + else if (mLoopType == ListLoopType::LIST_PAUSE_AT_END_ON_JUMP && abs(mScrollVelocity) > 1 && + (mScrollTier > 0 || absAmt > 1)) + stopScroll = true; + else if (mLoopType == ListLoopType::LIST_NEVER_LOOP) + stopScroll = true; + + if (stopScroll) { if (cursor < 0) { cursor = 0; mScrollVelocity = 0; @@ -351,10 +359,11 @@ protected: onScroll(); mCursor = cursor; - onCursorChanged((mScrollTier > 0) ? CURSOR_SCROLLING : CURSOR_STOPPED); + onCursorChanged((mScrollTier > 0) ? CursorState::CURSOR_SCROLLING : + CursorState::CURSOR_STOPPED); } - virtual void onCursorChanged(const CursorState& /*state*/) {} + virtual void onCursorChanged(const CursorState&) {} virtual void onScroll() {} }; diff --git a/es-app/src/components/CarouselComponent.cpp b/es-core/src/components/primary/CarouselComponent.h similarity index 62% rename from es-app/src/components/CarouselComponent.cpp rename to es-core/src/components/primary/CarouselComponent.h index 7dcda6872..7442f7017 100644 --- a/es-app/src/components/CarouselComponent.cpp +++ b/es-core/src/components/primary/CarouselComponent.h @@ -1,22 +1,147 @@ // SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition -// CarouselComponent.cpp +// CarouselComponent.h // // Carousel. // -#include "components/CarouselComponent.h" +#ifndef ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H +#define ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H #include "Log.h" +#include "Sound.h" #include "animations/LambdaAnimation.h" +#include "components/IList.h" +#include "components/primary/PrimaryComponent.h" +#include "resources/Font.h" -CarouselComponent::CarouselComponent() - : IList {LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP} +namespace +{ + struct CarouselElement { + std::shared_ptr logo; + std::string logoPath; + std::string defaultLogoPath; + }; +}; // namespace + +template +class CarouselComponent : public PrimaryComponent, protected IList +{ + using List = IList; + +protected: + using List::mCursor; + using List::mEntries; + using List::mScrollVelocity; + using List::mSize; + using List::mWindow; + + using GuiComponent::mDefaultZIndex; + using GuiComponent::mOrigin; + using GuiComponent::mPosition; + using GuiComponent::mZIndex; + +public: + using Entry = typename IList::Entry; + + enum class CarouselType { + HORIZONTAL, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). + VERTICAL, + VERTICAL_WHEEL, + HORIZONTAL_WHEEL, + NO_CAROUSEL + }; + + CarouselComponent(); + + void addEntry(Entry& entry, const std::shared_ptr& theme = nullptr); + Entry& getEntry(int index) { return mEntries.at(index); } + const CarouselType getType() { return mType; } + + void setCursorChangedCallback(const std::function& func) override + { + mCursorChangedCallback = func; + } + void setCancelTransitionsCallback(const std::function& func) override + { + mCancelTransitionsCallback = 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 onCursorChanged(const CursorState& state) override; + void onScroll() override + { + NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND); + } + + bool isScrolling() const override { return List::isScrolling(); } + void stopScrolling() override + { + List::stopScrolling(); + // Only finish the animation if we're in the gamelist view. + if constexpr (std::is_same_v) + 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(); } + + Renderer* mRenderer; + std::function mCursorChangedCallback; + std::function mCancelTransitionsCallback; + + float mEntryCamOffset; + int mPreviousScrollVelocity; + bool mTriggerJump; + + CarouselType mType; + std::shared_ptr mFont; + unsigned int mTextColor; + unsigned int mTextBackgroundColor; + std::string mText; + float mLineSpacing; + Alignment mLogoHorizontalAlignment; + Alignment mLogoVerticalAlignment; + float mMaxLogoCount; + glm::vec2 mLogoSize; + float mLogoScale; + float mLogoRotation; + glm::vec2 mLogoRotationOrigin; + unsigned int mCarouselColor; + unsigned int mCarouselColorEnd; + bool mColorGradientHorizontal; +}; + +template +CarouselComponent::CarouselComponent() + : IList {LIST_SCROLL_STYLE_SLOW, + (std::is_same_v ? + ListLoopType::LIST_ALWAYS_LOOP : + ListLoopType::LIST_PAUSE_AT_END_ON_JUMP)} , mRenderer {Renderer::getInstance()} - , mCamOffset {0.0f} + , mEntryCamOffset {0.0f} , mPreviousScrollVelocity {0} - , mType {HORIZONTAL} + , mTriggerJump {false} + , mType {CarouselType::HORIZONTAL} , mFont {Font::get(FONT_SIZE_LARGE)} , mTextColor {0x000000FF} , mTextBackgroundColor {0xFFFFFF00} @@ -34,10 +159,11 @@ CarouselComponent::CarouselComponent() { } -void CarouselComponent::addEntry(const std::shared_ptr& theme, - Entry& entry, - bool legacyMode) +template +void CarouselComponent::addEntry(Entry& entry, const std::shared_ptr& theme) { + bool legacyMode {theme->isLegacyTheme()}; + // Make logo. if (legacyMode) { const ThemeData::ThemeElement* logoElem { @@ -96,8 +222,10 @@ void CarouselComponent::addEntry(const std::shared_ptr& theme, } if (!legacyMode) { text->setLineSpacing(mLineSpacing); - if (mText != "") - text->setValue(mText); + if constexpr (std::is_same_v) { + if (mText != "") + text->setValue(mText); + } text->setColor(mTextColor); text->setBackgroundColor(mTextBackgroundColor); text->setRenderBackground(true); @@ -126,64 +254,116 @@ void CarouselComponent::addEntry(const std::shared_ptr& theme, glm::vec2 denormalized {mLogoSize * entry.data.logo->getOrigin()}; entry.data.logo->setPosition(glm::vec3 {denormalized.x, denormalized.y, 0.0f}); - add(entry); + List::add(entry); } -bool CarouselComponent::input(InputConfig* config, Input input) +template bool CarouselComponent::input(InputConfig* config, Input input) { if (input.value != 0) { switch (mType) { - case VERTICAL: - case VERTICAL_WHEEL: + case CarouselType::VERTICAL: + case CarouselType::VERTICAL_WHEEL: if (config->isMappedLike("up", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); - listInput(-1); + List::listInput(-1); return true; } if (config->isMappedLike("down", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); - listInput(1); + List::listInput(1); return true; } break; - case HORIZONTAL: - case HORIZONTAL_WHEEL: + case CarouselType::HORIZONTAL: + case CarouselType::HORIZONTAL_WHEEL: default: if (config->isMappedLike("left", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); - listInput(-1); + List::listInput(-1); return true; } if (config->isMappedLike("right", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); - listInput(1); + List::listInput(1); return true; } break; } + if constexpr (std::is_same_v) { + if (config->isMappedLike("leftshoulder", input)) { + if (getCursor() == 0) { + if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND)) + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + return true; + } + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(-10); + return true; + } + if (config->isMappedLike("rightshoulder", input)) { + if (getCursor() == static_cast(mEntries.size()) - 1) { + if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND)) + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + return true; + } + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(10); + return true; + } + if (config->isMappedLike("lefttrigger", input)) { + mTriggerJump = true; + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + return this->listFirstRow(); + } + if (config->isMappedLike("righttrigger", input)) { + mTriggerJump = 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)) { - listInput(0); + if constexpr (std::is_same_v) { + 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)) { + onCursorChanged(CursorState::CURSOR_STOPPED); + List::listInput(0); + mTriggerJump = false; + } + } + if constexpr (std::is_same_v) { + if (config->isMappedLike("up", input) || config->isMappedLike("down", input) || + config->isMappedLike("left", input) || config->isMappedLike("right", input)) + List::listInput(0); } } return GuiComponent::input(config, input); } -void CarouselComponent::update(int deltaTime) +template void CarouselComponent::update(int deltaTime) { - listUpdate(deltaTime); + List::listUpdate(deltaTime); GuiComponent::update(deltaTime); } -void CarouselComponent::render(const glm::mat4& parentTrans) +template void CarouselComponent::render(const glm::mat4& parentTrans) { + if (mEntries.size() == 0) + return; + glm::mat4 carouselTrans {parentTrans}; carouselTrans = glm::translate(carouselTrans, glm::vec3 {mPosition.x, mPosition.y, 0.0f}); carouselTrans = glm::translate( @@ -210,15 +390,15 @@ void CarouselComponent::render(const glm::mat4& parentTrans) float yOff {0.0f}; switch (mType) { - case HORIZONTAL_WHEEL: - case VERTICAL_WHEEL: - xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mCamOffset * logoSpacing.y)); + case CarouselType::HORIZONTAL_WHEEL: + case CarouselType::VERTICAL_WHEEL: + xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mEntryCamOffset * logoSpacing.y)); yOff = (mSize.y - mLogoSize.y) / 2.0f; break; - case VERTICAL: + case CarouselType::VERTICAL: logoSpacing.y = ((mSize.y - (mLogoSize.y * mMaxLogoCount)) / (mMaxLogoCount)) + mLogoSize.y; - yOff = (mSize.y - mLogoSize.y) / 2.0f - (mCamOffset * logoSpacing.y); + yOff = (mSize.y - mLogoSize.y) / 2.0f - (mEntryCamOffset * logoSpacing.y); if (mLogoHorizontalAlignment == ALIGN_LEFT) xOff = mLogoSize.x / 10.0f; else if (mLogoHorizontalAlignment == ALIGN_RIGHT) @@ -226,11 +406,11 @@ void CarouselComponent::render(const glm::mat4& parentTrans) else xOff = (mSize.x - mLogoSize.x) / 2.0f; break; - case HORIZONTAL: + case CarouselType::HORIZONTAL: default: logoSpacing.x = ((mSize.x - (mLogoSize.x * mMaxLogoCount)) / (mMaxLogoCount)) + mLogoSize.x; - xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mCamOffset * logoSpacing.x)); + xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mEntryCamOffset * logoSpacing.x)); if (mLogoVerticalAlignment == ALIGN_TOP) yOff = mLogoSize.y / 10.0f; else if (mLogoVerticalAlignment == ALIGN_BOTTOM) @@ -240,7 +420,7 @@ void CarouselComponent::render(const glm::mat4& parentTrans) break; } - int center {static_cast(mCamOffset)}; + int center {static_cast(mEntryCamOffset)}; int logoInclusion {static_cast(std::ceil(mMaxLogoCount / 2.0f))}; bool singleEntry {mEntries.size() == 1}; @@ -263,7 +443,7 @@ void CarouselComponent::render(const glm::mat4& parentTrans) logoTrans = glm::translate( logoTrans, glm::vec3 {i * logoSpacing.x + xOff, i * logoSpacing.y + yOff, 0.0f}); - float distance {i - mCamOffset}; + float distance {i - mEntryCamOffset}; float scale {1.0f + ((mLogoScale - 1.0f) * (1.0f - fabsf(distance)))}; scale = std::min(mLogoScale, std::max(1.0f, scale)); @@ -278,16 +458,16 @@ void CarouselComponent::render(const glm::mat4& parentTrans) if (comp == nullptr) continue; - if (mType == VERTICAL_WHEEL || mType == HORIZONTAL_WHEEL) { + if (mType == CarouselType::VERTICAL_WHEEL || mType == CarouselType::HORIZONTAL_WHEEL) { comp->setRotationDegrees(mLogoRotation * distance); comp->setRotationOrigin(mLogoRotationOrigin); } - // When running at lower resolutions, prevent the scale-down to go all the way to the - // minimum value. This avoids potential single-pixel alignment issues when the logo - // can't be vertically placed exactly in the middle of the carousel. Although the - // problem theoretically exists at all resolutions, it's not visble at around 1080p - // and above. + // When running at lower resolutions, prevent the scale-down to go all the way to + // the minimum value. This avoids potential single-pixel alignment issues when the + // logo can't be vertically placed exactly in the middle of the carousel. Although + // the problem theoretically exists at all resolutions, it's not visble at around + // 1080p and above. if (std::min(Renderer::getScreenWidth(), Renderer::getScreenHeight()) < 1080.0f) scale = glm::clamp(scale, 1.0f / mLogoScale + 0.01f, 1.0f); @@ -298,10 +478,11 @@ void CarouselComponent::render(const glm::mat4& parentTrans) mRenderer->popClipRect(); } -void CarouselComponent::applyTheme(const std::shared_ptr& theme, - const std::string& view, - const std::string& element, - unsigned int properties) +template +void CarouselComponent::applyTheme(const std::shared_ptr& theme, + const std::string& view, + const std::string& element, + unsigned int properties) { using namespace ThemeFlags; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "carousel")}; @@ -312,7 +493,7 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, mPosition.y = floorf(0.5f * (Renderer::getScreenHeight() - mSize.y)); mCarouselColor = 0xFFFFFFD8; mCarouselColorEnd = 0xFFFFFFD8; - mDefaultZIndex = 50.0f; + mZIndex = mDefaultZIndex; mText = ""; if (!elem) @@ -321,22 +502,22 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("type")) { const std::string type {elem->get("type")}; if (type == "horizontal") { - mType = HORIZONTAL; + mType = CarouselType::HORIZONTAL; } else if (type == "horizontal_wheel") { - mType = HORIZONTAL_WHEEL; + mType = CarouselType::HORIZONTAL_WHEEL; } else if (type == "vertical") { - mType = VERTICAL; + mType = CarouselType::VERTICAL; } else if (type == "vertical_wheel") { - mType = VERTICAL_WHEEL; + mType = CarouselType::VERTICAL_WHEEL; } else { LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " " defined as \"" << type << "\""; - mType = HORIZONTAL; + mType = CarouselType::HORIZONTAL; } } @@ -395,10 +576,10 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("logoHorizontalAlignment")) { const std::string alignment {elem->get("logoHorizontalAlignment")}; - if (alignment == "left" && mType != HORIZONTAL) { + if (alignment == "left" && mType != CarouselType::HORIZONTAL) { mLogoHorizontalAlignment = ALIGN_LEFT; } - else if (alignment == "right" && mType != HORIZONTAL) { + else if (alignment == "right" && mType != CarouselType::HORIZONTAL) { mLogoHorizontalAlignment = ALIGN_RIGHT; } else if (alignment == "center") { @@ -414,10 +595,10 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("logoVerticalAlignment")) { const std::string alignment {elem->get("logoVerticalAlignment")}; - if (alignment == "top" && mType != VERTICAL) { + if (alignment == "top" && mType != CarouselType::VERTICAL) { mLogoVerticalAlignment = ALIGN_TOP; } - else if (alignment == "bottom" && mType != VERTICAL) { + else if (alignment == "bottom" && mType != CarouselType::VERTICAL) { mLogoVerticalAlignment = ALIGN_BOTTOM; } else if (alignment == "center") { @@ -434,19 +615,19 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, // Legacy themes only. if (elem->has("logoAlignment")) { const std::string alignment {elem->get("logoAlignment")}; - if (alignment == "left" && mType != HORIZONTAL) { + if (alignment == "left" && mType != CarouselType::HORIZONTAL) { mLogoHorizontalAlignment = ALIGN_LEFT; mLogoVerticalAlignment = ALIGN_CENTER; } - else if (alignment == "right" && mType != HORIZONTAL) { + else if (alignment == "right" && mType != CarouselType::HORIZONTAL) { mLogoHorizontalAlignment = ALIGN_RIGHT; mLogoVerticalAlignment = ALIGN_CENTER; } - else if (alignment == "top" && mType != VERTICAL) { + else if (alignment == "top" && mType != CarouselType::VERTICAL) { mLogoVerticalAlignment = ALIGN_TOP; mLogoHorizontalAlignment = ALIGN_CENTER; } - else if (alignment == "bottom" && mType != VERTICAL) { + else if (alignment == "bottom" && mType != CarouselType::VERTICAL) { mLogoVerticalAlignment = ALIGN_BOTTOM; mLogoHorizontalAlignment = ALIGN_CENTER; } @@ -507,20 +688,28 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, mRenderer->getScreenHeight() * 1.5f); } -void CarouselComponent::onCursorChanged(const CursorState& state) +template void CarouselComponent::onCursorChanged(const CursorState& state) { - float startPos {mCamOffset}; + 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. - 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). + 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 logos. bool changedDirection {false}; @@ -536,11 +725,11 @@ void CarouselComponent::onCursorChanged(const CursorState& state) if (mScrollVelocity != 0) mPreviousScrollVelocity = mScrollVelocity; - // No need to animate transition, we're not going anywhere (probably mEntries.size() == 1). - if (endPos == mCamOffset) + // No need to animate transition, we're not going anywhere. + if (endPos == mEntryCamOffset) return; - Animation* anim = new LambdaAnimation( + Animation* anim {new LambdaAnimation( [this, startPos, endPos, posMax](float t) { t -= 1; float f {glm::mix(startPos, endPos, t * t * t + 1)}; @@ -549,12 +738,14 @@ void CarouselComponent::onCursorChanged(const CursorState& state) if (f >= posMax) f -= posMax; - mCamOffset = f; + mEntryCamOffset = f; }, - 500); + 500)}; - setAnimation(anim, 0, nullptr, false, 0); + GuiComponent::setAnimation(anim, 0, nullptr, false, 0); if (mCursorChangedCallback) mCursorChangedCallback(state); } + +#endif // ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H diff --git a/es-core/src/components/primary/PrimaryComponent.h b/es-core/src/components/primary/PrimaryComponent.h new file mode 100644 index 000000000..32a64bb86 --- /dev/null +++ b/es-core/src/components/primary/PrimaryComponent.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// PrimaryComponent.h +// +// Base class for the primary components (carousel and textlist). +// + +#ifndef ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H +#define ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H + +template class PrimaryComponent : public virtual GuiComponent +{ +public: + enum class PrimaryType { + CAROUSEL, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). + TEXTLIST + }; + + enum class PrimaryAlignment { + ALIGN_LEFT, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). + ALIGN_CENTER, + ALIGN_RIGHT + }; + + // IList functions. + virtual bool isScrolling() const = 0; + virtual void stopScrolling() = 0; + virtual const int getScrollingVelocity() = 0; + virtual void clear() = 0; + virtual const T& getSelected() const = 0; + virtual const T& getNext() const = 0; + virtual const T& getPrevious() const = 0; + virtual const T& getFirst() const = 0; + virtual const T& getLast() const = 0; + virtual bool setCursor(const T& obj) = 0; + virtual bool remove(const T& obj) = 0; + virtual int size() const = 0; + + // Functions used by all primary components. + virtual void setCancelTransitionsCallback(const std::function& func) = 0; + virtual void setCursorChangedCallback(const std::function& func) = 0; + virtual int getCursor() = 0; + virtual const size_t getNumEntries() = 0; + + // Functions used by some primary components. + virtual void setAlignment(PrimaryAlignment align) {}; +}; + +#endif // ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H diff --git a/es-app/src/components/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h similarity index 72% rename from es-app/src/components/TextListComponent.h rename to es-core/src/components/primary/TextListComponent.h index 56bfb15d3..489038ef7 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -12,35 +12,39 @@ #include "Log.h" #include "Sound.h" #include "components/IList.h" +#include "components/primary/PrimaryComponent.h" #include "resources/Font.h" -#include "utils/StringUtil.h" -#include +namespace +{ + struct TextListData { + unsigned int colorId; + std::shared_ptr textCache; + }; +}; // namespace -class TextCache; - -struct TextListData { - unsigned int colorId; - std::shared_ptr textCache; -}; - -// A scrollable text list supporting multiple row colors. -template class TextListComponent : public IList +template +class TextListComponent : public PrimaryComponent, private IList { using List = IList; protected: using List::mCursor; using List::mEntries; + using List::mScrollVelocity; using List::mSize; using List::mWindow; public: - using GuiComponent::setColor; using List::size; + using Entry = typename IList::Entry; + using PrimaryAlignment = typename PrimaryComponent::PrimaryAlignment; + using GuiComponent::setColor; TextListComponent(); + void addEntry(Entry& entry, const std::shared_ptr& theme = nullptr); + bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; void render(const glm::mat4& parentTrans) override; @@ -49,24 +53,16 @@ public: const std::string& element, unsigned int properties) override; - void add(const std::string& name, const T& obj, unsigned int colorId); + void setAlignment(PrimaryAlignment align) override { mAlignment = align; } - enum Alignment { - ALIGN_LEFT, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). - ALIGN_CENTER, - ALIGN_RIGHT - }; - - void setAlignment(Alignment align) - { - // Set alignment. - mAlignment = align; - } - - void setCursorChangedCallback(const std::function& func) + void setCursorChangedCallback(const std::function& func) override { mCursorChangedCallback = func; } + void setCancelTransitionsCallback(const std::function& func) override + { + mCancelTransitionsCallback = func; + } void setFont(const std::shared_ptr& font) { @@ -135,16 +131,37 @@ protected: void onCursorChanged(const CursorState& state) override; private: + 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(); } + + int getCursor() override { return mCursor; } + const size_t getNumEntries() override { return mEntries.size(); } + Renderer* mRenderer; + std::function mCancelTransitionsCallback; + float mCamOffset; + int mPreviousScrollVelocity; + int mLoopOffset; int mLoopOffset2; int mLoopTime; bool mLoopScroll; - Alignment mAlignment; + PrimaryAlignment mAlignment; float mHorizontalMargin; std::function mCursorChangedCallback; + ImageComponent mSelectorImage; std::shared_ptr mFont; bool mUppercase; @@ -159,34 +176,162 @@ private: unsigned int mSelectedColor; static const unsigned int COLOR_ID_COUNT = 2; unsigned int mColors[COLOR_ID_COUNT]; - - ImageComponent mSelectorImage; }; -template TextListComponent::TextListComponent() +template +TextListComponent::TextListComponent() + : IList {(std::is_same_v ? LIST_SCROLL_STYLE_SLOW : + LIST_SCROLL_STYLE_QUICK), + ListLoopType::LIST_PAUSE_AT_END} + , mRenderer {Renderer::getInstance()} + , mCamOffset {0.0f} + , mPreviousScrollVelocity {0} + , mLoopOffset {0} + , mLoopOffset2 {0} + , mLoopTime {0} + , mLoopScroll {false} + , mAlignment {PrimaryAlignment::ALIGN_CENTER} + , mHorizontalMargin {0.0f} + , mFont {Font::get(FONT_SIZE_MEDIUM)} + , mUppercase {false} + , mLowercase {false} + , mCapitalize {false} + , mLineSpacing {1.5f} + , mSelectorHeight {mFont->getSize() * 1.5f} + , mSelectorOffsetY {0.0f} + , mSelectorColor {0x000000FF} + , mSelectorColorEnd {0x000000FF} + , mSelectorColorGradientHorizontal {true} + , mSelectedColor {0} + , mColors {0x0000FFFF, 0x00FF00FF} { - mRenderer = Renderer::getInstance(); - mLoopOffset = 0; - mLoopOffset2 = 0; - mLoopTime = 0; - mLoopScroll = false; +} - mHorizontalMargin = 0.0f; - mAlignment = ALIGN_CENTER; +template +void TextListComponent::addEntry(Entry& entry, const std::shared_ptr& theme) +{ + List::add(entry); +} - mFont = Font::get(FONT_SIZE_MEDIUM); - mUppercase = false; - mLowercase = false; - mCapitalize = false; - mLineSpacing = 1.5f; - mSelectorHeight = mFont->getSize() * 1.5f; - mSelectorOffsetY = 0; - mSelectorColor = 0x000000FF; - mSelectorColorEnd = 0x000000FF; - mSelectorColorGradientHorizontal = true; - mSelectedColor = 0; - mColors[0] = 0x0000FFFF; - mColors[1] = 0x00FF00FF; +template bool TextListComponent::input(InputConfig* config, Input input) +{ + if (size() > 0) { + if (input.value != 0) { + 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; + } + if (config->isMappedLike("leftshoulder", input)) { + if (getCursor() == 0) { + if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND)) + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + return true; + } + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(-10); + return true; + } + if (config->isMappedLike("rightshoulder", input)) { + if (getCursor() == static_cast(mEntries.size()) - 1) { + if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND)) + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + return true; + } + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(10); + return true; + } + if (config->isMappedLike("righttrigger", input)) { + if (getCursor() == static_cast(mEntries.size()) - 1) { + if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND)) + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + return true; + } + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + return this->listLastRow(); + } + if (config->isMappedLike("lefttrigger", input)) { + if (getCursor() == 0) { + if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND)) + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + return true; + } + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + return this->listFirstRow(); + } + } + else { + if (config->isMappedLike("up", input) || config->isMappedLike("down", input) || + config->isMappedLike("leftshoulder", input) || + config->isMappedLike("rightshoulder", input) || + config->isMappedLike("lefttrigger", input) || + config->isMappedLike("righttrigger", input)) { + if constexpr (std::is_same_v) + List::listInput(0); + else + List::stopScrolling(); + } + } + } + + return GuiComponent::input(config, input); +} + +template void TextListComponent::update(int deltaTime) +{ + List::listUpdate(deltaTime); + + if (mWindow->isScreensaverActive() || !mWindow->getAllowTextScrolling()) + List::stopScrolling(); + + if (!isScrolling() && size() > 0) { + // Always reset the loop offsets. + mLoopOffset = 0; + mLoopOffset2 = 0; + + // If we're not scrolling and this object's text exceeds our size, then loop it. + const float textLength {mFont + ->sizeText(Utils::String::toUpper( + mEntries.at(static_cast(mCursor)).name)) + .x}; + const float limit {mSize.x - mHorizontalMargin * 2.0f}; + + if (textLength > limit) { + // Loop the text. + const float speed {mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x * 0.247f}; + const float delay {3000.0f}; + const float scrollLength {textLength}; + const float returnLength {speed * 1.5f}; + const float scrollTime {(scrollLength * 1000.0f) / speed}; + const float returnTime {(returnLength * 1000.0f) / speed}; + const int maxTime {static_cast(delay + scrollTime + returnTime)}; + + mLoopTime += deltaTime; + while (mLoopTime > maxTime) + mLoopTime -= maxTime; + + mLoopOffset = static_cast(Utils::Math::loop(delay, scrollTime + returnTime, + static_cast(mLoopTime), + scrollLength + returnLength)); + + if (mLoopOffset > (scrollLength - (limit - returnLength))) + mLoopOffset2 = static_cast(mLoopOffset - (scrollLength + returnLength)); + } + } + + GuiComponent::update(deltaTime); } template void TextListComponent::render(const glm::mat4& parentTrans) @@ -259,7 +404,7 @@ template void TextListComponent::render(const glm::mat4& parentT static_cast(std::round(dim.y))}); for (int i = startEntry; i < listCutoff; ++i) { - typename IList::Entry& entry {mEntries.at(static_cast(i))}; + Entry& entry {mEntries.at(i)}; unsigned int color; if (mCursor == i && mSelectedColor) @@ -282,28 +427,33 @@ template void TextListComponent::render(const glm::mat4& parentT std::unique_ptr(font->buildTextCache(entry.name, 0, 0, 0x000000FF)); } - // If a game is marked as hidden, lower the text opacity a lot. - // If a game is marked to not be counted, lower the opacity a moderate amount. - if (entry.object->getHidden()) - entry.data.textCache->setColor(color & 0xFFFFFF44); - else if (!entry.object->getCountAsGame()) - entry.data.textCache->setColor(color & 0xFFFFFF77); - else + if constexpr (std::is_same_v) { + // If a game is marked as hidden, lower the text opacity a lot. + // If a game is marked to not be counted, lower the opacity a moderate amount. + if (entry.object->getHidden()) + entry.data.textCache->setColor(color & 0xFFFFFF44); + else if (!entry.object->getCountAsGame()) + entry.data.textCache->setColor(color & 0xFFFFFF77); + else + entry.data.textCache->setColor(color); + } + else { entry.data.textCache->setColor(color); + } glm::vec3 offset {0.0f, y, 0.0f}; switch (mAlignment) { - case ALIGN_LEFT: + case PrimaryAlignment::ALIGN_LEFT: offset.x = mHorizontalMargin; break; - case ALIGN_CENTER: + case PrimaryAlignment::ALIGN_CENTER: offset.x = static_cast((mSize.x - entry.data.textCache->metrics.size.x) / 2.0f); if (offset.x < mHorizontalMargin) offset.x = mHorizontalMargin; break; - case ALIGN_RIGHT: + case PrimaryAlignment::ALIGN_RIGHT: offset.x = (mSize.x - entry.data.textCache->metrics.size.x); offset.x -= mHorizontalMargin; if (offset.x < mHorizontalMargin) @@ -340,122 +490,11 @@ template void TextListComponent::render(const glm::mat4& parentT y += entrySize; } mRenderer->popClipRect(); - List::listRenderTitleOverlay(trans); + if constexpr (std::is_same_v) + List::listRenderTitleOverlay(trans); GuiComponent::renderChildren(trans); } -template bool TextListComponent::input(InputConfig* config, Input input) -{ - if (size() > 0) { - if (input.value != 0) { - if (config->isMappedLike("down", input)) { - List::listInput(1); - return true; - } - - if (config->isMappedLike("up", input)) { - List::listInput(-1); - return true; - } - if (config->isMappedLike("rightshoulder", input)) { - List::listInput(10); - return true; - } - - if (config->isMappedLike("leftshoulder", input)) { - List::listInput(-10); - return true; - } - - if (config->isMappedLike("righttrigger", input)) { - return this->listLastRow(); - } - - if (config->isMappedLike("lefttrigger", input)) { - return this->listFirstRow(); - } - } - else { - if (config->isMappedLike("down", input) || config->isMappedLike("up", input) || - config->isMappedLike("rightshoulder", input) || - config->isMappedLike("leftshoulder", input) || - config->isMappedLike("lefttrigger", input) || - config->isMappedLike("righttrigger", input)) - List::stopScrolling(); - } - } - - return GuiComponent::input(config, input); -} - -template void TextListComponent::update(int deltaTime) -{ - List::listUpdate(deltaTime); - - if (mWindow->isScreensaverActive() || !mWindow->getAllowTextScrolling()) - List::stopScrolling(); - - if (!List::isScrolling() && size() > 0) { - // Always reset the loop offsets. - mLoopOffset = 0; - mLoopOffset2 = 0; - - // If we're not scrolling and this object's text exceeds our size, then loop it. - const float textLength {mFont - ->sizeText(Utils::String::toUpper( - mEntries.at(static_cast(mCursor)).name)) - .x}; - const float limit {mSize.x - mHorizontalMargin * 2.0f}; - - if (textLength > limit) { - // Loop the text. - const float speed {mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x * 0.247f}; - const float delay {3000.0f}; - const float scrollLength {textLength}; - const float returnLength {speed * 1.5f}; - const float scrollTime {(scrollLength * 1000.0f) / speed}; - const float returnTime {(returnLength * 1000.0f) / speed}; - const int maxTime {static_cast(delay + scrollTime + returnTime)}; - - mLoopTime += deltaTime; - while (mLoopTime > maxTime) - mLoopTime -= maxTime; - - mLoopOffset = static_cast(Utils::Math::loop(delay, scrollTime + returnTime, - static_cast(mLoopTime), - scrollLength + returnLength)); - - if (mLoopOffset > (scrollLength - (limit - returnLength))) - mLoopOffset2 = static_cast(mLoopOffset - (scrollLength + returnLength)); - } - } - - GuiComponent::update(deltaTime); -} - -// List management stuff. -template -void TextListComponent::add(const std::string& name, const T& obj, unsigned int color) -{ - assert(color < COLOR_ID_COUNT); - - typename IList::Entry entry; - entry.name = name; - entry.object = obj; - entry.data.colorId = color; - static_cast*>(this)->add(entry); -} - -template void TextListComponent::onCursorChanged(const CursorState& state) -{ - mLoopOffset = 0; - mLoopOffset2 = 0; - mLoopTime = 0; - - if (mCursorChangedCallback) - mCursorChangedCallback(state); -} - template void TextListComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, @@ -508,11 +547,11 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("horizontalAlignment")) { const std::string& str {elem->get("horizontalAlignment")}; if (str == "left") - setAlignment(ALIGN_LEFT); + setAlignment(PrimaryAlignment::ALIGN_LEFT); else if (str == "center") - setAlignment(ALIGN_CENTER); + setAlignment(PrimaryAlignment::ALIGN_CENTER); else if (str == "right") - setAlignment(ALIGN_RIGHT); + setAlignment(PrimaryAlignment::ALIGN_RIGHT); else LOG(LogWarning) << "TextListComponent: Invalid theme configuration, property " " defined as \"" @@ -522,11 +561,11 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, else if (elem->has("alignment")) { const std::string& str {elem->get("alignment")}; if (str == "left") - setAlignment(ALIGN_LEFT); + setAlignment(PrimaryAlignment::ALIGN_LEFT); else if (str == "center") - setAlignment(ALIGN_CENTER); + setAlignment(PrimaryAlignment::ALIGN_CENTER); else if (str == "right") - setAlignment(ALIGN_RIGHT); + setAlignment(PrimaryAlignment::ALIGN_RIGHT); else LOG(LogWarning) << "TextListComponent: Invalid theme configuration, property " " defined as \"" @@ -590,4 +629,35 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, } } +template void TextListComponent::onCursorChanged(const CursorState& state) +{ + mLoopOffset = 0; + mLoopOffset2 = 0; + mLoopTime = 0; + + if constexpr (std::is_same_v) { + float startPos {mCamOffset}; + float posMax {static_cast(mEntries.size())}; + float endPos {static_cast(mCursor)}; + + Animation* anim {new LambdaAnimation( + [this, startPos, endPos, posMax](float t) { + t -= 1; + float f {glm::mix(startPos, endPos, t * t * t + 1)}; + if (f < 0) + f += posMax; + if (f >= posMax) + f -= posMax; + + mCamOffset = f; + }, + 500)}; + + GuiComponent::setAnimation(anim, 0, nullptr, false, 0); + } + + if (mCursorChangedCallback) + mCursorChangedCallback(state); +} + #endif // ES_CORE_COMPONENTS_TEXT_LIST_COMPONENT_H