diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index 2250769d30..6c415699ec 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -60,6 +60,7 @@ class TextListComponent : public IList inline void setAlignment(Alignment align) { mAlignment = align; } inline void setCursorChangedCallback(const std::function& func) { mCursorChangedCallback = func; } + inline void setFavoriteIndicatorCallback(const std::function& func) { mFavoriteIndicatorCallback = func; } inline void setFont(const std::shared_ptr& font) { @@ -109,17 +110,20 @@ class TextListComponent : public IList bool mSelectorColorGradientHorizontal = true; unsigned int mSelectedColor; std::string mScrollSound; + std::function mFavoriteIndicatorCallback; static const unsigned int COLOR_ID_COUNT = 2; unsigned int mColors[COLOR_ID_COUNT]; int mViewportHeight; int mCursorPrev = -1; ImageComponent mSelectorImage; + ImageComponent mFavoriteIcon; + bool mFavoriteIconVisible; }; template TextListComponent::TextListComponent(Window* window) : - IList(window), mSelectorImage(window) + IList(window), mSelectorImage(window), mFavoriteIcon(window) { mMarqueeOffset = 0; mMarqueeOffset2 = 0; @@ -139,6 +143,9 @@ TextListComponent::TextListComponent(Window* window) : mSelectedColor = 0; mColors[0] = 0x0000FFFF; mColors[1] = 0x00FF00FF; + + mFavoriteIcon.setImage(":/heart_filled.svg"); + mFavoriteIconVisible = true; } template @@ -204,6 +211,12 @@ void TextListComponent::render(const Transform4x4f& parentTrans) entry.data.textCache->setColor(color); + const bool showFavorite = mFavoriteIconVisible && (mFavoriteIndicatorCallback ? mFavoriteIndicatorCallback(entry.object) : false); + const float iconSize = entrySize * 0.60f; + const float iconGap = showFavorite ? (iconSize * 0.35f) : 0.0f; + const float textWidth = entry.data.textCache->metrics.size.x(); + const float lineWidth = showFavorite ? (iconSize + iconGap + textWidth) : textWidth; + Vector3f offset(0, y, 0); switch(mAlignment) @@ -212,26 +225,41 @@ void TextListComponent::render(const Transform4x4f& parentTrans) offset[0] = mHorizontalMargin; break; case ALIGN_CENTER: - offset[0] = (int)((mSize.x() - entry.data.textCache->metrics.size.x()) / 2); + offset[0] = (int)((mSize.x() - lineWidth) / 2); if(offset[0] < mHorizontalMargin) offset[0] = mHorizontalMargin; break; case ALIGN_RIGHT: - offset[0] = (mSize.x() - entry.data.textCache->metrics.size.x()); + offset[0] = (mSize.x() - lineWidth); offset[0] -= mHorizontalMargin; if(offset[0] < mHorizontalMargin) offset[0] = mHorizontalMargin; break; } + float rowScrollOffset = 0.0f; + if((mCursor == i) && (mMarqueeOffset > 0)) + rowScrollOffset = (float)mMarqueeOffset; + + if(showFavorite) + { + Transform4x4f iconTrans = trans; + iconTrans.translate(offset - Vector3f(rowScrollOffset, 0, 0)); + mFavoriteIcon.setPosition(0, (entrySize - iconSize) * 0.5f, 0); + mFavoriteIcon.setResize(iconSize, iconSize); + mFavoriteIcon.render(iconTrans); + } + + Vector3f textOffset = offset + Vector3f(showFavorite ? (iconSize + iconGap) : 0.0f, 0, 0); + // render text Transform4x4f drawTrans = trans; // currently selected item text might be scrolling if((mCursor == i) && (mMarqueeOffset > 0)) - drawTrans.translate(offset - Vector3f((float)mMarqueeOffset, 0, 0)); + drawTrans.translate(textOffset - Vector3f((float)mMarqueeOffset, 0, 0)); else - drawTrans.translate(offset); + drawTrans.translate(textOffset); Renderer::setMatrix(drawTrans); font->renderTextCache(entry.data.textCache.get()); @@ -240,8 +268,16 @@ void TextListComponent::render(const Transform4x4f& parentTrans) // marquee is scrolled far enough for it to repeat if((mCursor == i) && (mMarqueeOffset2 < 0)) { + // also render the favorite icon alongside the wrap-around copy of the text + if(showFavorite) + { + Transform4x4f iconTrans2 = trans; + iconTrans2.translate(offset - Vector3f((float)mMarqueeOffset2, 0, 0)); + mFavoriteIcon.render(iconTrans2); + } + drawTrans = trans; - drawTrans.translate(offset - Vector3f((float)mMarqueeOffset2, 0, 0)); + drawTrans.translate(textOffset - Vector3f((float)mMarqueeOffset2, 0, 0)); Renderer::setMatrix(drawTrans); font->renderTextCache(entry.data.textCache.get()); } @@ -481,6 +517,21 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, c } else { mSelectorImage.setImage(""); } + + if (elem->has("favoriteIconPath")) + mFavoriteIcon.setImage(elem->get("favoriteIconPath")); + else + mFavoriteIcon.setImage(":/heart_filled.svg"); + + if (elem->has("favoriteIconColor")) + mFavoriteIcon.setColorShift(elem->get("favoriteIconColor")); + else + mFavoriteIcon.setColorShift(0xFFFFFFFF); + + { + std::string var = theme->getVariable("favoriteIconVisible"); + mFavoriteIconVisible = (var == "true"); + } } #endif // ES_APP_COMPONENTS_TEXT_LIST_COMPONENT_H diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 605c8bad2b..24a919cd78 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -13,6 +13,9 @@ BasicGameListView::BasicGameListView(Window* window, FileData* root) mList.setSize(mSize.x(), mSize.y() * 0.8f); mList.setPosition(0, mSize.y() * 0.2f); mList.setDefaultZIndex(20); + mList.setFavoriteIndicatorCallback([](FileData* file) { + return file != nullptr && file->getType() == GAME && file->metadata.get("favorite") == "true"; + }); addChild(&mList); populateList(root->getChildrenListToDisplay()); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 4f299adce8..db4cc63d65 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -92,6 +92,8 @@ std::map> The { "horizontalMargin", RESOLUTION_FLOAT }, { "forceUppercase", BOOLEAN }, { "lineSpacing", FLOAT }, + { "favoriteIconPath", PATH }, + { "favoriteIconColor", COLOR }, { "zIndex", FLOAT } } }, { "container", { { "pos", RESOLUTION_PAIR }, @@ -515,7 +517,9 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map= 2 && str[0] == ':' && str[1] == '/') + ? str + : Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true, false); if(!ResourceManager::getInstance()->fileExists(path)) { std::stringstream ss; @@ -560,6 +564,14 @@ bool ThemeData::hasView(const std::string& view) return (viewIt != mViews.cend()); } +std::string ThemeData::getVariable(const std::string& name) const +{ + auto it = mVariables.find(name); + if (it != mVariables.cend()) + return it->second; + return ""; +} + const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const { auto viewIt = mViews.find(view); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index ae91d7c8de..0f70c8e6f1 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -161,6 +161,9 @@ class ThemeData // If expectedType is an empty string, will do no type checking. const ThemeElement* getElement(const std::string& view, const std::string& element, const std::string& expectedType) const; + // Returns the value of a theme variable, or empty string if not defined. + std::string getVariable(const std::string& name) const; + static std::vector makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window); static const std::shared_ptr& getDefault(); diff --git a/resources/heart_filled.svg b/resources/heart_filled.svg new file mode 100644 index 0000000000..3a8356b609 --- /dev/null +++ b/resources/heart_filled.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file