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.
This commit is contained in:
Leon Styhre 2022-03-24 23:05:23 +01:00
parent 5625f44a0a
commit 3a1c9d41ce
22 changed files with 1135 additions and 708 deletions

View file

@ -25,10 +25,6 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.h ${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.h
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.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 # GUIs
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.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/UIModeController.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.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 # GUIs
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp

View file

@ -215,9 +215,10 @@ void Screensaver::launchGame()
// Launching game // Launching game
ViewController::getInstance()->triggerGameLaunch(mCurrentGame); ViewController::getInstance()->triggerGameLaunch(mCurrentGame);
ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem()); ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem());
GamelistView* view = GamelistView* view {
ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get(); ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get()};
view->setCursor(mCurrentGame); view->setCursor(mCurrentGame);
view->stopListScrolling();
ViewController::getInstance()->cancelViewTransitions(); ViewController::getInstance()->cancelViewTransitions();
ViewController::getInstance()->pauseViewVideos(); ViewController::getInstance()->pauseViewVideos();
} }
@ -228,9 +229,10 @@ void Screensaver::goToGame()
if (mCurrentGame != nullptr) { if (mCurrentGame != nullptr) {
// Go to the game in the gamelist view, but don't launch it. // Go to the game in the gamelist view, but don't launch it.
ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem()); ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem());
GamelistView* view = GamelistView* view {
ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get(); ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get()};
view->setCursor(mCurrentGame); view->setCursor(mCurrentGame);
view->stopListScrolling();
ViewController::getInstance()->cancelViewTransitions(); ViewController::getInstance()->cancelViewTransitions();
} }
} }

View file

@ -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<GuiComponent> logo;
std::string logoPath;
std::string defaultLogoPath;
};
class CarouselComponent : public IList<CarouselElement, SystemData*>
{
public:
CarouselComponent();
void addEntry(const std::shared_ptr<ThemeData>& 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<void(CursorState state)>& func)
{
mCursorChangedCallback = func;
}
void setCancelTransitionsCallback(const std::function<void()>& 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<ThemeData>& 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<void(CursorState state)> mCursorChangedCallback;
std::function<void()> mCancelTransitionsCallback;
float mCamOffset;
int mPreviousScrollVelocity;
CarouselType mType;
std::shared_ptr<Font> 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

View file

@ -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"

View file

@ -121,17 +121,18 @@ void GuiMenu::openUIOptions()
Scripting::fireEvent("theme-changed", theme_set->getSelected(), Scripting::fireEvent("theme-changed", theme_set->getSelected(),
Settings::getInstance()->getString("ThemeSet")); Settings::getInstance()->getString("ThemeSet"));
Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); Settings::getInstance()->setString("ThemeSet", theme_set->getSelected());
CollectionSystemsManager::getInstance()->updateSystemsList();
mWindow->setChangedThemeSet(); mWindow->setChangedThemeSet();
// This is required so that the custom collection system does not disappear // 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 the user is editing a custom collection when switching theme sets.
if (CollectionSystemsManager::getInstance()->isEditing()) { if (CollectionSystemsManager::getInstance()->isEditing())
CollectionSystemsManager::getInstance()->exitEditMode(); 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->setNeedsSaving();
s->setNeedsReloading(); s->setNeedsReloading();
s->setNeedsGoToStart(); s->setNeedsGoToStart();
s->setNeedsCollectionsUpdate();
s->setInvalidateCachedBackground(); s->setInvalidateCachedBackground();
} }
}); });

View file

@ -130,7 +130,7 @@ GuiScraperSearch::GuiScraperSearch(SearchType type, unsigned int scrapeCount)
// Result list. // Result list.
mResultList = std::make_shared<ComponentList>(); mResultList = std::make_shared<ComponentList>();
mResultList->setCursorChangedCallback([this](CursorState state) { mResultList->setCursorChangedCallback([this](CursorState state) {
if (state == CURSOR_STOPPED) if (state == CursorState::CURSOR_STOPPED)
updateInfoPane(); updateInfoPane();
}); });

View file

