diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index 94276f69e..4335f3074 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -574,22 +574,6 @@ void GamelistBase::populateList(const std::vector& files, FileData* f CarouselComponent::Entry carouselEntry; carouselEntry.name = (*it)->getName(); carouselEntry.object = *it; - if (carouselItemType == "" || carouselItemType == "marquee") - carouselEntry.data.itemPath = (*it)->getMarqueePath(); - else if (carouselItemType == "cover") - carouselEntry.data.itemPath = (*it)->getCoverPath(); - else if (carouselItemType == "3dbox") - carouselEntry.data.itemPath = (*it)->get3DBoxPath(); - else if (carouselItemType == "screenshot") - carouselEntry.data.itemPath = (*it)->getScreenshotPath(); - else if (carouselItemType == "titlescreen") - carouselEntry.data.itemPath = (*it)->getTitleScreenPath(); - else if (carouselItemType == "backcover") - carouselEntry.data.itemPath = (*it)->getBackCoverPath(); - else if (carouselItemType == "miximage") - carouselEntry.data.itemPath = (*it)->getMiximagePath(); - else if (carouselItemType == "fanart") - carouselEntry.data.itemPath = (*it)->getFanArtPath(); if (carouselDefaultItem != "") carouselEntry.data.defaultItemPath = carouselDefaultItem; diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index cd362d9f0..d3ea5bf3a 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -387,6 +387,9 @@ void GamelistView::updateInfoPanel(const CursorState& state) mPrimary->getSelected() : nullptr}; + if (mCarousel != nullptr) + mCarousel->onDemandTextureLoad(); + // If the game data has already been rendered to the info panel, then skip it this time. if (file == mLastUpdated) return; diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 1983ea384..c3f12b39a 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -16,16 +16,16 @@ #include "components/primary/PrimaryComponent.h" #include "resources/Font.h" -struct CarouselElement { +struct CarouselEntry { std::shared_ptr item; std::string itemPath; std::string defaultItemPath; }; template -class CarouselComponent : public PrimaryComponent, protected IList +class CarouselComponent : public PrimaryComponent, protected IList { - using List = IList; + using List = IList; protected: using List::mCursor; @@ -38,7 +38,7 @@ protected: using GuiComponent::mZIndex; public: - using Entry = typename IList::Entry; + using Entry = typename IList::Entry; enum class CarouselType { HORIZONTAL, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). @@ -50,8 +50,10 @@ public: CarouselComponent(); - void addEntry(Entry& entry, const std::shared_ptr& theme = nullptr); + void addEntry(Entry& entry, const std::shared_ptr& theme); + void updateEntry(Entry& entry, const std::shared_ptr& theme); Entry& getEntry(int index) { return mEntries.at(index); } + void onDemandTextureLoad(); const CarouselType getType() { return mType; } const std::string& getItemType() { return mItemType; } void setItemType(std::string itemType) { mItemType = itemType; } @@ -87,7 +89,7 @@ private: { List::stopScrolling(); // Only finish the animation if we're in the gamelist view. - if constexpr (std::is_same_v) + if (mGamelistView) GuiComponent::finishAnimation(0); } const int getScrollingVelocity() override { return List::getScrollingVelocity(); } @@ -111,6 +113,7 @@ private: float mEntryCamOffset; int mPreviousScrollVelocity; bool mTriggerJump; + bool mGamelistView; CarouselType mType; std::string mItemType; @@ -141,14 +144,15 @@ private: template CarouselComponent::CarouselComponent() - : IList {LIST_SCROLL_STYLE_SLOW, - (std::is_same_v ? - ListLoopType::LIST_ALWAYS_LOOP : - ListLoopType::LIST_PAUSE_AT_END_ON_JUMP)} + : IList {LIST_SCROLL_STYLE_SLOW, + (std::is_same_v ? + ListLoopType::LIST_ALWAYS_LOOP : + ListLoopType::LIST_PAUSE_AT_END_ON_JUMP)} , mRenderer {Renderer::getInstance()} , mEntryCamOffset {0.0f} , mPreviousScrollVelocity {0} , mTriggerJump {false} + , mGamelistView {std::is_same_v ? true : false} , mType {CarouselType::HORIZONTAL} , mLegacyMode {false} , mFont {Font::get(FONT_SIZE_LARGE)} @@ -180,7 +184,7 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptrisLegacyTheme()}; bool dynamic {true}; - if constexpr (std::is_same_v) + if (!mGamelistView) dynamic = false; if (legacyMode) { @@ -240,7 +244,7 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptrsetLineSpacing(mLineSpacing); - if constexpr (std::is_same_v) { + if (!mGamelistView) { if (mText != "") text->setValue(mText); } @@ -275,6 +279,85 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptr +void CarouselComponent::updateEntry(Entry& entry, const std::shared_ptr& theme) +{ + if (entry.data.itemPath != "") { + auto item = std::make_shared(false, true); + item->setLinearInterpolation(true); + item->setImage(entry.data.itemPath); + item->setMaxSize(mItemSize * mItemScale); + item->applyTheme(theme, "system", "", ThemeFlags::ALL); + item->setRotateByTargetSize(true); + entry.data.item = item; + } + else { + return; + } + + // Set origin for the items based on their alignment so they line up properly. + if (mItemHorizontalAlignment == ALIGN_LEFT) + entry.data.item->setOrigin(0.0f, 0.5f); + else if (mItemHorizontalAlignment == ALIGN_RIGHT) + entry.data.item->setOrigin(1.0f, 0.5f); + else + entry.data.item->setOrigin(0.5f, 0.5f); + + if (mItemVerticalAlignment == ALIGN_TOP) + entry.data.item->setOrigin(entry.data.item->getOrigin().x, 0.0f); + else if (mItemVerticalAlignment == ALIGN_BOTTOM) + entry.data.item->setOrigin(entry.data.item->getOrigin().x, 1.0f); + else + entry.data.item->setOrigin(entry.data.item->getOrigin().x, 0.5f); + + glm::vec2 denormalized {mItemSize * entry.data.item->getOrigin()}; + entry.data.item->setPosition(glm::vec3 {denormalized.x, denormalized.y, 0.0f}); +} + +template void CarouselComponent::onDemandTextureLoad() +{ + if constexpr (std::is_same_v) { + int numEntries {static_cast(mEntries.size())}; + int center {getCursor()}; + int itemInclusion {static_cast(std::ceil((mMaxItemCount + 1) / 2.0f))}; + + for (int i = center - itemInclusion + 1; i < center + itemInclusion; ++i) { + int cursor {i}; + + while (cursor < 0) + cursor += numEntries; + while (cursor >= numEntries) + cursor -= numEntries; + + auto& entry = mEntries.at(cursor); + + if (entry.data.itemPath == "") { + FileData* game {entry.object}; + + if (mItemType == "" || mItemType == "marquee") + entry.data.itemPath = game->getMarqueePath(); + else if (mItemType == "cover") + entry.data.itemPath = game->getCoverPath(); + else if (mItemType == "3dbox") + entry.data.itemPath = game->get3DBoxPath(); + else if (mItemType == "screenshot") + entry.data.itemPath = game->getScreenshotPath(); + else if (mItemType == "titlescreen") + entry.data.itemPath = game->getTitleScreenPath(); + else if (mItemType == "backcover") + entry.data.itemPath = game->getBackCoverPath(); + else if (mItemType == "miximage") + entry.data.itemPath = game->getMiximagePath(); + else if (mItemType == "fanart") + entry.data.itemPath = game->getFanArtPath(); + + auto theme = game->getSystem()->getTheme(); + updateEntry(entry, theme); + } + } + } +} + template bool CarouselComponent::input(InputConfig* config, Input input) { if (input.value != 0) { @@ -311,7 +394,7 @@ template bool CarouselComponent::input(InputConfig* config, Inpu } break; } - if constexpr (std::is_same_v) { + if (mGamelistView) { if (config->isMappedLike("leftshoulder", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); @@ -355,7 +438,7 @@ template bool CarouselComponent::input(InputConfig* config, Inpu } } else { - if constexpr (std::is_same_v) { + if (mGamelistView) { if (config->isMappedLike("up", input) || config->isMappedLike("down", input) || config->isMappedLike("left", input) || config->isMappedLike("right", input) || config->isMappedLike("leftshoulder", input) || @@ -367,7 +450,7 @@ template bool CarouselComponent::input(InputConfig* config, Inpu mTriggerJump = false; } } - if constexpr (std::is_same_v) { + else { if (config->isMappedLike("up", input) || config->isMappedLike("down", input) || config->isMappedLike("left", input) || config->isMappedLike("right", input)) List::listInput(0); @@ -385,7 +468,9 @@ template void CarouselComponent::update(int deltaTime) template void CarouselComponent::render(const glm::mat4& parentTrans) { - if (mEntries.size() == 0) + int numEntries {static_cast(mEntries.size())}; + + if (numEntries == 0) return; glm::mat4 carouselTrans {parentTrans}; @@ -475,15 +560,26 @@ template void CarouselComponent::render(const glm::mat4& parentT int center {static_cast(mEntryCamOffset)}; int itemInclusion {static_cast(std::ceil(mMaxItemCount / 2.0f))}; - bool singleEntry {mEntries.size() == 1}; + bool singleEntry {numEntries == 1}; + + struct renderStruct { + int index; + float distance; + float scale; + float opacity; + glm::mat4 trans; + }; + + std::vector renderItems; + std::vector renderItemsSorted; for (int i = center - itemInclusion; i < center + itemInclusion + 2; ++i) { int index {i}; while (index < 0) - index += static_cast(mEntries.size()); - while (index >= static_cast(mEntries.size())) - index -= static_cast(mEntries.size()); + index += numEntries; + while (index >= numEntries) + index -= numEntries; float distance {i - mEntryCamOffset}; @@ -514,25 +610,47 @@ template void CarouselComponent::render(const glm::mat4& parentT opacity = mUnfocusedItemOpacity + (maxDiff - (maxDiff * fabsf(distance))); } - const std::shared_ptr& comp {mEntries.at(index).data.item}; + renderStruct renderItem; + renderItem.index = index; + renderItem.distance = distance; + renderItem.scale = scale; + renderItem.opacity = opacity; + renderItem.trans = itemTrans; + + renderItems.emplace_back(renderItem); + } + + int belowCenter {static_cast(std::ceil(renderItems.size() / 2)) - 1}; + + // TODO: Fix glitches when navigating to the right. + // The following sorting makes sure that overlapping items are rendered in the correct order. + for (int i = 0; i < belowCenter - 0; ++i) + renderItemsSorted.emplace_back(renderItems[i]); + + for (int i = static_cast(renderItems.size()) - 1; i > belowCenter - 1; --i) + renderItemsSorted.emplace_back(renderItems[i]); + + for (auto& renderItem : renderItemsSorted) { + const std::shared_ptr& comp {mEntries.at(renderItem.index).data.item}; if (comp == nullptr) continue; if (mType == CarouselType::VERTICAL_WHEEL || mType == CarouselType::HORIZONTAL_WHEEL) { - comp->setRotationDegrees(mItemRotation * distance); + comp->setRotationDegrees(mItemRotation * renderItem.distance); comp->setRotationOrigin(mItemRotationOrigin); } - comp->setScale(scale); - comp->setOpacity(opacity); - comp->render(itemTrans); + comp->setScale(renderItem.scale); + comp->setOpacity(renderItem.opacity); + comp->render(renderItem.trans); + // TODO: Rewrite to use "real" reflections instead of this hack. // Don't attempt to add reflections for text entries. - if (mReflections && (mEntries.at(index).data.itemPath != "" || - mEntries.at(index).data.defaultItemPath != "")) { - itemTrans = - glm::translate(itemTrans, glm::vec3 {0.0f, comp->getSize().y * scale, 0.0f}); + if (mReflections && (mEntries.at(renderItem.index).data.itemPath != "" || + mEntries.at(renderItem.index).data.defaultItemPath != "")) { + glm::mat4 reflectionTrans {glm::translate( + renderItem.trans, glm::vec3 {0.0f, comp->getSize().y * renderItem.scale, 0.0f})}; const unsigned int colorShift {comp->getColorShift()}; comp->setColorGradientHorizontal(false); comp->setColorShift(0xFFFFFF00 | static_cast(mReflectionsOpacity * 255.0f)); @@ -543,7 +661,7 @@ template void CarouselComponent::render(const glm::mat4& parentT if (mReflectionsFalloff > 1.0f) comp->setReflectionsFalloff(mReflectionsFalloff - 1.0f); comp->setFlipY(true); - comp->render(itemTrans); + comp->render(reflectionTrans); comp->setFlipY(false); comp->setColorShift(colorShift); comp->setReflectionsFalloff(0.0f);