diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index a97df45b9..74f707d68 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -509,6 +509,18 @@ void GuiMenu::openUIOptions() } }); + // Enable menu scroll indicators. + auto scroll_indicators = std::make_shared(mWindow); + scroll_indicators->setState(Settings::getInstance()->getBool("ScrollIndicators")); + s->addWithLabel("ENABLE MENU SCROLL INDICATORS", scroll_indicators); + s->addSaveFunc([scroll_indicators, s] { + if (scroll_indicators->getState() != Settings::getInstance()->getBool("ScrollIndicators")) { + Settings::getInstance()->setBool("ScrollIndicators", scroll_indicators->getState()); + s->setNeedsSaving(); + s->setInvalidateCachedBackground(); + } + }); + // Enable the 'Y' button for tagging games as favorites. auto favorites_add_button = std::make_shared(mWindow); favorites_add_button->setState(Settings::getInstance()->getBool("FavoritesAddButton")); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index fde6e762d..13ccf7ac8 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -43,7 +43,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, std::function deleteGameFunc) : GuiComponent{window} , mBackground{window, ":/graphics/frame.svg"} - , mGrid{window, glm::ivec2{1, 5}} + , mGrid{window, glm::ivec2{3, 6}} , mScraperParams{scraperParams} , mMetaDataDecl{mdd} , mMetaData{md} @@ -58,6 +58,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, mTitle = std::make_shared(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mGrid.setEntry(mTitle, glm::ivec2{0, 0}, false, true, glm::ivec2{3, 2}); // Extract possible subfolders from the path. std::string folderPath = @@ -82,11 +83,24 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER, glm::vec3{}, glm::vec2{}, 0x00000000, 0.05f); - mGrid.setEntry(mTitle, glm::ivec2{0, 0}, false, true); - mGrid.setEntry(mSubtitle, glm::ivec2{0, 1}, false, true); + mGrid.setEntry(mSubtitle, glm::ivec2{0, 2}, false, true, glm::ivec2{3, 1}); mList = std::make_shared(mWindow); - mGrid.setEntry(mList, glm::ivec2{0, 3}, true, true); + mGrid.setEntry(mList, glm::ivec2{0, 4}, true, true, glm::ivec2{3, 1}); + + // Set up scroll indicators. + mScrollUp = std::make_shared(mWindow); + mScrollDown = std::make_shared(mWindow); + mScrollIndicator = std::make_shared(mList, mScrollUp, mScrollDown); + + mScrollUp->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f); + mScrollUp->setOrigin(0.0f, -0.35f); + + mScrollDown->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f); + mScrollDown->setOrigin(0.0f, 0.35f); + + mGrid.setEntry(mScrollUp, glm::ivec2{2, 0}, false, false, glm::ivec2{1, 1}); + mGrid.setEntry(mScrollDown, glm::ivec2{2, 1}, false, false, glm::ivec2{1, 1}); // Populate list. for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { @@ -463,7 +477,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, } mButtons = makeButtonGrid(mWindow, buttons); - mGrid.setEntry(mButtons, glm::ivec2{0, 4}, true, false); + mGrid.setEntry(mButtons, glm::ivec2{0, 5}, true, false, glm::ivec2{3, 1}); // Resize + center. float width = @@ -481,10 +495,14 @@ void GuiMetaDataEd::onSizeChanged() { const float titleSubtitleSpacing = mSize.y * 0.03f; - mGrid.setRowHeightPerc(0, TITLE_HEIGHT / mSize.y); - mGrid.setRowHeightPerc(1, titleSubtitleSpacing / mSize.y); - mGrid.setRowHeightPerc(2, (titleSubtitleSpacing * 1.2f) / mSize.y); - mGrid.setRowHeightPerc(3, ((mList->getRowHeight(0) * 10.0f) + 2.0f) / mSize.y); + mGrid.setRowHeightPerc(0, TITLE_HEIGHT / mSize.y / 2.0f); + mGrid.setRowHeightPerc(1, TITLE_HEIGHT / mSize.y / 2.0f); + mGrid.setRowHeightPerc(2, titleSubtitleSpacing / mSize.y); + mGrid.setRowHeightPerc(3, (titleSubtitleSpacing * 1.2f) / mSize.y); + mGrid.setRowHeightPerc(4, ((mList->getRowHeight(0) * 10.0f) + 2.0f) / mSize.y); + + mGrid.setColWidthPerc(0, 0.08f); + mGrid.setColWidthPerc(2, 0.08f); mGrid.setSize(mSize); mBackground.fitTo(mSize, glm::vec3{}, glm::vec2{-32.0f, -32.0f}); diff --git a/es-app/src/guis/GuiMetaDataEd.h b/es-app/src/guis/GuiMetaDataEd.h index 91d781996..ec1793145 100644 --- a/es-app/src/guis/GuiMetaDataEd.h +++ b/es-app/src/guis/GuiMetaDataEd.h @@ -15,6 +15,7 @@ #include "MetaData.h" #include "components/ComponentGrid.h" #include "components/NinePatchComponent.h" +#include "components/ScrollIndicatorComponent.h" #include "guis/GuiSettings.h" #include "scrapers/Scraper.h" @@ -49,6 +50,9 @@ private: ComponentGrid mGrid; std::shared_ptr mTitle; + std::shared_ptr mScrollUp; + std::shared_ptr mScrollDown; + std::shared_ptr mScrollIndicator; std::shared_ptr mSubtitle; std::shared_ptr mHeaderGrid; std::shared_ptr mList; diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index e038b1270..9cc65d92c 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -50,6 +50,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/OptionListComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScrollableContainer.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScrollIndicatorComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/SliderComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.h diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 49926b01b..e7035633c 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -181,6 +181,7 @@ void Settings::setDefaults() mBoolMap["SpecialCharsASCII"] = {false, false}; mBoolMap["ListScrollOverlay"] = {false, false}; mBoolMap["VirtualKeyboard"] = {true, true}; + mBoolMap["ScrollIndicators"] = {true, true}; mBoolMap["FavoritesAddButton"] = {true, true}; mBoolMap["RandomAddButton"] = {false, false}; mBoolMap["GamelistFilters"] = {true, true}; diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index db6e65fff..c829399ee 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -11,17 +11,19 @@ #define TOTAL_HORIZONTAL_PADDING_PX 20.0f ComponentList::ComponentList(Window* window) - : IList(window, LIST_SCROLL_STYLE_SLOW, LIST_NEVER_LOOP) + : IList{window, LIST_SCROLL_STYLE_SLOW, LIST_NEVER_LOOP} + , mFocused{false} + , mSetupCompleted{false} + , mBottomCameraOffset{false} + , mSelectorBarOffset{0.0f} + , mCameraOffset{0.0f} + , mScrollIndicatorStatus{SCROLL_NONE} { // Adjust the padding relative to the aspect ratio and screen resolution to make it look // coherent regardless of screen type. The 1.778 aspect ratio value is the 16:9 reference. - float aspectValue = 1.778f / Renderer::getScreenAspectRatio(); + float aspectValue{1.778f / Renderer::getScreenAspectRatio()}; mHorizontalPadding = TOTAL_HORIZONTAL_PADDING_PX * aspectValue * Renderer::getScreenWidthModifier(); - - mSelectorBarOffset = 0.0f; - mCameraOffset = 0.0f; - mFocused = false; } void ComponentList::addRow(const ComponentListRow& row, bool setCursorHere) @@ -113,6 +115,35 @@ bool ComponentList::input(InputConfig* config, Input input) void ComponentList::update(int deltaTime) { + const float totalHeight = getTotalRowHeight(); + + // Scroll indicator logic, used by ScrollIndicatorComponent. + bool scrollIndicatorChanged = false; + + if (totalHeight > mSize.y) { + if (mCameraOffset == 0) { + if (mScrollIndicatorStatus != SCROLL_DOWN) { + mScrollIndicatorStatus = SCROLL_DOWN; + scrollIndicatorChanged = true; + } + } + else if (mBottomCameraOffset) { + if (mScrollIndicatorStatus != SCROLL_UP) { + mScrollIndicatorStatus = SCROLL_UP; + scrollIndicatorChanged = true; + } + } + else if (mCameraOffset > 0) { + if (mScrollIndicatorStatus != SCROLL_UP_DOWN) { + mScrollIndicatorStatus = SCROLL_UP_DOWN; + scrollIndicatorChanged = true; + } + } + } + + if (scrollIndicatorChanged == true && mScrollIndicatorChangedCallback != nullptr) + mScrollIndicatorChangedCallback(mScrollIndicatorStatus); + listUpdate(deltaTime); if (size()) { @@ -125,6 +156,8 @@ void ComponentList::update(int deltaTime) void ComponentList::onCursorChanged(const CursorState& state) { + mSetupCompleted = true; + // Update the selector bar position. // In the future this might be animated. mSelectorBarOffset = 0; @@ -149,6 +182,8 @@ void ComponentList::onCursorChanged(const CursorState& state) void ComponentList::updateCameraOffset() { + float oldCameraOffset = mCameraOffset; + // Move the camera to scroll. const float totalHeight = getTotalRowHeight(); if (totalHeight > mSize.y) { @@ -160,11 +195,17 @@ void ComponentList::updateCameraOffset() unsigned int i = 0; while (mCameraOffset < target && i < mEntries.size()) { mCameraOffset += getRowHeight(mEntries.at(i).data); - if (mCameraOffset > totalHeight - mSize.y) + if (mCameraOffset > totalHeight - mSize.y) { + if (mSetupCompleted && mCameraOffset != oldCameraOffset) + mBottomCameraOffset = true; break; + } i++; } + if (mCameraOffset < oldCameraOffset) + mBottomCameraOffset = false; + if (mCameraOffset < 0.0f) mCameraOffset = 0.0f; } diff --git a/es-core/src/components/ComponentList.h b/es-core/src/components/ComponentList.h index c5dc392c1..9035cd881 100644 --- a/es-core/src/components/ComponentList.h +++ b/es-core/src/components/ComponentList.h @@ -61,6 +61,13 @@ class ComponentList : public IList public: ComponentList(Window* window); + enum ScrollIndicator { + SCROLL_NONE, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). + SCROLL_UP, + SCROLL_UP_DOWN, + SCROLL_DOWN + }; + void addRow(const ComponentListRow& row, bool setCursorHere = false); void textInput(const std::string& text) override; @@ -87,12 +94,19 @@ public: { return mCursorChangedCallback; } + void setScrollIndicatorChangedCallback( + const std::function& callback) + { + mScrollIndicatorChangedCallback = callback; + } protected: void onCursorChanged(const CursorState& state) override; private: bool mFocused; + bool mSetupCompleted; + bool mBottomCameraOffset; void updateCameraOffset(); void updateElementPosition(const ComponentListRow& row); @@ -105,6 +119,9 @@ private: float mCameraOffset; std::function mCursorChangedCallback; + std::function mScrollIndicatorChangedCallback; + + ScrollIndicator mScrollIndicatorStatus; }; #endif // ES_CORE_COMPONENTS_COMPONENT_LIST_H diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 3d5b49e9d..4e5a82184 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -14,14 +14,14 @@ #define BUTTON_GRID_VERT_PADDING 32.0f #define BUTTON_GRID_HORIZ_PADDING 10.0f -#define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + TITLE_VERT_PADDING) +#define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + Renderer::getScreenHeight() * 0.0637f) MenuComponent::MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont) : GuiComponent(window) , mBackground(window) - , mGrid(window, glm::ivec2{1, 3}) + , mGrid(window, glm::ivec2{3, 4}) , mNeedsSaving(false) { addChild(&mBackground); @@ -34,11 +34,25 @@ MenuComponent::MenuComponent(Window* window, mTitle->setHorizontalAlignment(ALIGN_CENTER); mTitle->setColor(0x555555FF); setTitle(title, titleFont); - mGrid.setEntry(mTitle, glm::ivec2{}, false); + mGrid.setEntry(mTitle, glm::ivec2{0, 0}, false, true, glm::ivec2{3, 2}); // Set up list which will never change (externally, anyway). mList = std::make_shared(mWindow); - mGrid.setEntry(mList, glm::ivec2{0, 1}, true); + mGrid.setEntry(mList, glm::ivec2{0, 2}, true, true, glm::ivec2{3, 1}); + + // Set up scroll indicators. + mScrollUp = std::make_shared(mWindow); + mScrollDown = std::make_shared(mWindow); + mScrollIndicator = std::make_shared(mList, mScrollUp, mScrollDown); + + mScrollUp->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f); + mScrollUp->setOrigin(0.0f, -0.35f); + + mScrollDown->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f); + mScrollDown->setOrigin(0.0f, 0.35f); + + mGrid.setEntry(mScrollUp, glm::ivec2{2, 0}, false, false, glm::ivec2{1, 1}); + mGrid.setEntry(mScrollDown, glm::ivec2{2, 1}, false, false, glm::ivec2{1, 1}); updateGrid(); updateSize(); @@ -109,8 +123,12 @@ void MenuComponent::onSizeChanged() mBackground.fitTo(mSize, glm::vec3{}, glm::vec2{-32.0f, -32.0f}); // Update grid row/column sizes. - mGrid.setRowHeightPerc(0, TITLE_HEIGHT / mSize.y); - mGrid.setRowHeightPerc(2, getButtonGridHeight() / mSize.y); + mGrid.setRowHeightPerc(0, TITLE_HEIGHT / mSize.y / 2.0f); + mGrid.setRowHeightPerc(1, TITLE_HEIGHT / mSize.y / 2.0f); + mGrid.setRowHeightPerc(3, getButtonGridHeight() / mSize.y); + + mGrid.setColWidthPerc(0, 0.08f); + mGrid.setColWidthPerc(2, 0.08f); mGrid.setSize(mSize); } @@ -134,7 +152,7 @@ void MenuComponent::updateGrid() if (mButtons.size()) { mButtonGrid = makeButtonGrid(mWindow, mButtons); - mGrid.setEntry(mButtonGrid, glm::ivec2{0, 2}, true, false); + mGrid.setEntry(mButtonGrid, glm::ivec2{0, 3}, true, false, glm::ivec2{3, 1}); } } diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index 2604ca837..f2c538c2a 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -12,13 +12,12 @@ #include "components/ComponentGrid.h" #include "components/ComponentList.h" #include "components/NinePatchComponent.h" +#include "components/ScrollIndicatorComponent.h" #include "components/TextComponent.h" #include "utils/StringUtil.h" #include -#define TITLE_VERT_PADDING (Renderer::getScreenHeight() * 0.0637f) - class ButtonComponent; class ImageComponent; @@ -87,6 +86,9 @@ private: ComponentGrid mGrid; std::shared_ptr mTitle; + std::shared_ptr mScrollUp; + std::shared_ptr mScrollDown; + std::shared_ptr mScrollIndicator; std::shared_ptr mList; std::shared_ptr mButtonGrid; std::vector> mButtons; diff --git a/es-core/src/components/ScrollIndicatorComponent.h b/es-core/src/components/ScrollIndicatorComponent.h new file mode 100644 index 000000000..2cee0783e --- /dev/null +++ b/es-core/src/components/ScrollIndicatorComponent.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// ScrollIndicatorComponent.h +// +// Visually indicates whether a menu can be scrolled (up, up/down or down). +// + +#ifndef ES_CORE_COMPONENTS_SCROLL_INDICATOR_COMPONENT_H +#define ES_CORE_COMPONENTS_SCROLL_INDICATOR_COMPONENT_H + +#define FADE_IN_TIME 90.0f + +#include "animations/LambdaAnimation.h" +#include "components/ComponentList.h" + +class ScrollIndicatorComponent +{ +public: + ScrollIndicatorComponent(std::shared_ptr componentList, + std::shared_ptr scrollUp, + std::shared_ptr scrollDown) + : mPreviousScrollState(ComponentList::SCROLL_NONE) + { + assert(componentList != nullptr && scrollUp != nullptr && scrollDown != nullptr); + + scrollUp->setImage(":/graphics/scroll_up.svg"); + scrollDown->setImage(":/graphics/scroll_down.svg"); + + scrollUp->setOpacity(0); + scrollDown->setOpacity(0); + + if (!Settings::getInstance()->getBool("ScrollIndicators")) + return; + + componentList.get()->setScrollIndicatorChangedCallback( + [this, scrollUp, scrollDown](ComponentList::ScrollIndicator state) { + float fadeInTime{FADE_IN_TIME}; + + bool upFadeIn = false; + bool upFadeOut = false; + bool downFadeIn = false; + bool downFadeOut = false; + + scrollUp->finishAnimation(0); + scrollDown->finishAnimation(0); + + if (state == ComponentList::SCROLL_UP && + mPreviousScrollState == ComponentList::SCROLL_NONE) { + scrollUp->setOpacity(255); + } + else if (state == ComponentList::SCROLL_UP && + mPreviousScrollState == ComponentList::SCROLL_UP_DOWN) { + downFadeOut = true; + } + else if (state == ComponentList::SCROLL_UP && + mPreviousScrollState == ComponentList::SCROLL_DOWN) { + upFadeIn = true; + fadeInTime *= 1.5f; + scrollDown->setOpacity(0); + } + else if (state == ComponentList::SCROLL_UP_DOWN && + mPreviousScrollState == ComponentList::SCROLL_NONE) { + scrollUp->setOpacity(255); + scrollDown->setOpacity(255); + } + else if (state == ComponentList::SCROLL_UP_DOWN && + mPreviousScrollState == ComponentList::SCROLL_DOWN) { + upFadeIn = true; + } + else if (state == ComponentList::SCROLL_UP_DOWN && + mPreviousScrollState == ComponentList::SCROLL_UP) { + downFadeIn = true; + } + else if (state == ComponentList::SCROLL_DOWN && + mPreviousScrollState == ComponentList::SCROLL_NONE) { + scrollDown->setOpacity(255); + } + else if (state == ComponentList::SCROLL_DOWN && + mPreviousScrollState == ComponentList::SCROLL_UP_DOWN) { + upFadeOut = true; + } + else if (state == ComponentList::SCROLL_DOWN && + mPreviousScrollState == ComponentList::SCROLL_UP) { + downFadeIn = true; + fadeInTime *= 1.5f; + scrollUp->setOpacity(0); + } + + if (downFadeIn) { + auto downFadeInFunc = [scrollDown](float t) { + scrollDown->setOpacity( + static_cast(glm::mix(0.0f, 1.0f, t) * 255)); + }; + scrollDown->setAnimation(new LambdaAnimation(downFadeInFunc, fadeInTime), 0, + nullptr, false); + } + + if (downFadeOut) { + auto downFadeOutFunc = [scrollDown](float t) { + scrollDown->setOpacity( + static_cast(glm::mix(0.0f, 1.0f, t) * 255)); + }; + scrollDown->setAnimation(new LambdaAnimation(downFadeOutFunc, fadeInTime), 0, + nullptr, true); + } + + if (upFadeIn) { + auto upFadeInFunc = [scrollUp](float t) { + scrollUp->setOpacity( + static_cast(glm::mix(0.0f, 1.0f, t) * 255)); + }; + scrollUp->setAnimation(new LambdaAnimation(upFadeInFunc, fadeInTime), 0, + nullptr, false); + } + + if (upFadeOut) { + auto upFadeOutFunc = [scrollUp](float t) { + scrollUp->setOpacity( + static_cast(glm::mix(0.0f, 1.0f, t) * 255)); + }; + scrollUp->setAnimation(new LambdaAnimation(upFadeOutFunc, fadeInTime), 0, + nullptr, true); + } + + mPreviousScrollState = state; + }); + } + +private: + ComponentList::ScrollIndicator mPreviousScrollState; +}; + +#endif // ES_CORE_COMPONENTS_SCROLL_INDICATOR_COMPONENT_H diff --git a/resources/graphics/scroll_down.svg b/resources/graphics/scroll_down.svg new file mode 100644 index 000000000..a1bb24837 --- /dev/null +++ b/resources/graphics/scroll_down.svg @@ -0,0 +1,74 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/resources/graphics/scroll_up.svg b/resources/graphics/scroll_up.svg new file mode 100644 index 000000000..595827b17 --- /dev/null +++ b/resources/graphics/scroll_up.svg @@ -0,0 +1,74 @@ + + + + + + image/svg+xml + + + + + + + + + + + +