@ -16,6 +16,7 @@
GamelistBase::GamelistBase(FileData* root) GamelistBase::GamelistBase(FileData* root)
: mRoot {root} : mRoot {root}
, mPrimary {nullptr}
, mRandomGame {nullptr} , mRandomGame {nullptr}
, mLastUpdated {nullptr} , mLastUpdated {nullptr}
, mGameCount {0} , mGameCount {0}
@ -25,33 +26,22 @@ GamelistBase::GamelistBase(FileData* root)
, mIsFiltered {false} , mIsFiltered {false}
, mIsFolder {false} , mIsFolder {false}
, mVideoPlaying {false} , mVideoPlaying {false}
, mLeftRightAvailable {true}
{ {
setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); 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) void GamelistBase::setCursor(FileData* cursor)
{ {
if (!mList.setCursor(cursor) && (!cursor->isPlaceHolder())) { if (!mPrimary->setCursor(cursor) && (!cursor->isPlaceHolder())) {
populateList(cursor->getParent()->getChildrenListToDisplay(), cursor->getParent()); 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 // Update our cursor stack in case our cursor just got set to some folder
// we weren't in before. // we weren't in before.
if (mCursorStack.empty() || mCursorStack.top() != cursor->getParent()) { if (mCursorStack.empty() || mCursorStack.top() != cursor->getParent()) {
std::stack<FileData*> tmp; std::stack<FileData*> tmp;
FileData* ptr = cursor->getParent(); FileData* ptr {cursor->getParent()};
while (ptr && ptr != mRoot) { while (ptr && ptr != mRoot) {
tmp.push(ptr); tmp.push(ptr);
@ -72,7 +62,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
{ {
if (input.value != 0) { if (input.value != 0) {
if (config->isMappedTo("a", input)) { if (config->isMappedTo("a", input)) {
FileData* cursor = getCursor(); FileData* cursor {getCursor()};
if (cursor->getType() == GAME) { if (cursor->getType() == GAME) {
pauseViewVideos(); pauseViewVideos();
ViewController::getInstance()->cancelViewTransitions(); ViewController::getInstance()->cancelViewTransitions();
@ -87,7 +77,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
mCursorStack.push(cursor); mCursorStack.push(cursor);
populateList(cursor->getChildrenListToDisplay(), cursor); populateList(cursor->getChildrenListToDisplay(), cursor);
FileData* newCursor = nullptr; FileData* newCursor {nullptr};
std::vector<FileData*> listEntries = cursor->getChildrenListToDisplay(); std::vector<FileData*> listEntries = cursor->getChildrenListToDisplay();
// Check if there is an entry in the cursor stack history matching any entry // Check if there is an entry in the cursor stack history matching any entry
// in the currect folder. If so, select that entry. // in the currect folder. If so, select that entry.
@ -105,6 +95,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
if (!newCursor) if (!newCursor)
newCursor = getCursor(); newCursor = getCursor();
setCursor(newCursor); setCursor(newCursor);
stopListScrolling();
if (mRoot->getSystem()->getThemeFolder() == "custom-collections") if (mRoot->getSystem()->getThemeFolder() == "custom-collections")
updateHelpPrompts(); updateHelpPrompts();
} }
@ -124,6 +115,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
populateList(mCursorStack.top()->getParent()->getChildrenListToDisplay(), populateList(mCursorStack.top()->getParent()->getChildrenListToDisplay(),
mCursorStack.top()->getParent()); mCursorStack.top()->getParent());
setCursor(mCursorStack.top()); setCursor(mCursorStack.top());
stopListScrolling();
if (mCursorStack.size() > 0) if (mCursorStack.size() > 0)
mCursorStack.pop(); mCursorStack.pop();
if (mRoot->getSystem()->getThemeFolder() == "custom-collections") if (mRoot->getSystem()->getThemeFolder() == "custom-collections")
@ -173,7 +165,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
} }
} }
else if (config->isMappedLike(getQuickSystemSelectRightButton(), input)) { else if (config->isMappedLike(getQuickSystemSelectRightButton(), input)) {
if (Settings::getInstance()->getBool("QuickSystemSelect") && if (mLeftRightAvailable && Settings::getInstance()->getBool("QuickSystemSelect") &&
SystemData::sSystemVector.size() > 1) { SystemData::sSystemVector.size() > 1) {
muteViewVideos(); muteViewVideos();
onFocusLost(); onFocusLost();
@ -183,7 +175,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
} }
} }
else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input)) { else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input)) {
if (Settings::getInstance()->getBool("QuickSystemSelect") && if (mLeftRightAvailable && Settings::getInstance()->getBool("QuickSystemSelect") &&
SystemData::sSystemVector.size() > 1) { SystemData::sSystemVector.size() > 1) {
muteViewVideos(); muteViewVideos();
onFocusLost(); onFocusLost();
@ -199,7 +191,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
stopListScrolling(); stopListScrolling();
// Jump to a random game. // Jump to a random game.
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
FileData* randomGame = getCursor()->getSystem()->getRandomGame(getCursor()); FileData* randomGame {getCursor()->getSystem()->getRandomGame(getCursor())};
if (randomGame) if (randomGame)
setCursor(randomGame); setCursor(randomGame);
return true; return true;
@ -214,8 +206,8 @@ bool GamelistBase::input(InputConfig* config, Input input)
NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND); NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND);
// If there is already an mCursorStackHistory entry for the collection, then // If there is already an mCursorStackHistory entry for the collection, then
// remove it so we don't get multiple entries. // remove it so we don't get multiple entries.
std::vector<FileData*> listEntries = std::vector<FileData*> listEntries {
mRandomGame->getSystem()->getRootFolder()->getChildrenListToDisplay(); mRandomGame->getSystem()->getRootFolder()->getChildrenListToDisplay()};
for (auto it = mCursorStackHistory.begin(); it != mCursorStackHistory.end(); ++it) { for (auto it = mCursorStackHistory.begin(); it != mCursorStackHistory.end(); ++it) {
if (std::find(listEntries.begin(), listEntries.end(), *it) != if (std::find(listEntries.begin(), listEntries.end(), *it) !=
listEntries.end()) { listEntries.end()) {
@ -224,6 +216,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
} }
} }
setCursor(mRandomGame); setCursor(mRandomGame);
stopListScrolling();
updateHelpPrompts(); updateHelpPrompts();
} }
else { 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 // 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 // it gets after the gamelist sorting. Instead retain the cursor position in the
// list using the logic below. // list using the logic below.
FileData* entryToUpdate = getCursor(); FileData* entryToUpdate {getCursor()};
SystemData* system = getCursor()->getSystem(); SystemData* system {getCursor()->getSystem()};
bool favoritesSorting; bool favoritesSorting;
bool removedLastFavorite = false; bool removedLastFavorite {false};
bool selectLastEntry = false; bool selectLastEntry {false};
bool isEditing = CollectionSystemsManager::getInstance()->isEditing(); bool isEditing {CollectionSystemsManager::getInstance()->isEditing()};
bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop"); bool foldersOnTop {Settings::getInstance()->getBool("FoldersOnTop")};
// If the current list only contains folders, then treat it as if the folders // 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 // are not sorted on top, this way the logic should work exactly as for mixed
// lists or files-only lists. // 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 // 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 // from ViewController instead of using the reference that existed before the
// destruction. Otherwise we get random crashes. // destruction. Otherwise we get random crashes.
GamelistView* view = GamelistView* view {
ViewController::getInstance()->getGamelistView(system).get(); ViewController::getInstance()->getGamelistView(system).get()};
// Jump to the first entry in the gamelist if the last favorite was unmarked. // Jump to the first entry in the gamelist if the last favorite was unmarked.
if (foldersOnTop && removedLastFavorite && if (foldersOnTop && removedLastFavorite &&
!entryToUpdate->getSystem()->isCustomCollection()) { !entryToUpdate->getSystem()->isCustomCollection()) {
@ -428,7 +421,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
setCursor(getFirstEntry()); setCursor(getFirstEntry());
view->setCursor(view->getFirstEntry()); view->setCursor(view->getFirstEntry());
} }
else if (selectLastEntry && mList.size() > 0) { else if (selectLastEntry && mPrimary->size() > 0) {
setCursor(getLastEntry()); setCursor(getLastEntry());
view->setCursor(view->getLastEntry()); view->setCursor(view->getLastEntry());
} }
@ -498,45 +491,65 @@ void GamelistBase::populateList(const std::vector<FileData*>& files, FileData* f
favoriteStar = Settings::getInstance()->getBool("FavoritesStar"); 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) { if (files.size() > 0) {
for (auto it = files.cbegin(); it != files.cend(); ++it) { for (auto it = files.cbegin(); it != files.cend(); ++it) {
if (!mFirstGameEntry && (*it)->getType() == GAME) if (mCarousel != nullptr) {
mFirstGameEntry = (*it); CarouselComponent<FileData*>::Entry carouselEntry;
// Add a leading tick mark icon to the game name if it's part of the custom collection carouselEntry.name = (*it)->getName();
// currently being edited. carouselEntry.object = *it;
if (isEditing && (*it)->getType() == GAME) { carouselEntry.data.logoPath = (*it)->getMarqueePath();
if (CollectionSystemsManager::getInstance()->inCustomCollection(editingCollection, mCarousel->addEntry(carouselEntry, theme);
(*it))) {
if (Settings::getInstance()->getBool("SpecialCharsASCII"))
inCollectionPrefix = "! ";
else
inCollectionPrefix = ViewController::TICKMARK_CHAR + " ";
}
else {
inCollectionPrefix = "";
}
} }
if ((*it)->getFavorite() && favoriteStar && if (mTextList != nullptr) {
mRoot->getSystem()->getName() != "favorites") { TextListComponent<FileData*>::Entry textListEntry;
if (Settings::getInstance()->getBool("SpecialCharsASCII")) if (!mFirstGameEntry && (*it)->getType() == GAME)
mList.add(inCollectionPrefix + "* " + (*it)->getName(), *it, mFirstGameEntry = (*it);
((*it)->getType() == FOLDER)); // Add a leading tick mark icon to the game name if it's part of the custom
else // collection currently being edited.
mList.add(inCollectionPrefix + ViewController::FAVORITE_CHAR + " " + if (isEditing && (*it)->getType() == GAME) {
(*it)->getName(), if (CollectionSystemsManager::getInstance()->inCustomCollection(
*it, ((*it)->getType() == FOLDER)); editingCollection, (*it))) {
} if (Settings::getInstance()->getBool("SpecialCharsASCII"))
else if ((*it)->getType() == FOLDER && mRoot->getSystem()->getName() != "collections") { inCollectionPrefix = "! ";
if (Settings::getInstance()->getBool("SpecialCharsASCII")) else
mList.add("# " + (*it)->getName(), *it, true); inCollectionPrefix = ViewController::TICKMARK_CHAR + " ";
else }
mList.add(ViewController::FOLDER_CHAR + " " + (*it)->getName(), *it, true); else {
} inCollectionPrefix = "";
else { }
mList.add(inCollectionPrefix + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); }
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<FileData*>& files, FileData* f
void GamelistBase::addPlaceholder(FileData* firstEntry) void GamelistBase::addPlaceholder(FileData* firstEntry)
{ {
// Empty list, add a placeholder. // Empty list, add a placeholder.
FileData* placeholder; FileData* placeholder {nullptr};
if (firstEntry && firstEntry->getSystem()->isGroupedCustomCollection()) if (firstEntry && firstEntry->getSystem()->isGroupedCustomCollection())
placeholder = firstEntry->getSystem()->getPlaceholder(); placeholder = firstEntry->getSystem()->getPlaceholder();
else else
placeholder = this->mRoot->getSystem()->getPlaceholder(); placeholder = this->mRoot->getSystem()->getPlaceholder();
mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER)); if (mTextList != nullptr) {
TextListComponent<FileData*>::Entry textListEntry;
textListEntry.name = placeholder->getName();
textListEntry.object = placeholder;
textListEntry.data.colorId = 1;
mTextList->addEntry(textListEntry);
}
if (mCarousel != nullptr) {
CarouselComponent<FileData*>::Entry carouselEntry;
carouselEntry.name = placeholder->getName();
carouselEntry.object = placeholder;
mCarousel->addEntry(carouselEntry, mRoot->getSystem()->getTheme());
}
} }
void GamelistBase::generateFirstLetterIndex(const std::vector<FileData*>& files) void GamelistBase::generateFirstLetterIndex(const std::vector<FileData*>& files)
@ -679,9 +704,10 @@ void GamelistBase::remove(FileData* game, bool deleteFile)
setCursor(siblings.at(gamePos - 1)); setCursor(siblings.at(gamePos - 1));
} }
} }
mList.remove(game);
if (mList.size() == 0) mPrimary->remove(game);
if (mPrimary->size() == 0)
addPlaceholder(nullptr); addPlaceholder(nullptr);
// If a game has been deleted, immediately remove the entry from gamelist.xml // If a game has been deleted, immediately remove the entry from gamelist.xml

View file

@ -21,23 +21,24 @@
#include "components/RatingComponent.h" #include "components/RatingComponent.h"
#include "components/ScrollableContainer.h" #include "components/ScrollableContainer.h"
#include "components/TextComponent.h" #include "components/TextComponent.h"
#include "components/TextListComponent.h"
#include "components/VideoFFmpegComponent.h" #include "components/VideoFFmpegComponent.h"
#include "components/primary/CarouselComponent.h"
#include "components/primary/TextListComponent.h"
#include <stack> #include <stack>
class GamelistBase : public GuiComponent class GamelistBase : public GuiComponent
{ {
public: public:
FileData* getCursor() { return mList.getSelected(); } FileData* getCursor() { return mPrimary->getSelected(); }
void setCursor(FileData*); void setCursor(FileData*);
bool input(InputConfig* config, Input input) override; bool input(InputConfig* config, Input input) override;
FileData* getNextEntry() { return mList.getNext(); } FileData* getNextEntry() { return mPrimary->getNext(); }
FileData* getPreviousEntry() { return mList.getPrevious(); } FileData* getPreviousEntry() { return mPrimary->getPrevious(); }
FileData* getFirstEntry() { return mList.getFirst(); } FileData* getFirstEntry() { return mPrimary->getFirst(); }
FileData* getLastEntry() { return mList.getLast(); } FileData* getLastEntry() { return mPrimary->getLast(); }
FileData* getFirstGameEntry() { return mFirstGameEntry; } FileData* getFirstGameEntry() { return mFirstGameEntry; }
// These functions are used to retain the folder cursor history, for instance // These functions are used to retain the folder cursor history, for instance
@ -58,9 +59,10 @@ public:
const std::vector<std::string>& getFirstLetterIndex() { return mFirstLetterIndex; } const std::vector<std::string>& getFirstLetterIndex() { return mFirstLetterIndex; }
void stopListScrolling() override { mPrimary->stopScrolling(); }
protected: protected:
GamelistBase(FileData* root); GamelistBase(FileData* root);
~GamelistBase();
// Called when a FileData* is added, has its metadata changed, or is removed. // Called when a FileData* is added, has its metadata changed, or is removed.
virtual void onFileChanged(FileData* file, bool reloadGamelist) = 0; virtual void onFileChanged(FileData* file, bool reloadGamelist) = 0;
@ -72,14 +74,15 @@ protected:
virtual void launch(FileData* game) = 0; virtual void launch(FileData* game) = 0;
bool isListScrolling() override { return mList.isScrolling(); } bool isListScrolling() override { return mPrimary->isScrolling(); }
void stopListScrolling() override { mList.stopScrolling(); }
std::string getQuickSystemSelectRightButton() { return "right"; } std::string getQuickSystemSelectRightButton() { return "right"; }
std::string getQuickSystemSelectLeftButton() { return "left"; } std::string getQuickSystemSelectLeftButton() { return "left"; }
FileData* mRoot; FileData* mRoot;
TextListComponent<FileData*> mList; std::unique_ptr<CarouselComponent<FileData*>> mCarousel;
std::unique_ptr<TextListComponent<FileData*>> mTextList;
PrimaryComponent<FileData*>* mPrimary;
// Points to the first game in the list, i.e. the first entry which is of the type "GAME". // Points to the first game in the list, i.e. the first entry which is of the type "GAME".
FileData* mFirstGameEntry; FileData* mFirstGameEntry;
@ -100,6 +103,7 @@ protected:
bool mIsFiltered; bool mIsFiltered;
bool mIsFolder; bool mIsFolder;
bool mVideoPlaying; bool mVideoPlaying;
bool mLeftRightAvailable;
private: private:
}; };

View file

@ -67,7 +67,7 @@ void GamelistView::legacyPopulateFields()
mImageComponents.back()->setThemeMetadata("image_md_image"); mImageComponents.back()->setThemeMetadata("image_md_image");
mImageComponents.back()->setOrigin(0.5f, 0.5f); mImageComponents.back()->setOrigin(0.5f, 0.5f);
mImageComponents.back()->setPosition(mSize.x * 0.25f, 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()->setMaxSize(mSize.x * (0.50f - 2.0f * padding), mSize.y * 0.4f);
mImageComponents.back()->setDefaultZIndex(30.0f); mImageComponents.back()->setDefaultZIndex(30.0f);
mImageComponents.back()->setScrollFadeIn(true); mImageComponents.back()->setScrollFadeIn(true);
@ -79,7 +79,7 @@ void GamelistView::legacyPopulateFields()
mVideoComponents.back()->setThemeMetadata("video_md_video"); mVideoComponents.back()->setThemeMetadata("video_md_video");
mVideoComponents.back()->setOrigin(0.5f, 0.5f); mVideoComponents.back()->setOrigin(0.5f, 0.5f);
mVideoComponents.back()->setPosition(mSize.x * 0.25f, 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()->setSize(mSize.x * (0.5f - 2.0f * padding), mSize.y * 0.4f);
mVideoComponents.back()->setDefaultZIndex(30.0f); mVideoComponents.back()->setDefaultZIndex(30.0f);
mVideoComponents.back()->setScrollFadeIn(true); mVideoComponents.back()->setScrollFadeIn(true);
@ -87,10 +87,11 @@ void GamelistView::legacyPopulateFields()
addChild(mVideoComponents.back().get()); addChild(mVideoComponents.back().get());
} }
mList.setPosition(mSize.x * (0.50f + padding), mList.getPosition().y); mPrimary->setPosition(mSize.x * (0.50f + padding), mPrimary->getPosition().y);
mList.setSize(mSize.x * (0.50f - padding), mList.getSize().y); mPrimary->setSize(mSize.x * (0.50f - padding), mPrimary->getSize().y);
mList.setAlignment(TextListComponent<FileData*>::ALIGN_LEFT); mPrimary->setAlignment(TextListComponent<FileData*>::PrimaryAlignment::ALIGN_LEFT);
mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); mPrimary->setCursorChangedCallback(
[&](const CursorState& state) { legacyUpdateInfoPanel(state); });
// Metadata labels + values. // Metadata labels + values.
mTextComponents.push_back(std::make_unique<TextComponent>()); mTextComponents.push_back(std::make_unique<TextComponent>());
@ -210,6 +211,11 @@ void GamelistView::legacyPopulateFields()
void GamelistView::legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme) void GamelistView::legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme)
{ {
if (mTextList == nullptr) {
mTextList = std::make_unique<TextListComponent<FileData*>>();
mPrimary = mTextList.get();
}
legacyPopulateFields(); legacyPopulateFields();
using namespace ThemeFlags; using namespace ThemeFlags;
@ -230,7 +236,11 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme)
for (auto extra : mThemeExtras) for (auto extra : mThemeExtras)
addChild(extra); 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( mImageComponents[LegacyImage::MD_THUMBNAIL]->applyTheme(
theme, getName(), mImageComponents[LegacyImage::MD_THUMBNAIL]->getThemeMetadata(), ALL); theme, getName(), mImageComponents[LegacyImage::MD_THUMBNAIL]->getThemeMetadata(), ALL);
@ -306,19 +316,22 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme)
container->setVisible(false); container->setVisible(false);
} }
populateList(mRoot->getChildrenListToDisplay(), mRoot);
sortChildren(); sortChildren();
mHelpStyle.applyTheme(mTheme, getName()); 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 the game data has already been rendered to the info panel, then skip it this time.
if (file == mLastUpdated) if (file == mLastUpdated)
return; return;
if (!mList.isScrolling()) if (state == CursorState::CURSOR_STOPPED)
mLastUpdated = file; mLastUpdated = file;
bool hideMetaDataFields {false}; 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, // 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. // or if we're in the grouped custom collection view.
if (mList.isScrolling()) { if (mPrimary->isScrolling()) {
if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") || if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") ||
(mLastUpdated->getSystem()->isCustomCollection() && (mLastUpdated->getSystem()->isCustomCollection() &&
mLastUpdated->getPath() == mLastUpdated->getSystem()->getName())) mLastUpdated->getPath() == mLastUpdated->getSystem()->getName()))
@ -566,10 +579,6 @@ void GamelistView::legacyUpdateInfoPanel()
for (auto it = comps.cbegin(); it != comps.cend(); ++it) { for (auto it = comps.cbegin(); it != comps.cend(); ++it) {
GuiComponent* comp {*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 playing, then animate if reverse != fadingOut.
// An animation is not playing, then animate if opacity != our target opacity. // An animation is not playing, then animate if opacity != our target opacity.
if ((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || if ((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) ||
@ -578,6 +587,9 @@ void GamelistView::legacyUpdateInfoPanel()
comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut);
} }
} }
if (state == CursorState::CURSOR_SCROLLING)
mLastUpdated = nullptr;
} }
void GamelistView::legacyUpdate(int deltaTime) void GamelistView::legacyUpdate(int deltaTime)

View file

@ -26,13 +26,6 @@ GamelistView::GamelistView(FileData* root)
if (mLegacyMode) if (mLegacyMode)
return; 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<FileData*>::ALIGN_LEFT);
mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); });
} }
GamelistView::~GamelistView() GamelistView::~GamelistView()
@ -79,9 +72,11 @@ void GamelistView::onShow()
GuiComponent::onShow(); GuiComponent::onShow();
if (mLegacyMode) if (mLegacyMode)
legacyUpdateInfoPanel(); legacyUpdateInfoPanel(CursorState::CURSOR_STOPPED);
else else
updateInfoPanel(); updateInfoPanel(CursorState::CURSOR_STOPPED);
mPrimary->finishAnimation(0);
} }
void GamelistView::onTransition() void GamelistView::onTransition()
@ -111,6 +106,44 @@ void GamelistView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
if (mTheme->hasView("gamelist")) { if (mTheme->hasView("gamelist")) {
for (auto& element : mTheme->getViewElements("gamelist").elements) { 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 <carousel> configuration entry";
continue;
}
if (element.second.type == "textlist" && mCarousel != nullptr) {
LOG(LogWarning) << "SystemView::populate(): Multiple primary components "
<< "defined, skipping <textlist> configuration entry";
continue;
}
}
if (element.second.type == "textlist") {
if (mTextList == nullptr) {
mTextList = std::make_unique<TextListComponent<FileData*>>();
mPrimary = mTextList.get();
}
mPrimary->setPosition(0.2f, mSize.y * 0.2f);
mPrimary->setSize(mSize.x * 0.7f, mSize.y * 0.6f);
mPrimary->setAlignment(TextListComponent<FileData*>::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<CarouselComponent<FileData*>>();
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") { if (element.second.type == "image") {
mImageComponents.push_back(std::make_unique<ImageComponent>()); mImageComponents.push_back(std::make_unique<ImageComponent>());
mImageComponents.back()->setDefaultZIndex(30.0f); mImageComponents.back()->setDefaultZIndex(30.0f);
@ -211,11 +244,31 @@ void GamelistView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
addChild(mRatingComponents.back().get()); addChild(mRatingComponents.back().get());
} }
} }
if (mPrimary == nullptr) {
mTextList = std::make_unique<TextListComponent<FileData*>>();
mPrimary = mTextList.get();
mPrimary->setPosition(0.2f, mSize.y * 0.2f);
mPrimary->setSize(mSize.x * 0.7f, mSize.y * 0.6f);
mPrimary->setAlignment(TextListComponent<FileData*>::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"); mHelpStyle.applyTheme(mTheme, "gamelist");
populateList(mRoot->getChildrenListToDisplay(), mRoot);
} }
mList.setDefaultZIndex(50.0f); // Disable quick system select if the primary component uses the left and right buttons.
mList.applyTheme(theme, "gamelist", "textlist_gamelist", ALL); if (mCarousel != nullptr) {
if (mCarousel->getType() == CarouselComponent<FileData*>::CarouselType::HORIZONTAL ||
mCarousel->getType() == CarouselComponent<FileData*>::CarouselType::HORIZONTAL_WHEEL)
mLeftRightAvailable = false;
}
sortChildren(); sortChildren();
} }
@ -259,7 +312,7 @@ std::vector<HelpPrompt> GamelistView::getHelpPrompts()
std::vector<HelpPrompt> prompts; std::vector<HelpPrompt> prompts;
if (Settings::getInstance()->getBool("QuickSystemSelect") && if (Settings::getInstance()->getBool("QuickSystemSelect") &&
SystemData::sSystemVector.size() > 1) SystemData::sSystemVector.size() > 1 && mLeftRightAvailable)
prompts.push_back(HelpPrompt("left/right", "system")); prompts.push_back(HelpPrompt("left/right", "system"));
if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && mCursorStack.empty() && if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && mCursorStack.empty() &&
@ -301,20 +354,22 @@ std::vector<HelpPrompt> GamelistView::getHelpPrompts()
return prompts; return prompts;
} }
void GamelistView::updateInfoPanel() void GamelistView::updateInfoPanel(const CursorState& state)
{ {
if (mLegacyMode) { if (mLegacyMode) {
legacyUpdateInfoPanel(); legacyUpdateInfoPanel(state);
return; 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 the game data has already been rendered to the info panel, then skip it this time.
if (file == mLastUpdated) if (file == mLastUpdated)
return; return;
if (!mList.isScrolling()) if (state == CursorState::CURSOR_STOPPED)
mLastUpdated = file; mLastUpdated = file;
bool hideMetaDataFields {false}; 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, // 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. // 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") || if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") ||
(mLastUpdated->getSystem()->isCustomCollection() && (mLastUpdated->getSystem()->isCustomCollection() &&
mLastUpdated->getPath() == mLastUpdated->getSystem()->getName())) mLastUpdated->getPath() == mLastUpdated->getSystem()->getName()))
@ -686,10 +741,6 @@ void GamelistView::updateInfoPanel()
for (auto it = comps.cbegin(); it != comps.cend(); ++it) { for (auto it = comps.cbegin(); it != comps.cend(); ++it) {
GuiComponent* comp {*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 playing, then animate if reverse != fadingOut.
// An animation is not playing, then animate if opacity != our target opacity. // An animation is not playing, then animate if opacity != our target opacity.
if ((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || if ((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) ||
@ -698,6 +749,9 @@ void GamelistView::updateInfoPanel()
comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut);
} }
} }
if (state == CursorState::CURSOR_SCROLLING)
mLastUpdated = nullptr;
} }
void GamelistView::setGameImage(FileData* file, GuiComponent* comp) void GamelistView::setGameImage(FileData* file, GuiComponent* comp)

View file

@ -25,7 +25,7 @@ public:
void onShow() override; void onShow() override;
void onTransition() override; void onTransition() override;
void preloadGamelist() { updateInfoPanel(); } void preloadGamelist() { updateInfoPanel(CursorState::CURSOR_STOPPED); }
void launch(FileData* game) override { ViewController::getInstance()->triggerGameLaunch(game); } void launch(FileData* game) override { ViewController::getInstance()->triggerGameLaunch(game); }
std::string getName() const std::string getName() const
@ -91,13 +91,13 @@ public:
std::vector<HelpPrompt> getHelpPrompts() override; std::vector<HelpPrompt> getHelpPrompts() override;
private: private:
void updateInfoPanel(); void updateInfoPanel(const CursorState& state);
void setGameImage(FileData* file, GuiComponent* comp); void setGameImage(FileData* file, GuiComponent* comp);
// Legacy (backward compatibility) functions. // Legacy (backward compatibility) functions.
void legacyPopulateFields(); void legacyPopulateFields();
void legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme); void legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme);
void legacyUpdateInfoPanel(); void legacyUpdateInfoPanel(const CursorState& state);
void legacyUpdate(int deltaTime); void legacyUpdate(int deltaTime);
void legacyInitMDLabels(); void legacyInitMDLabels();
void legacyInitMDValues(); void legacyInitMDValues();

View file

@ -23,6 +23,8 @@
SystemView::SystemView() SystemView::SystemView()
: mRenderer {Renderer::getInstance()} : mRenderer {Renderer::getInstance()}
, mPrimary {nullptr}
, mPrimaryType {PrimaryType::CAROUSEL}
, mCamOffset {0.0f} , mCamOffset {0.0f}
, mFadeOpacity {0.0f} , mFadeOpacity {0.0f}
, mPreviousScrollVelocity {0} , mPreviousScrollVelocity {0}
@ -34,20 +36,6 @@ SystemView::SystemView()
, mFadeTransitions {false} , mFadeTransitions {false}
{ {
setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight());
mCarousel = std::make_unique<CarouselComponent>();
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(); populate();
} }
@ -65,29 +53,29 @@ SystemView::~SystemView()
void SystemView::onTransition() void SystemView::onTransition()
{ {
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
anim->setPauseAnimation(true); anim->setPauseAnimation(true);
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
anim->setPauseAnimation(true); anim->setPauseAnimation(true);
} }
void SystemView::goToSystem(SystemData* system, bool animate) 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) if (selector->getGameSelection() == GameSelectorComponent::GameSelection::RANDOM)
selector->setNeedsRefresh(); selector->setNeedsRefresh();
} }
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
video->setStaticVideo(); video->setStaticVideo();
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
anim->resetFileAnimation(); anim->resetFileAnimation();
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
anim->resetFileAnimation(); anim->resetFileAnimation();
updateGameSelectors(); updateGameSelectors();
@ -111,9 +99,9 @@ bool SystemView::input(InputConfig* config, Input input)
} }
if (config->isMappedTo("a", input)) { if (config->isMappedTo("a", input)) {
mCarousel->stopScrolling(); mPrimary->stopScrolling();
pauseViewVideos(); pauseViewVideos();
ViewController::getInstance()->goToGamelist(mCarousel->getSelected()); ViewController::getInstance()->goToGamelist(mPrimary->getSelected());
NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND); NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND);
return true; return true;
} }
@ -122,7 +110,7 @@ bool SystemView::input(InputConfig* config, Input input)
config->isMappedTo("rightthumbstickclick", input))) { config->isMappedTo("rightthumbstickclick", input))) {
// Get a random system and jump to it. // Get a random system and jump to it.
NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND); NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND);
mCarousel->setCursor(SystemData::getRandomSystem(mCarousel->getSelected())); mPrimary->setCursor(SystemData::getRandomSystem(mPrimary->getSelected()));
return true; 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) void SystemView::update(int deltaTime)
{ {
if (!mCarousel->isAnimationPlaying(0)) if (!mPrimary->isAnimationPlaying(0))
mMaxFade = false; 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); video->update(deltaTime);
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
anim->update(deltaTime); anim->update(deltaTime);
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
anim->update(deltaTime); anim->update(deltaTime);
GuiComponent::update(deltaTime); GuiComponent::update(deltaTime);
@ -162,7 +150,7 @@ void SystemView::update(int deltaTime)
void SystemView::render(const glm::mat4& parentTrans) void SystemView::render(const glm::mat4& parentTrans)
{ {
if (mCarousel->getNumEntries() == 0) if (mPrimary->getNumEntries() == 0)
return; // Nothing to render. return; // Nothing to render.
bool fade {false}; bool fade {false};
@ -174,7 +162,7 @@ void SystemView::render(const glm::mat4& parentTrans)
renderElements(parentTrans, false); renderElements(parentTrans, false);
glm::mat4 trans {getTransform() * parentTrans}; 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. // For legacy themes the carousel is always rendered on top of all other elements.
if (!mLegacyMode && !fade) if (!mLegacyMode && !fade)
@ -191,11 +179,16 @@ void SystemView::onThemeChanged(const std::shared_ptr<ThemeData>& /*theme*/)
std::vector<HelpPrompt> SystemView::getHelpPrompts() std::vector<HelpPrompt> SystemView::getHelpPrompts()
{ {
std::vector<HelpPrompt> prompts; std::vector<HelpPrompt> prompts;
if (mCarousel->getType() == CarouselComponent::VERTICAL || if (mCarousel != nullptr) {
mCarousel->getType() == CarouselComponent::VERTICAL_WHEEL) if (mCarousel->getType() == CarouselComponent<SystemData*>::CarouselType::VERTICAL ||
mCarousel->getType() == CarouselComponent<SystemData*>::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")); prompts.push_back(HelpPrompt("up/down", "choose"));
else }
prompts.push_back(HelpPrompt("left/right", "choose"));
prompts.push_back(HelpPrompt("a", "select")); prompts.push_back(HelpPrompt("a", "select"));
@ -209,9 +202,9 @@ std::vector<HelpPrompt> SystemView::getHelpPrompts()
return prompts; 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) { for (auto& selector : mSystemElements[cursor].gameSelectors) {
if (selector->getGameSelection() == GameSelectorComponent::GameSelection::RANDOM) if (selector->getGameSelection() == GameSelectorComponent::GameSelection::RANDOM)
@ -221,34 +214,41 @@ void SystemView::onCursorChanged(const CursorState& /*state*/)
for (auto& video : mSystemElements[cursor].videoComponents) for (auto& video : mSystemElements[cursor].videoComponents)
video->setStaticVideo(); video->setStaticVideo();
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents) for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
anim->resetFileAnimation(); anim->resetFileAnimation();
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents) for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
anim->resetFileAnimation(); anim->resetFileAnimation();
updateGameSelectors(); updateGameSelectors();
startViewVideos(); startViewVideos();
updateHelpPrompts(); updateHelpPrompts();
int scrollVelocity {mCarousel->getScrollingVelocity()}; int scrollVelocity {mPrimary->getScrollingVelocity()};
float startPos {mCamOffset}; float startPos {mCamOffset};
float posMax {static_cast<float>(mCarousel->getNumEntries())}; float posMax {static_cast<float>(mPrimary->getNumEntries())};
float target {static_cast<float>(cursor)}; float target {static_cast<float>(cursor)};
float endPos {target};
// Find the shortest path to the target. if (mPreviousScrollVelocity > 0 && scrollVelocity == 0 && mCamOffset > posMax - 1.0f)
float endPos {target}; // Directly. startPos = 0.0f;
float dist {fabs(endPos - startPos)};
if (fabs(target + posMax - startPos - scrollVelocity) < dist) if (mPrimaryType == PrimaryType::CAROUSEL) {
endPos = target + posMax; // Loop around the end (0 -> max). // Find the shortest path to the target.
if (fabs(target - posMax - startPos - scrollVelocity) < dist) float dist {fabs(endPos - startPos)};
endPos = target - posMax; // Loop around the start (max - 1 -> -1).
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. // Make sure transitions do not animate in reverse.
bool changedDirection {false}; bool changedDirection {false};
if (mPreviousScrollVelocity != 0 && mPreviousScrollVelocity != scrollVelocity) if (mPreviousScrollVelocity != 0 && mPreviousScrollVelocity != scrollVelocity) {
changedDirection = true; if (scrollVelocity > 0 && startPos + scrollVelocity < posMax)
changedDirection = true;
}
if (!changedDirection && scrollVelocity > 0 && endPos < startPos) if (!changedDirection && scrollVelocity > 0 && endPos < startPos)
endPos = endPos + posMax; endPos = endPos + posMax;
@ -374,8 +374,27 @@ void SystemView::populate()
std::string logoPath; std::string logoPath;
std::string defaultLogoPath; std::string defaultLogoPath;
if (mLegacyMode && mViewNeedsReload) if (mLegacyMode && mViewNeedsReload) {
if (mCarousel == nullptr) {
mCarousel = std::make_unique<CarouselComponent<SystemData*>>();
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); legacyApplyTheme(theme);
}
if (mLegacyMode) { if (mLegacyMode) {
SystemViewElements elements; SystemViewElements elements;
@ -404,12 +423,51 @@ void SystemView::populate()
ThemeFlags::ALL); ThemeFlags::ALL);
elements.gameSelectors.back()->setNeedsRefresh(); elements.gameSelectors.back()->setNeedsRefresh();
} }
if (element.second.type == "carousel") { if (element.second.type == "textlist" || element.second.type == "carousel") {
mCarousel->applyTheme(theme, "system", element.first, ThemeFlags::ALL); if (element.second.type == "carousel" && mTextList != nullptr) {
if (element.second.has("logo")) LOG(LogWarning)
logoPath = element.second.get<std::string>("logo"); << "SystemView::populate(): Multiple primary components "
if (element.second.has("defaultLogo")) << "defined, skipping <carousel> configuration entry";
defaultLogoPath = element.second.get<std::string>("defaultLogo"); continue;
}
if (element.second.type == "textlist" && mCarousel != nullptr) {
LOG(LogWarning)
<< "SystemView::populate(): Multiple primary components "
<< "defined, skipping <textlist> configuration entry";
continue;
}
if (element.second.type == "carousel" && mCarousel == nullptr) {
mCarousel = std::make_unique<CarouselComponent<SystemData*>>();
mPrimary = mCarousel.get();
mPrimaryType = PrimaryType::CAROUSEL;
}
else if (element.second.type == "textlist" && mTextList == nullptr) {
mTextList = std::make_unique<TextListComponent<SystemData*>>();
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<std::string>("logo");
if (element.second.has("defaultLogo"))
defaultLogoPath = element.second.get<std::string>("defaultLogo");
}
} }
else if (element.second.type == "image") { else if (element.second.type == "image") {
elements.imageComponents.emplace_back(std::make_unique<ImageComponent>()); elements.imageComponents.emplace_back(std::make_unique<ImageComponent>());
@ -500,10 +558,6 @@ void SystemView::populate()
} }
} }
} }
else {
// Apply default carousel configuration.
mCarousel->applyTheme(theme, "system", "", ThemeFlags::ALL);
}
std::stable_sort( std::stable_sort(
elements.children.begin(), elements.children.end(), elements.children.begin(), elements.children.end(),
@ -523,13 +577,41 @@ void SystemView::populate()
mSystemElements.back().helpStyle.applyTheme(theme, "system"); mSystemElements.back().helpStyle.applyTheme(theme, "system");
} }
CarouselComponent::Entry entry; if (mPrimary == nullptr) {
entry.name = it->getName(); mCarousel = std::make_unique<CarouselComponent<SystemData*>>();
entry.object = it; mPrimary = mCarousel.get();
entry.data.logoPath = logoPath; mPrimaryType = PrimaryType::CAROUSEL;
entry.data.defaultLogoPath = defaultLogoPath; 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<SystemData*>::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<SystemData*>::Entry entry;
entry.name = it->getFullName();
entry.object = it;
entry.data.colorId = 0;
mTextList->addEntry(entry);
}
} }
for (auto& elements : mSystemElements) { 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. // Something is wrong, there is not a single system to show, check if UI mode is not full.
if (!UIModeController::getInstance()->isUIModeFull()) { if (!UIModeController::getInstance()->isUIModeFull()) {
Settings::getInstance()->setString("UIMode", "full"); Settings::getInstance()->setString("UIMode", "full");
@ -562,21 +644,21 @@ void SystemView::populate()
void SystemView::updateGameCount() void SystemView::updateGameCount()
{ {
std::pair<unsigned int, unsigned int> gameCount = std::pair<unsigned int, unsigned int> gameCount =
mCarousel->getSelected()->getDisplayedGameCount(); mPrimary->getSelected()->getDisplayedGameCount();
std::stringstream ss; std::stringstream ss;
std::stringstream ssGames; std::stringstream ssGames;
std::stringstream ssFavorites; std::stringstream ssFavorites;
bool games {false}; bool games {false};
if (!mCarousel->getSelected()->isGameSystem()) { if (!mPrimary->getSelected()->isGameSystem()) {
ss << "Configuration"; ss << "Configuration";
} }
else if (mCarousel->getSelected()->isCollection() && else if (mPrimary->getSelected()->isCollection() &&
(mCarousel->getSelected()->getName() == "favorites")) { (mPrimary->getSelected()->getName() == "favorites")) {
ss << gameCount.first << " Game" << (gameCount.first == 1 ? " " : "s"); ss << gameCount.first << " Game" << (gameCount.first == 1 ? " " : "s");
} }
else if (mCarousel->getSelected()->isCollection() && else if (mPrimary->getSelected()->isCollection() &&
(mCarousel->getSelected()->getName() == "recent")) { (mPrimary->getSelected()->getName() == "recent")) {
// The "recent" gamelist has probably been trimmed after sorting, so we'll cap it at // The "recent" gamelist has probably been trimmed after sorting, so we'll cap it at
// its maximum limit of 50 games. // its maximum limit of 50 games.
ss << (gameCount.first > 50 ? 50 : gameCount.first) << " Game" ss << (gameCount.first > 50 ? 50 : gameCount.first) << " Game"
@ -594,7 +676,7 @@ void SystemView::updateGameCount()
mLegacySystemInfo->setText(ss.str()); mLegacySystemInfo->setText(ss.str());
} }
else { else {
for (auto& gameCount : mSystemElements[mCarousel->getCursor()].gameCountComponents) { for (auto& gameCount : mSystemElements[mPrimary->getCursor()].gameCountComponents) {
if (gameCount->getThemeSystemdata() == "gamecount") { if (gameCount->getThemeSystemdata() == "gamecount") {
gameCount->setValue(ss.str()); gameCount->setValue(ss.str());
} }
@ -619,7 +701,7 @@ void SystemView::updateGameSelectors()
if (mLegacyMode) if (mLegacyMode)
return; return;
int cursor {mCarousel->getCursor()}; int cursor {mPrimary->getCursor()};
if (mSystemElements[cursor].gameSelectors.size() == 0) if (mSystemElements[cursor].gameSelectors.size() == 0)
return; return;
@ -1052,10 +1134,13 @@ void SystemView::legacyApplyTheme(const std::shared_ptr<ThemeData>& theme)
else else
mViewNeedsReload = true; 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->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->setBackgroundColor(0xDDDDDDD8);
mLegacySystemInfo->setRenderBackground(true); mLegacySystemInfo->setRenderBackground(true);
mLegacySystemInfo->setFont( mLegacySystemInfo->setFont(
@ -1076,33 +1161,41 @@ void SystemView::renderElements(const glm::mat4& parentTrans, bool abovePrimary)
{ {
glm::mat4 trans {getTransform() * parentTrans}; glm::mat4 trans {getTransform() * parentTrans};
const float primaryZIndex {mCarousel->getZIndex()}; const float primaryZIndex {mPrimary->getZIndex()};
int renderLeft {static_cast<int>(mCamOffset)}; int renderBefore {static_cast<int>(mCamOffset)};
int renderRight {static_cast<int>(mCamOffset)}; int renderAfter {static_cast<int>(mCamOffset)};
// If we're transitioning then also render the previous and next systems. // If we're transitioning then also render the previous and next systems.
if (mCarousel->isAnimationPlaying(0)) { if (mPrimary->isAnimationPlaying(0)) {
renderLeft -= 1; renderBefore -= 1;
renderRight += 1; renderAfter += 1;
} }
for (int i = renderLeft; i <= renderRight; ++i) { for (int i = renderBefore; i <= renderAfter; ++i) {
int index {i}; int index {i};
while (index < 0) while (index < 0)
index += static_cast<int>(mCarousel->getNumEntries()); index += static_cast<int>(mPrimary->getNumEntries());
while (index >= static_cast<int>(mCarousel->getNumEntries())) while (index >= static_cast<int>(mPrimary->getNumEntries()))
index -= static_cast<int>(mCarousel->getNumEntries()); index -= static_cast<int>(mPrimary->getNumEntries());
if (mCarousel->isAnimationPlaying(0) || index == mCarousel->getCursor()) { if (mPrimary->isAnimationPlaying(0) || index == mPrimary->getCursor()) {
glm::mat4 elementTrans {trans}; glm::mat4 elementTrans {trans};
if (mCarousel->getType() == CarouselComponent::HORIZONTAL || if (mCarousel != nullptr) {
mCarousel->getType() == CarouselComponent::HORIZONTAL_WHEEL) if (mCarousel->getType() ==
elementTrans = glm::translate(elementTrans, CarouselComponent<SystemData*>::CarouselType::HORIZONTAL ||
glm::vec3 {(i - mCamOffset) * mSize.x, 0.0f, 0.0f}); mCarousel->getType() ==
else CarouselComponent<SystemData*>::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, elementTrans = glm::translate(elementTrans,
glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f}); glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f});
}
mRenderer->pushClipRect( mRenderer->pushClipRect(
glm::ivec2 {static_cast<int>(glm::round(elementTrans[3].x)), glm::ivec2 {static_cast<int>(glm::round(elementTrans[3].x)),

View file

@ -13,42 +13,26 @@
#include "GuiComponent.h" #include "GuiComponent.h"
#include "Sound.h" #include "Sound.h"
#include "SystemData.h" #include "SystemData.h"
#include "components/CarouselComponent.h"
#include "components/DateTimeComponent.h" #include "components/DateTimeComponent.h"
#include "components/GIFAnimComponent.h" #include "components/GIFAnimComponent.h"
#include "components/GameSelectorComponent.h" #include "components/GameSelectorComponent.h"
#include "components/LottieAnimComponent.h" #include "components/LottieAnimComponent.h"
#include "components/RatingComponent.h" #include "components/RatingComponent.h"
#include "components/TextComponent.h" #include "components/TextComponent.h"
#include "components/TextListComponent.h"
#include "components/VideoFFmpegComponent.h" #include "components/VideoFFmpegComponent.h"
#include "components/primary/CarouselComponent.h"
#include "components/primary/TextListComponent.h"
#include "resources/Font.h" #include "resources/Font.h"
#include <memory> #include <memory>
class SystemData; class SystemData;
struct SystemViewElements {
HelpStyle helpStyle;
std::string name;
std::string fullName;
std::vector<std::unique_ptr<GameSelectorComponent>> gameSelectors;
std::vector<GuiComponent*> legacyExtras;
std::vector<GuiComponent*> children;
std::vector<std::unique_ptr<ImageComponent>> imageComponents;
std::vector<std::unique_ptr<VideoFFmpegComponent>> videoComponents;
std::vector<std::unique_ptr<LottieAnimComponent>> lottieAnimComponents;
std::vector<std::unique_ptr<GIFAnimComponent>> GIFAnimComponents;
std::vector<std::unique_ptr<TextComponent>> gameCountComponents;
std::vector<std::unique_ptr<TextComponent>> textComponents;
std::vector<std::unique_ptr<DateTimeComponent>> dateTimeComponents;
std::vector<std::unique_ptr<RatingComponent>> ratingComponents;
};
class SystemView : public GuiComponent class SystemView : public GuiComponent
{ {
public: public:
using PrimaryType = PrimaryComponent<SystemData*>::PrimaryType;
SystemView(); SystemView();
~SystemView(); ~SystemView();
@ -59,47 +43,49 @@ public:
void update(int deltaTime) override; void update(int deltaTime) override;
void render(const glm::mat4& parentTrans) override; void render(const glm::mat4& parentTrans) override;
bool isScrolling() { return mCarousel->isScrolling(); } bool isScrolling() { return mPrimary->isScrolling(); }
void stopScrolling() { mCarousel->stopScrolling(); } void stopScrolling() { mPrimary->stopScrolling(); }
bool isSystemAnimationPlaying(unsigned char slot) bool isSystemAnimationPlaying(unsigned char slot) { return mPrimary->isAnimationPlaying(slot); }
{
return mCarousel->isAnimationPlaying(slot);
}
void finishSystemAnimation(unsigned char slot) void finishSystemAnimation(unsigned char slot)
{ {
finishAnimation(slot); finishAnimation(slot);
mCarousel->finishAnimation(slot); mPrimary->finishAnimation(slot);
} }
CarouselComponent::CarouselType getCarouselType() { return mCarousel->getType(); } PrimaryComponent<SystemData*>::PrimaryType getPrimaryType() { return mPrimaryType; }
SystemData* getFirstSystem() { return mCarousel->getFirst(); } CarouselComponent<SystemData*>::CarouselType getCarouselType()
{
return (mCarousel != nullptr) ? mCarousel->getType() :
CarouselComponent<SystemData*>::CarouselType::NO_CAROUSEL;
}
SystemData* getFirstSystem() { return mPrimary->getFirst(); }
void startViewVideos() override void startViewVideos() override
{ {
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
video->startVideoPlayer(); video->startVideoPlayer();
} }
void stopViewVideos() override void stopViewVideos() override
{ {
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
video->stopVideoPlayer(); video->stopVideoPlayer();
} }
void pauseViewVideos() override void pauseViewVideos() override
{ {
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) { for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) {
video->pauseVideoPlayer(); video->pauseVideoPlayer();
} }
} }
void muteViewVideos() override void muteViewVideos() override
{ {
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
video->muteVideoPlayer(); video->muteVideoPlayer();
} }
void onThemeChanged(const std::shared_ptr<ThemeData>& theme); void onThemeChanged(const std::shared_ptr<ThemeData>& theme);
std::vector<HelpPrompt> getHelpPrompts() override; std::vector<HelpPrompt> getHelpPrompts() override;
HelpStyle getHelpStyle() override { return mSystemElements[mCarousel->getCursor()].helpStyle; } HelpStyle getHelpStyle() override { return mSystemElements[mPrimary->getCursor()].helpStyle; }
protected: protected:
void onCursorChanged(const CursorState& state); void onCursorChanged(const CursorState& state);
@ -111,10 +97,31 @@ private:
void legacyApplyTheme(const std::shared_ptr<ThemeData>& theme); void legacyApplyTheme(const std::shared_ptr<ThemeData>& theme);
void renderElements(const glm::mat4& parentTrans, bool abovePrimary); void renderElements(const glm::mat4& parentTrans, bool abovePrimary);
struct SystemViewElements {
HelpStyle helpStyle;
std::string name;
std::string fullName;
std::vector<std::unique_ptr<GameSelectorComponent>> gameSelectors;
std::vector<GuiComponent*> legacyExtras;
std::vector<GuiComponent*> children;
std::vector<std::unique_ptr<ImageComponent>> imageComponents;
std::vector<std::unique_ptr<VideoFFmpegComponent>> videoComponents;
std::vector<std::unique_ptr<LottieAnimComponent>> lottieAnimComponents;
std::vector<std::unique_ptr<GIFAnimComponent>> GIFAnimComponents;
std::vector<std::unique_ptr<TextComponent>> gameCountComponents;
std::vector<std::unique_ptr<TextComponent>> textComponents;
std::vector<std::unique_ptr<DateTimeComponent>> dateTimeComponents;
std::vector<std::unique_ptr<RatingComponent>> ratingComponents;
};
Renderer* mRenderer; Renderer* mRenderer;
std::unique_ptr<CarouselComponent> mCarousel; std::unique_ptr<CarouselComponent<SystemData*>> mCarousel;
std::unique_ptr<TextListComponent<SystemData*>> mTextList;
std::unique_ptr<TextComponent> mLegacySystemInfo; std::unique_ptr<TextComponent> mLegacySystemInfo;
std::vector<SystemViewElements> mSystemElements; std::vector<SystemViewElements> mSystemElements;
PrimaryComponent<SystemData*>* mPrimary;
PrimaryType mPrimaryType;
float mCamOffset; float mCamOffset;
float mFadeOpacity; float mFadeOpacity;

View file

@ -355,19 +355,33 @@ void ViewController::goToSystemView(SystemData* system, bool playTransition)
if (applicationStartup) { if (applicationStartup) {
mCamera = glm::translate(mCamera, -mCurrentView->getPosition()); mCamera = glm::translate(mCamera, -mCurrentView->getPosition());
if (Settings::getInstance()->getString("TransitionStyle") == "slide") { if (Settings::getInstance()->getString("TransitionStyle") == "slide") {
if (getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL || if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) {
getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL_WHEEL) if (getSystemListView()->getCarouselType() ==
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL ||
getSystemListView()->getCarouselType() ==
CarouselComponent<SystemData*>::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(); mCamera[3].y += Renderer::getScreenHeight();
else }
mCamera[3].x -= Renderer::getScreenWidth();
updateHelpPrompts(); updateHelpPrompts();
} }
else if (Settings::getInstance()->getString("TransitionStyle") == "fade") { else if (Settings::getInstance()->getString("TransitionStyle") == "fade") {
if (getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL || if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) {
getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL_WHEEL) if (getSystemListView()->getCarouselType() ==
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL ||
getSystemListView()->getCarouselType() ==
CarouselComponent<SystemData*>::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(); mCamera[3].y += Renderer::getScreenHeight();
else }
mCamera[3].x += Renderer::getScreenWidth();
} }
else { else {
updateHelpPrompts(); updateHelpPrompts();
@ -415,9 +429,9 @@ void ViewController::goToPrevGamelist()
void ViewController::goToGamelist(SystemData* system) void ViewController::goToGamelist(SystemData* system)
{ {
bool wrapFirstToLast = false; bool wrapFirstToLast {false};
bool wrapLastToFirst = false; bool wrapLastToFirst {false};
bool slideTransitions = false; bool slideTransitions {false};
if (mCurrentView != nullptr) if (mCurrentView != nullptr)
mCurrentView->onTransition(); mCurrentView->onTransition();
@ -521,6 +535,7 @@ void ViewController::goToGamelist(SystemData* system)
} }
mCurrentView = getGamelistView(system); mCurrentView = getGamelistView(system);
mCurrentView->finishAnimation(0);
// Application startup animation, if starting in a gamelist rather than in the system view. // Application startup animation, if starting in a gamelist rather than in the system view.
if (mState.viewing == NOTHING) { if (mState.viewing == NOTHING) {

View file

@ -31,7 +31,12 @@ set(CORE_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/LambdaAnimation.h ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/LambdaAnimation.h
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/MoveCameraAnimation.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/AnimatedImageComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgeComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgeComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.h
@ -111,7 +116,7 @@ set(CORE_SOURCES
# Animations # Animations
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/AnimationController.cpp ${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/AnimatedImageComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgeComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgeComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp

View file

@ -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 // up. So scale it to full size so it won't be stuck at a smaller size when returning
// from the submenu. // from the submenu.
mTopScale = 1.0f; mTopScale = 1.0f;
GuiComponent* menu = mGuiStack.back(); GuiComponent* menu {mGuiStack.back()};
glm::vec2 menuCenter {menu->getCenter()}; glm::vec2 menuCenter {menu->getCenter()};
menu->setOrigin(0.5f, 0.5f); menu->setOrigin(0.5f, 0.5f);
menu->setPosition(menuCenter.x, menuCenter.y, 0.0f); menu->setPosition(menuCenter.x, menuCenter.y, 0.0f);

View file

@ -13,7 +13,7 @@
#define TOTAL_HORIZONTAL_PADDING_PX 20.0f #define TOTAL_HORIZONTAL_PADDING_PX 20.0f
ComponentList::ComponentList() ComponentList::ComponentList()
: IList<ComponentListRow, void*> {LIST_SCROLL_STYLE_SLOW, LIST_NEVER_LOOP} : IList<ComponentListRow, void*> {LIST_SCROLL_STYLE_SLOW, ListLoopType::LIST_NEVER_LOOP}
, mRenderer {Renderer::getInstance()} , mRenderer {Renderer::getInstance()}
, mFocused {false} , mFocused {false}
, mSetupCompleted {false} , mSetupCompleted {false}
@ -53,7 +53,7 @@ void ComponentList::addRow(const ComponentListRow& row, bool setCursorHere)
if (setCursorHere) { if (setCursorHere) {
mCursor = static_cast<int>(mEntries.size()) - 1; mCursor = static_cast<int>(mEntries.size()) - 1;
onCursorChanged(CURSOR_STOPPED); onCursorChanged(CursorState::CURSOR_STOPPED);
} }
} }

View file

@ -11,6 +11,7 @@
#include "GuiComponent.h" #include "GuiComponent.h"
#include "Log.h" #include "Log.h"
#include "Settings.h"
#include "ThemeData.h" #include "ThemeData.h"
class GameSelectorComponent : public GuiComponent class GameSelectorComponent : public GuiComponent

View file

@ -13,14 +13,15 @@
#include "components/ImageComponent.h" #include "components/ImageComponent.h"
#include "utils/StringUtil.h" #include "utils/StringUtil.h"
enum CursorState { enum class CursorState {
CURSOR_STOPPED, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). CURSOR_STOPPED, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
CURSOR_SCROLLING CURSOR_SCROLLING
}; };
enum ListLoopType { enum class ListLoopType {
LIST_ALWAYS_LOOP, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). LIST_ALWAYS_LOOP, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
LIST_PAUSE_AT_END, LIST_PAUSE_AT_END,
LIST_PAUSE_AT_END_ON_JUMP,
LIST_NEVER_LOOP LIST_NEVER_LOOP
}; };
@ -57,7 +58,7 @@ const ScrollTierList LIST_SCROLL_STYLE_SLOW = {
}; };
// clang-format on // clang-format on
template <typename EntryData, typename UserData> class IList : public GuiComponent template <typename EntryData, typename UserData> class IList : public virtual GuiComponent
{ {
public: public:
struct Entry { struct Entry {
@ -67,6 +68,10 @@ public:
}; };
protected: protected:
Window* mWindow;
std::vector<Entry> mEntries;
const ScrollTierList& mTierList;
const ListLoopType mLoopType;
int mCursor; int mCursor;
int mScrollTier; int mScrollTier;
int mScrollVelocity; int mScrollVelocity;
@ -76,32 +81,23 @@ protected:
float mTitleOverlayOpacity; float mTitleOverlayOpacity;
unsigned int mTitleOverlayColor; unsigned int mTitleOverlayColor;
const ScrollTierList& mTierList;
const ListLoopType mLoopType;
std::vector<Entry> mEntries;
Window* mWindow;
public: public:
IList(const ScrollTierList& tierList = LIST_SCROLL_STYLE_QUICK, IList(const ScrollTierList& tierList = LIST_SCROLL_STYLE_QUICK,
const ListLoopType& loopType = LIST_PAUSE_AT_END) const ListLoopType& loopType = ListLoopType::LIST_PAUSE_AT_END)
: mTierList {tierList} : mWindow {Window::getInstance()}
, mTierList {tierList}
, mLoopType {loopType} , 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); } const bool isScrolling() const { return (mScrollVelocity != 0 && mScrollTier > 0); }
int getScrollingVelocity() { return mScrollVelocity; }
void stopScrolling() void stopScrolling()
{ {
@ -109,15 +105,17 @@ public:
listInput(0); listInput(0);
if (mScrollVelocity == 0) if (mScrollVelocity == 0)
onCursorChanged(CURSOR_STOPPED); onCursorChanged(CursorState::CURSOR_STOPPED);
} }
const int getScrollingVelocity() const { return mScrollVelocity; }
void clear() void clear()
{ {
mEntries.clear(); mEntries.clear();
mCursor = 0; mCursor = 0;
listInput(0); listInput(0);
onCursorChanged(CURSOR_STOPPED); onCursorChanged(CursorState::CURSOR_STOPPED);
} }
const std::string& getSelectedName() const std::string& getSelectedName()
@ -166,16 +164,15 @@ public:
{ {
assert(it != mEntries.cend()); assert(it != mEntries.cend());
mCursor = it - mEntries.cbegin(); 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) bool setCursor(const UserData& obj)
{ {
for (auto it = mEntries.cbegin(); it != mEntries.cend(); ++it) { for (auto it = mEntries.cbegin(); it != mEntries.cend(); ++it) {
if ((*it).object == obj) { if ((*it).object == obj) {
mCursor = static_cast<int>(it - mEntries.cbegin()); mCursor = static_cast<int>(it - mEntries.cbegin());
onCursorChanged(CURSOR_STOPPED); onCursorChanged(CursorState::CURSOR_STOPPED);
return true; return true;
} }
} }
@ -205,7 +202,7 @@ protected:
{ {
if (mCursor > 0 && it - mEntries.cbegin() <= mCursor) { if (mCursor > 0 && it - mEntries.cbegin() <= mCursor) {
--mCursor; --mCursor;
onCursorChanged(CURSOR_STOPPED); onCursorChanged(CursorState::CURSOR_STOPPED);
} }
mEntries.erase(it); mEntries.erase(it);
@ -214,7 +211,7 @@ protected:
bool listFirstRow() bool listFirstRow()
{ {
mCursor = 0; mCursor = 0;
onCursorChanged(CURSOR_STOPPED); onCursorChanged(CursorState::CURSOR_STOPPED);
onScroll(); onScroll();
return true; return true;
} }
@ -222,7 +219,7 @@ protected:
bool listLastRow() bool listLastRow()
{ {
mCursor = static_cast<int>(mEntries.size()) - 1; mCursor = static_cast<int>(mEntries.size()) - 1;
onCursorChanged(CURSOR_STOPPED); onCursorChanged(CursorState::CURSOR_STOPPED);
onScroll(); onScroll();
return true; return true;
} }
@ -257,7 +254,7 @@ protected:
// We delay scrolling until after scroll tier has updated so isScrolling() returns // We delay scrolling until after scroll tier has updated so isScrolling() returns
// accurately during onCursorChanged callbacks. We don't just do scroll tier first // accurately during onCursorChanged callbacks. We don't just do scroll tier first
// because it would not catch the scrollDelay == tier length case. // because it would not catch the scrollDelay == tier length case.
int scrollCount = 0; int scrollCount {0};
while (mScrollCursorAccumulator >= mTierList.tiers[mScrollTier].scrollDelay) { while (mScrollCursorAccumulator >= mTierList.tiers[mScrollTier].scrollDelay) {
mScrollCursorAccumulator -= mTierList.tiers[mScrollTier].scrollDelay; mScrollCursorAccumulator -= mTierList.tiers[mScrollTier].scrollDelay;
++scrollCount; ++scrollCount;
@ -275,45 +272,47 @@ protected:
scroll(mScrollVelocity); scroll(mScrollVelocity);
} }
void listRenderTitleOverlay(const glm::mat4& /*trans*/) void listRenderTitleOverlay(const glm::mat4&)
{ {
if (!Settings::getInstance()->getBool("ListScrollOverlay")) if constexpr (std::is_same_v<UserData, FileData*>) {
return; if (!Settings::getInstance()->getBool("ListScrollOverlay"))
return;
if (size() == 0 || mTitleOverlayOpacity == 0.0f) { if (size() == 0 || mTitleOverlayOpacity == 0.0f) {
mWindow->renderListScrollOverlay(0.0f, ""); mWindow->renderListScrollOverlay(0.0f, "");
return; return;
} }
std::string titleIndex; std::string titleIndex;
bool favoritesSorting; bool favoritesSorting;
if (getSelected()->getSystem()->isCustomCollection()) if (getSelected()->getSystem()->isCustomCollection())
favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom"); favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom");
else else
favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst");
if (favoritesSorting && getSelected()->getFavorite()) { if (favoritesSorting && getSelected()->getFavorite()) {
#if defined(_MSC_VER) // MSVC compiler. #if defined(_MSC_VER) // MSVC compiler.
titleIndex = Utils::String::wideStringToString(L"\uF005"); titleIndex = Utils::String::wideStringToString(L"\uF005");
#else #else
titleIndex = "\uF005"; titleIndex = "\uF005";
#endif #endif
} }
else { else {
titleIndex = getSelected()->getName(); titleIndex = getSelected()->getName();
if (titleIndex.size()) { if (titleIndex.size()) {
titleIndex[0] = toupper(titleIndex[0]); titleIndex[0] = toupper(titleIndex[0]);
if (titleIndex.size() > 1) { if (titleIndex.size() > 1) {
titleIndex = titleIndex.substr(0, 2); titleIndex = titleIndex.substr(0, 2);
titleIndex[1] = tolower(titleIndex[1]); titleIndex[1] = tolower(titleIndex[1]);
}
} }
} }
}
// The actual rendering takes place in Window to make sure that the overlay is placed on // 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. // top of all GUI elements but below the info popups and GPU statistics overlay.
mWindow->renderListScrollOverlay(mTitleOverlayOpacity, titleIndex); mWindow->renderListScrollOverlay(mTitleOverlayOpacity, titleIndex);
}
} }
void scroll(int amt) void scroll(int amt)
@ -321,14 +320,23 @@ protected:
if (mScrollVelocity == 0 || size() < 2) if (mScrollVelocity == 0 || size() < 2)
return; return;
int cursor = mCursor + amt; int cursor {mCursor + amt};
int absAmt = amt < 0 ? -amt : amt; int absAmt {amt < 0 ? -amt : amt};
// Stop at the end if we've been holding down the button for a long time or bool stopScroll {false};
// we're scrolling faster than one item at a time (e.g. page up/down).
// Otherwise, loop around. // Depending on the loop type we'll either pause at the ends if holding a navigation
if ((mLoopType == LIST_PAUSE_AT_END && (mScrollTier > 0 || absAmt > 1)) || // button, or we'll only stop if it's a quick jump key (should or trigger button) that
mLoopType == LIST_NEVER_LOOP) { // 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) { if (cursor < 0) {
cursor = 0; cursor = 0;
mScrollVelocity = 0; mScrollVelocity = 0;
@ -351,10 +359,11 @@ protected:
onScroll(); onScroll();
mCursor = cursor; 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() {} virtual void onScroll() {}
}; };

View file

@ -1,22 +1,147 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// EmulationStation Desktop Edition // EmulationStation Desktop Edition
// CarouselComponent.cpp // CarouselComponent.h
// //
// Carousel. // Carousel.
// //
#include "components/CarouselComponent.h" #ifndef ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
#define ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
#include "Log.h" #include "Log.h"
#include "Sound.h"
#include "animations/LambdaAnimation.h" #include "animations/LambdaAnimation.h"
#include "components/IList.h"
#include "components/primary/PrimaryComponent.h"
#include "resources/Font.h"
CarouselComponent::CarouselComponent() namespace
: IList<CarouselElement, SystemData*> {LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP} {
struct CarouselElement {
std::shared_ptr<GuiComponent> logo;
std::string logoPath;
std::string defaultLogoPath;
};
}; // namespace
template <typename T>
class CarouselComponent : public PrimaryComponent<T>, protected IList<CarouselElement, T>
{
using List = IList<CarouselElement, T>;
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<CarouselElement, T>::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<ThemeData>& theme = nullptr);
Entry& getEntry(int index) { return mEntries.at(index); }
const CarouselType getType() { return mType; }
void setCursorChangedCallback(const std::function<void(CursorState state)>& func) override
{
mCursorChangedCallback = func;
}
void setCancelTransitionsCallback(const std::function<void()>& 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<ThemeData>& 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<T, FileData*>)
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<void(CursorState state)> mCursorChangedCallback;
std::function<void()> mCancelTransitionsCallback;
float mEntryCamOffset;
int mPreviousScrollVelocity;
bool mTriggerJump;
CarouselType mType;
std::shared_ptr<Font> 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 <typename T>
CarouselComponent<T>::CarouselComponent()
: IList<CarouselElement, T> {LIST_SCROLL_STYLE_SLOW,
(std::is_same_v<T, SystemData*> ?
ListLoopType::LIST_ALWAYS_LOOP :
ListLoopType::LIST_PAUSE_AT_END_ON_JUMP)}
, mRenderer {Renderer::getInstance()} , mRenderer {Renderer::getInstance()}
, mCamOffset {0.0f} , mEntryCamOffset {0.0f}
, mPreviousScrollVelocity {0} , mPreviousScrollVelocity {0}
, mType {HORIZONTAL} , mTriggerJump {false}
, mType {CarouselType::HORIZONTAL}
, mFont {Font::get(FONT_SIZE_LARGE)} , mFont {Font::get(FONT_SIZE_LARGE)}
, mTextColor {0x000000FF} , mTextColor {0x000000FF}
, mTextBackgroundColor {0xFFFFFF00} , mTextBackgroundColor {0xFFFFFF00}
@ -34,10 +159,11 @@ CarouselComponent::CarouselComponent()
{ {
} }
void CarouselComponent::addEntry(const std::shared_ptr<ThemeData>& theme, template <typename T>
Entry& entry, void CarouselComponent<T>::addEntry(Entry& entry, const std::shared_ptr<ThemeData>& theme)
bool legacyMode)
{ {
bool legacyMode {theme->isLegacyTheme()};
// Make logo. // Make logo.
if (legacyMode) { if (legacyMode) {
const ThemeData::ThemeElement* logoElem { const ThemeData::ThemeElement* logoElem {
@ -96,8 +222,10 @@ void CarouselComponent::addEntry(const std::shared_ptr<ThemeData>& theme,
} }
if (!legacyMode) { if (!legacyMode) {
text->setLineSpacing(mLineSpacing); text->setLineSpacing(mLineSpacing);
if (mText != "") if constexpr (std::is_same_v<T, SystemData*>) {
text->setValue(mText); if (mText != "")
text->setValue(mText);
}
text->setColor(mTextColor); text->setColor(mTextColor);
text->setBackgroundColor(mTextBackgroundColor); text->setBackgroundColor(mTextBackgroundColor);
text->setRenderBackground(true); text->setRenderBackground(true);
@ -126,64 +254,116 @@ void CarouselComponent::addEntry(const std::shared_ptr<ThemeData>& theme,
glm::vec2 denormalized {mLogoSize * entry.data.logo->getOrigin()}; glm::vec2 denormalized {mLogoSize * entry.data.logo->getOrigin()};
entry.data.logo->setPosition(glm::vec3 {denormalized.x, denormalized.y, 0.0f}); 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 <typename T> bool CarouselComponent<T>::input(InputConfig* config, Input input)
{ {
if (input.value != 0) { if (input.value != 0) {
switch (mType) { switch (mType) {
case VERTICAL: case CarouselType::VERTICAL:
case VERTICAL_WHEEL: case CarouselType::VERTICAL_WHEEL:
if (config->isMappedLike("up", input)) { if (config->isMappedLike("up", input)) {
if (mCancelTransitionsCallback) if (mCancelTransitionsCallback)
mCancelTransitionsCallback(); mCancelTransitionsCallback();
listInput(-1); List::listInput(-1);
return true; return true;
} }
if (config->isMappedLike("down", input)) { if (config->isMappedLike("down", input)) {
if (mCancelTransitionsCallback) if (mCancelTransitionsCallback)
mCancelTransitionsCallback(); mCancelTransitionsCallback();
listInput(1); List::listInput(1);
return true; return true;
} }
break; break;
case HORIZONTAL: case CarouselType::HORIZONTAL:
case HORIZONTAL_WHEEL: case CarouselType::HORIZONTAL_WHEEL:
default: default:
if (config->isMappedLike("left", input)) { if (config->isMappedLike("left", input)) {
if (mCancelTransitionsCallback) if (mCancelTransitionsCallback)
mCancelTransitionsCallback(); mCancelTransitionsCallback();
listInput(-1); List::listInput(-1);
return true; return true;
} }
if (config->isMappedLike("right", input)) { if (config->isMappedLike("right", input)) {
if (mCancelTransitionsCallback) if (mCancelTransitionsCallback)
mCancelTransitionsCallback(); mCancelTransitionsCallback();
listInput(1); List::listInput(1);
return true; return true;
} }
break; break;
} }
if constexpr (std::is_same_v<T, FileData*>) {
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<int>(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 { else {
if (config->isMappedLike("left", input) || config->isMappedLike("right", input) || if constexpr (std::is_same_v<T, FileData*>) {
config->isMappedLike("up", input) || config->isMappedLike("down", input)) { if (config->isMappedLike("up", input) || config->isMappedLike("down", input) ||
listInput(0); 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<T, SystemData*>) {
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); return GuiComponent::input(config, input);
} }
void CarouselComponent::update(int deltaTime) template <typename T> void CarouselComponent<T>::update(int deltaTime)
{ {
listUpdate(deltaTime); List::listUpdate(deltaTime);
GuiComponent::update(deltaTime); GuiComponent::update(deltaTime);
} }
void CarouselComponent::render(const glm::mat4& parentTrans) template <typename T> void CarouselComponent<T>::render(const glm::mat4& parentTrans)
{ {
if (mEntries.size() == 0)
return;
glm::mat4 carouselTrans {parentTrans}; glm::mat4 carouselTrans {parentTrans};
carouselTrans = glm::translate(carouselTrans, glm::vec3 {mPosition.x, mPosition.y, 0.0f}); carouselTrans = glm::translate(carouselTrans, glm::vec3 {mPosition.x, mPosition.y, 0.0f});
carouselTrans = glm::translate( carouselTrans = glm::translate(
@ -210,15 +390,15 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
float yOff {0.0f}; float yOff {0.0f};
switch (mType) { switch (mType) {
case HORIZONTAL_WHEEL: case CarouselType::HORIZONTAL_WHEEL:
case VERTICAL_WHEEL: case CarouselType::VERTICAL_WHEEL:
xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mCamOffset * logoSpacing.y)); xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mEntryCamOffset * logoSpacing.y));
yOff = (mSize.y - mLogoSize.y) / 2.0f; yOff = (mSize.y - mLogoSize.y) / 2.0f;
break; break;
case VERTICAL: case CarouselType::VERTICAL:
logoSpacing.y = logoSpacing.y =
((mSize.y - (mLogoSize.y * mMaxLogoCount)) / (mMaxLogoCount)) + mLogoSize.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) if (mLogoHorizontalAlignment == ALIGN_LEFT)
xOff = mLogoSize.x / 10.0f; xOff = mLogoSize.x / 10.0f;
else if (mLogoHorizontalAlignment == ALIGN_RIGHT) else if (mLogoHorizontalAlignment == ALIGN_RIGHT)
@ -226,11 +406,11 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
else else
xOff = (mSize.x - mLogoSize.x) / 2.0f; xOff = (mSize.x - mLogoSize.x) / 2.0f;
break; break;
case HORIZONTAL: case CarouselType::HORIZONTAL:
default: default:
logoSpacing.x = logoSpacing.x =
((mSize.x - (mLogoSize.x * mMaxLogoCount)) / (mMaxLogoCount)) + mLogoSize.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) if (mLogoVerticalAlignment == ALIGN_TOP)
yOff = mLogoSize.y / 10.0f; yOff = mLogoSize.y / 10.0f;
else if (mLogoVerticalAlignment == ALIGN_BOTTOM) else if (mLogoVerticalAlignment == ALIGN_BOTTOM)
@ -240,7 +420,7 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
break; break;
} }
int center {static_cast<int>(mCamOffset)}; int center {static_cast<int>(mEntryCamOffset)};
int logoInclusion {static_cast<int>(std::ceil(mMaxLogoCount / 2.0f))}; int logoInclusion {static_cast<int>(std::ceil(mMaxLogoCount / 2.0f))};
bool singleEntry {mEntries.size() == 1}; bool singleEntry {mEntries.size() == 1};
@ -263,7 +443,7 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
logoTrans = glm::translate( logoTrans = glm::translate(
logoTrans, glm::vec3 {i * logoSpacing.x + xOff, i * logoSpacing.y + yOff, 0.0f}); 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)))}; float scale {1.0f + ((mLogoScale - 1.0f) * (1.0f - fabsf(distance)))};
scale = std::min(mLogoScale, std::max(1.0f, scale)); scale = std::min(mLogoScale, std::max(1.0f, scale));
@ -278,16 +458,16 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
if (comp == nullptr) if (comp == nullptr)
continue; continue;
if (mType == VERTICAL_WHEEL || mType == HORIZONTAL_WHEEL) { if (mType == CarouselType::VERTICAL_WHEEL || mType == CarouselType::HORIZONTAL_WHEEL) {
comp->setRotationDegrees(mLogoRotation * distance); comp->setRotationDegrees(mLogoRotation * distance);
comp->setRotationOrigin(mLogoRotationOrigin); comp->setRotationOrigin(mLogoRotationOrigin);
} }
// When running at lower resolutions, prevent the scale-down to go all the way to the // When running at lower resolutions, prevent the scale-down to go all the way to
// minimum value. This avoids potential single-pixel alignment issues when the logo // the minimum value. This avoids potential single-pixel alignment issues when the
// can't be vertically placed exactly in the middle of the carousel. Although the // logo can't be vertically placed exactly in the middle of the carousel. Although
// problem theoretically exists at all resolutions, it's not visble at around 1080p // the problem theoretically exists at all resolutions, it's not visble at around
// and above. // 1080p and above.
if (std::min(Renderer::getScreenWidth(), Renderer::getScreenHeight()) < 1080.0f) if (std::min(Renderer::getScreenWidth(), Renderer::getScreenHeight()) < 1080.0f)
scale = glm::clamp(scale, 1.0f / mLogoScale + 0.01f, 1.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(); mRenderer->popClipRect();
} }
void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme, template <typename T>
const std::string& view, void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
const std::string& element, const std::string& view,
unsigned int properties) const std::string& element,
unsigned int properties)
{ {
using namespace ThemeFlags; using namespace ThemeFlags;
const ThemeData::ThemeElement* elem {theme->getElement(view, element, "carousel")}; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "carousel")};
@ -312,7 +493,7 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
mPosition.y = floorf(0.5f * (Renderer::getScreenHeight() - mSize.y)); mPosition.y = floorf(0.5f * (Renderer::getScreenHeight() - mSize.y));
mCarouselColor = 0xFFFFFFD8; mCarouselColor = 0xFFFFFFD8;
mCarouselColorEnd = 0xFFFFFFD8; mCarouselColorEnd = 0xFFFFFFD8;
mDefaultZIndex = 50.0f; mZIndex = mDefaultZIndex;
mText = ""; mText = "";
if (!elem) if (!elem)
@ -321,22 +502,22 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (elem->has("type")) { if (elem->has("type")) {
const std::string type {elem->get<std::string>("type")}; const std::string type {elem->get<std::string>("type")};
if (type == "horizontal") { if (type == "horizontal") {
mType = HORIZONTAL; mType = CarouselType::HORIZONTAL;
} }
else if (type == "horizontal_wheel") { else if (type == "horizontal_wheel") {
mType = HORIZONTAL_WHEEL; mType = CarouselType::HORIZONTAL_WHEEL;
} }
else if (type == "vertical") { else if (type == "vertical") {
mType = VERTICAL; mType = CarouselType::VERTICAL;
} }
else if (type == "vertical_wheel") { else if (type == "vertical_wheel") {
mType = VERTICAL_WHEEL; mType = CarouselType::VERTICAL_WHEEL;
} }
else { else {
LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property "
"<type> defined as \"" "<type> defined as \""
<< type << "\""; << type << "\"";
mType = HORIZONTAL; mType = CarouselType::HORIZONTAL;
} }
} }
@ -395,10 +576,10 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (elem->has("logoHorizontalAlignment")) { if (elem->has("logoHorizontalAlignment")) {
const std::string alignment {elem->get<std::string>("logoHorizontalAlignment")}; const std::string alignment {elem->get<std::string>("logoHorizontalAlignment")};
if (alignment == "left" && mType != HORIZONTAL) { if (alignment == "left" && mType != CarouselType::HORIZONTAL) {
mLogoHorizontalAlignment = ALIGN_LEFT; mLogoHorizontalAlignment = ALIGN_LEFT;
} }
else if (alignment == "right" && mType != HORIZONTAL) { else if (alignment == "right" && mType != CarouselType::HORIZONTAL) {
mLogoHorizontalAlignment = ALIGN_RIGHT; mLogoHorizontalAlignment = ALIGN_RIGHT;
} }
else if (alignment == "center") { else if (alignment == "center") {
@ -414,10 +595,10 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (elem->has("logoVerticalAlignment")) { if (elem->has("logoVerticalAlignment")) {
const std::string alignment {elem->get<std::string>("logoVerticalAlignment")}; const std::string alignment {elem->get<std::string>("logoVerticalAlignment")};
if (alignment == "top" && mType != VERTICAL) { if (alignment == "top" && mType != CarouselType::VERTICAL) {
mLogoVerticalAlignment = ALIGN_TOP; mLogoVerticalAlignment = ALIGN_TOP;
} }
else if (alignment == "bottom" && mType != VERTICAL) { else if (alignment == "bottom" && mType != CarouselType::VERTICAL) {
mLogoVerticalAlignment = ALIGN_BOTTOM; mLogoVerticalAlignment = ALIGN_BOTTOM;
} }
else if (alignment == "center") { else if (alignment == "center") {
@ -434,19 +615,19 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
// Legacy themes only. // Legacy themes only.
if (elem->has("logoAlignment")) { if (elem->has("logoAlignment")) {
const std::string alignment {elem->get<std::string>("logoAlignment")}; const std::string alignment {elem->get<std::string>("logoAlignment")};
if (alignment == "left" && mType != HORIZONTAL) { if (alignment == "left" && mType != CarouselType::HORIZONTAL) {
mLogoHorizontalAlignment = ALIGN_LEFT; mLogoHorizontalAlignment = ALIGN_LEFT;
mLogoVerticalAlignment = ALIGN_CENTER; mLogoVerticalAlignment = ALIGN_CENTER;
} }
else if (alignment == "right" && mType != HORIZONTAL) { else if (alignment == "right" && mType != CarouselType::HORIZONTAL) {
mLogoHorizontalAlignment = ALIGN_RIGHT; mLogoHorizontalAlignment = ALIGN_RIGHT;
mLogoVerticalAlignment = ALIGN_CENTER; mLogoVerticalAlignment = ALIGN_CENTER;
} }
else if (alignment == "top" && mType != VERTICAL) { else if (alignment == "top" && mType != CarouselType::VERTICAL) {
mLogoVerticalAlignment = ALIGN_TOP; mLogoVerticalAlignment = ALIGN_TOP;
mLogoHorizontalAlignment = ALIGN_CENTER; mLogoHorizontalAlignment = ALIGN_CENTER;
} }
else if (alignment == "bottom" && mType != VERTICAL) { else if (alignment == "bottom" && mType != CarouselType::VERTICAL) {
mLogoVerticalAlignment = ALIGN_BOTTOM; mLogoVerticalAlignment = ALIGN_BOTTOM;
mLogoHorizontalAlignment = ALIGN_CENTER; mLogoHorizontalAlignment = ALIGN_CENTER;
} }
@ -507,20 +688,28 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
mRenderer->getScreenHeight() * 1.5f); mRenderer->getScreenHeight() * 1.5f);
} }
void CarouselComponent::onCursorChanged(const CursorState& state) template <typename T> void CarouselComponent<T>::onCursorChanged(const CursorState& state)
{ {
float startPos {mCamOffset}; float startPos {mEntryCamOffset};
float posMax {static_cast<float>(mEntries.size())}; float posMax {static_cast<float>(mEntries.size())};
float target {static_cast<float>(mCursor)}; float target {static_cast<float>(mCursor)};
// Find the shortest path to the target. // Find the shortest path to the target.
float endPos {target}; // Directly. float endPos {target}; // Directly.
float dist {fabsf(endPos - startPos)};
if (fabsf(target + posMax - startPos - mScrollVelocity) < dist) if (mPreviousScrollVelocity > 0 && mScrollVelocity == 0 && mEntryCamOffset > posMax - 1.0f)
endPos = target + posMax; // Loop around the end (0 -> max). startPos = 0.0f;
if (fabsf(target - posMax - startPos - mScrollVelocity) < dist)
endPos = target - posMax; // Loop around the start (max - 1 -> -1). // 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. // Make sure there are no reverse jumps between logos.
bool changedDirection {false}; bool changedDirection {false};
@ -536,11 +725,11 @@ void CarouselComponent::onCursorChanged(const CursorState& state)
if (mScrollVelocity != 0) if (mScrollVelocity != 0)
mPreviousScrollVelocity = mScrollVelocity; mPreviousScrollVelocity = mScrollVelocity;
// No need to animate transition, we're not going anywhere (probably mEntries.size() == 1). // No need to animate transition, we're not going anywhere.
if (endPos == mCamOffset) if (endPos == mEntryCamOffset)
return; return;
Animation* anim = new LambdaAnimation( Animation* anim {new LambdaAnimation(
[this, startPos, endPos, posMax](float t) { [this, startPos, endPos, posMax](float t) {
t -= 1; t -= 1;
float f {glm::mix(startPos, endPos, t * 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) if (f >= posMax)
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) if (mCursorChangedCallback)
mCursorChangedCallback(state); mCursorChangedCallback(state);
} }
#endif // ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H

View file

@ -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 <typename T> 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<void()>& func) = 0;
virtual void setCursorChangedCallback(const std::function<void(CursorState state)>& 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

View file

@ -12,35 +12,39 @@
#include "Log.h" #include "Log.h"
#include "Sound.h" #include "Sound.h"
#include "components/IList.h" #include "components/IList.h"
#include "components/primary/PrimaryComponent.h"
#include "resources/Font.h" #include "resources/Font.h"
#include "utils/StringUtil.h"
#include <memory> namespace
{
struct TextListData {
unsigned int colorId;
std::shared_ptr<TextCache> textCache;
};
}; // namespace
class TextCache; template <typename T>
class TextListComponent : public PrimaryComponent<T>, private IList<TextListData, T>
struct TextListData {
unsigned int colorId;
std::shared_ptr<TextCache> textCache;
};
// A scrollable text list supporting multiple row colors.
template <typename T> class TextListComponent : public IList<TextListData, T>
{ {
using List = IList<TextListData, T>; using List = IList<TextListData, T>;
protected: protected:
using List::mCursor; using List::mCursor;
using List::mEntries; using List::mEntries;
using List::mScrollVelocity;
using List::mSize; using List::mSize;
using List::mWindow; using List::mWindow;
public: public:
using GuiComponent::setColor;
using List::size; using List::size;
using Entry = typename IList<TextListData, T>::Entry;
using PrimaryAlignment = typename PrimaryComponent<T>::PrimaryAlignment;
using GuiComponent::setColor;
TextListComponent(); TextListComponent();
void addEntry(Entry& entry, const std::shared_ptr<ThemeData>& theme = nullptr);
bool input(InputConfig* config, Input input) override; bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override; void update(int deltaTime) override;
void render(const glm::mat4& parentTrans) override; void render(const glm::mat4& parentTrans) override;
@ -49,24 +53,16 @@ public:
const std::string& element, const std::string& element,
unsigned int properties) override; 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 { void setCursorChangedCallback(const std::function<void(CursorState state)>& func) override
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<void(CursorState state)>& func)
{ {
mCursorChangedCallback = func; mCursorChangedCallback = func;
} }
void setCancelTransitionsCallback(const std::function<void()>& func) override
{
mCancelTransitionsCallback = func;
}
void setFont(const std::shared_ptr<Font>& font) void setFont(const std::shared_ptr<Font>& font)
{ {
@ -135,16 +131,37 @@ protected:
void onCursorChanged(const CursorState& state) override; void onCursorChanged(const CursorState& state) override;
private: 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; Renderer* mRenderer;
std::function<void()> mCancelTransitionsCallback;
float mCamOffset;
int mPreviousScrollVelocity;
int mLoopOffset; int mLoopOffset;
int mLoopOffset2; int mLoopOffset2;
int mLoopTime; int mLoopTime;
bool mLoopScroll; bool mLoopScroll;
Alignment mAlignment; PrimaryAlignment mAlignment;
float mHorizontalMargin; float mHorizontalMargin;
std::function<void(CursorState state)> mCursorChangedCallback; std::function<void(CursorState state)> mCursorChangedCallback;
ImageComponent mSelectorImage;
std::shared_ptr<Font> mFont; std::shared_ptr<Font> mFont;
bool mUppercase; bool mUppercase;
@ -159,34 +176,162 @@ private:
unsigned int mSelectedColor; unsigned int mSelectedColor;
static const unsigned int COLOR_ID_COUNT = 2; static const unsigned int COLOR_ID_COUNT = 2;
unsigned int mColors[COLOR_ID_COUNT]; unsigned int mColors[COLOR_ID_COUNT];
ImageComponent mSelectorImage;
}; };
template <typename T> TextListComponent<T>::TextListComponent() template <typename T>
TextListComponent<T>::TextListComponent()
: IList<TextListData, T> {(std::is_same_v<T, SystemData*> ? 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; template <typename T>
mAlignment = ALIGN_CENTER; void TextListComponent<T>::addEntry(Entry& entry, const std::shared_ptr<ThemeData>& theme)
{
List::add(entry);
}
mFont = Font::get(FONT_SIZE_MEDIUM); template <typename T> bool TextListComponent<T>::input(InputConfig* config, Input input)
mUppercase = false; {
mLowercase = false; if (size() > 0) {
mCapitalize = false; if (input.value != 0) {
mLineSpacing = 1.5f; if (config->isMappedLike("up", input)) {
mSelectorHeight = mFont->getSize() * 1.5f; if (mCancelTransitionsCallback)
mSelectorOffsetY = 0; mCancelTransitionsCallback();
mSelectorColor = 0x000000FF; List::listInput(-1);
mSelectorColorEnd = 0x000000FF; return true;
mSelectorColorGradientHorizontal = true; }
mSelectedColor = 0; if (config->isMappedLike("down", input)) {
mColors[0] = 0x0000FFFF; if (mCancelTransitionsCallback)
mColors[1] = 0x00FF00FF; 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<int>(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<int>(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<T, SystemData*>)
List::listInput(0);
else
List::stopScrolling();
}
}
}
return GuiComponent::input(config, input);
}
template <typename T> void TextListComponent<T>::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<unsigned int>(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<int>(delay + scrollTime + returnTime)};
mLoopTime += deltaTime;
while (mLoopTime > maxTime)
mLoopTime -= maxTime;
mLoopOffset = static_cast<int>(Utils::Math::loop(delay, scrollTime + returnTime,
static_cast<float>(mLoopTime),
scrollLength + returnLength));
if (mLoopOffset > (scrollLength - (limit - returnLength)))
mLoopOffset2 = static_cast<int>(mLoopOffset - (scrollLength + returnLength));
}
}
GuiComponent::update(deltaTime);
} }
template <typename T> void TextListComponent<T>::render(const glm::mat4& parentTrans) template <typename T> void TextListComponent<T>::render(const glm::mat4& parentTrans)
@ -259,7 +404,7 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
static_cast<int>(std::round(dim.y))}); static_cast<int>(std::round(dim.y))});
for (int i = startEntry; i < listCutoff; ++i) { for (int i = startEntry; i < listCutoff; ++i) {
typename IList<TextListData, T>::Entry& entry {mEntries.at(static_cast<unsigned int>(i))}; Entry& entry {mEntries.at(i)};
unsigned int color; unsigned int color;
if (mCursor == i && mSelectedColor) if (mCursor == i && mSelectedColor)
@ -282,28 +427,33 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
std::unique_ptr<TextCache>(font->buildTextCache(entry.name, 0, 0, 0x000000FF)); std::unique_ptr<TextCache>(font->buildTextCache(entry.name, 0, 0, 0x000000FF));
} }
// If a game is marked as hidden, lower the text opacity a lot. if constexpr (std::is_same_v<T, FileData*>) {
// If a game is marked to not be counted, lower the opacity a moderate amount. // If a game is marked as hidden, lower the text opacity a lot.
if (entry.object->getHidden()) // If a game is marked to not be counted, lower the opacity a moderate amount.
entry.data.textCache->setColor(color & 0xFFFFFF44); if (entry.object->getHidden())
else if (!entry.object->getCountAsGame()) entry.data.textCache->setColor(color & 0xFFFFFF44);
entry.data.textCache->setColor(color & 0xFFFFFF77); else if (!entry.object->getCountAsGame())
else entry.data.textCache->setColor(color & 0xFFFFFF77);
else
entry.data.textCache->setColor(color);
}
else {
entry.data.textCache->setColor(color); entry.data.textCache->setColor(color);
}
glm::vec3 offset {0.0f, y, 0.0f}; glm::vec3 offset {0.0f, y, 0.0f};
switch (mAlignment) { switch (mAlignment) {
case ALIGN_LEFT: case PrimaryAlignment::ALIGN_LEFT:
offset.x = mHorizontalMargin; offset.x = mHorizontalMargin;
break; break;
case ALIGN_CENTER: case PrimaryAlignment::ALIGN_CENTER:
offset.x = offset.x =
static_cast<float>((mSize.x - entry.data.textCache->metrics.size.x) / 2.0f); static_cast<float>((mSize.x - entry.data.textCache->metrics.size.x) / 2.0f);
if (offset.x < mHorizontalMargin) if (offset.x < mHorizontalMargin)
offset.x = mHorizontalMargin; offset.x = mHorizontalMargin;
break; break;
case ALIGN_RIGHT: case PrimaryAlignment::ALIGN_RIGHT:
offset.x = (mSize.x - entry.data.textCache->metrics.size.x); offset.x = (mSize.x - entry.data.textCache->metrics.size.x);
offset.x -= mHorizontalMargin; offset.x -= mHorizontalMargin;
if (offset.x < mHorizontalMargin) if (offset.x < mHorizontalMargin)
@ -340,122 +490,11 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
y += entrySize; y += entrySize;
} }
mRenderer->popClipRect(); mRenderer->popClipRect();
List::listRenderTitleOverlay(trans); if constexpr (std::is_same_v<T, FileData*>)
List::listRenderTitleOverlay(trans);
GuiComponent::renderChildren(trans); GuiComponent::renderChildren(trans);
} }
template <typename T> bool TextListComponent<T>::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 <typename T> void TextListComponent<T>::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<unsigned int>(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<int>(delay + scrollTime + returnTime)};
mLoopTime += deltaTime;
while (mLoopTime > maxTime)
mLoopTime -= maxTime;
mLoopOffset = static_cast<int>(Utils::Math::loop(delay, scrollTime + returnTime,
static_cast<float>(mLoopTime),
scrollLength + returnLength));
if (mLoopOffset > (scrollLength - (limit - returnLength)))
mLoopOffset2 = static_cast<int>(mLoopOffset - (scrollLength + returnLength));
}
}
GuiComponent::update(deltaTime);
}
// List management stuff.
template <typename T>
void TextListComponent<T>::add(const std::string& name, const T& obj, unsigned int color)
{
assert(color < COLOR_ID_COUNT);
typename IList<TextListData, T>::Entry entry;
entry.name = name;
entry.object = obj;
entry.data.colorId = color;
static_cast<IList<TextListData, T>*>(this)->add(entry);
}
template <typename T> void TextListComponent<T>::onCursorChanged(const CursorState& state)
{
mLoopOffset = 0;
mLoopOffset2 = 0;
mLoopTime = 0;
if (mCursorChangedCallback)
mCursorChangedCallback(state);
}
template <typename T> template <typename T>
void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme, void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
const std::string& view, const std::string& view,
@ -508,11 +547,11 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (elem->has("horizontalAlignment")) { if (elem->has("horizontalAlignment")) {
const std::string& str {elem->get<std::string>("horizontalAlignment")}; const std::string& str {elem->get<std::string>("horizontalAlignment")};
if (str == "left") if (str == "left")
setAlignment(ALIGN_LEFT); setAlignment(PrimaryAlignment::ALIGN_LEFT);
else if (str == "center") else if (str == "center")
setAlignment(ALIGN_CENTER); setAlignment(PrimaryAlignment::ALIGN_CENTER);
else if (str == "right") else if (str == "right")
setAlignment(ALIGN_RIGHT); setAlignment(PrimaryAlignment::ALIGN_RIGHT);
else else
LOG(LogWarning) << "TextListComponent: Invalid theme configuration, property " LOG(LogWarning) << "TextListComponent: Invalid theme configuration, property "
"<horizontalAlignment> defined as \"" "<horizontalAlignment> defined as \""
@ -522,11 +561,11 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
else if (elem->has("alignment")) { else if (elem->has("alignment")) {
const std::string& str {elem->get<std::string>("alignment")}; const std::string& str {elem->get<std::string>("alignment")};
if (str == "left") if (str == "left")
setAlignment(ALIGN_LEFT); setAlignment(PrimaryAlignment::ALIGN_LEFT);
else if (str == "center") else if (str == "center")
setAlignment(ALIGN_CENTER); setAlignment(PrimaryAlignment::ALIGN_CENTER);
else if (str == "right") else if (str == "right")
setAlignment(ALIGN_RIGHT); setAlignment(PrimaryAlignment::ALIGN_RIGHT);
else else
LOG(LogWarning) << "TextListComponent: Invalid theme configuration, property " LOG(LogWarning) << "TextListComponent: Invalid theme configuration, property "
"<alignment> defined as \"" "<alignment> defined as \""
@ -590,4 +629,35 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
} }
} }
template <typename T> void TextListComponent<T>::onCursorChanged(const CursorState& state)
{
mLoopOffset = 0;
mLoopOffset2 = 0;
mLoopTime = 0;
if constexpr (std::is_same_v<T, SystemData*>) {
float startPos {mCamOffset};
float posMax {static_cast<float>(mEntries.size())};
float endPos {static_cast<float>(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 #endif // ES_CORE_COMPONENTS_TEXT_LIST_COMPONENT_H