Skip to content

Commit d748b90

Browse files
Add favorite icon to game list with full theme support
Renders a heart icon inline before the game name for any entry the favorite indicator callback returns true for. The icon is sized at 60% of the row height, vertically centered, with a proportional gap before the text. Text alignment (left/center/right) and marquee scrolling both account for the icon+gap width so the combined label is positioned correctly. The icon also renders alongside the wrap-around repeat during marquee scroll. Theme authors can customize the icon via the textlist element: - favoriteIconPath — replace the default heart_filled.svg with any image - favoriteIconColor — apply a color/tint to the icon (default: white) - favoriteIconVisible — set false to suppress the icon entirely BasicGameListView wires the callback to check file metadata "favorite". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent db3848e commit d748b90

4 files changed

Lines changed: 71 additions & 6 deletions

File tree

es-app/src/components/TextListComponent.h

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class TextListComponent : public IList<TextListData, T>
6060
inline void setAlignment(Alignment align) { mAlignment = align; }
6161

6262
inline void setCursorChangedCallback(const std::function<void(CursorState state)>& func) { mCursorChangedCallback = func; }
63+
inline void setFavoriteIndicatorCallback(const std::function<bool(const T&)>& func) { mFavoriteIndicatorCallback = func; }
6364

6465
inline void setFont(const std::shared_ptr<Font>& font)
6566
{
@@ -109,17 +110,20 @@ class TextListComponent : public IList<TextListData, T>
109110
bool mSelectorColorGradientHorizontal = true;
110111
unsigned int mSelectedColor;
111112
std::string mScrollSound;
113+
std::function<bool(const T&)> mFavoriteIndicatorCallback;
112114
static const unsigned int COLOR_ID_COUNT = 2;
113115
unsigned int mColors[COLOR_ID_COUNT];
114116
int mViewportHeight;
115117
int mCursorPrev = -1;
116118

117119
ImageComponent mSelectorImage;
120+
ImageComponent mFavoriteIcon;
121+
bool mFavoriteIconVisible;
118122
};
119123

120124
template <typename T>
121125
TextListComponent<T>::TextListComponent(Window* window) :
122-
IList<TextListData, T>(window), mSelectorImage(window)
126+
IList<TextListData, T>(window), mSelectorImage(window), mFavoriteIcon(window)
123127
{
124128
mMarqueeOffset = 0;
125129
mMarqueeOffset2 = 0;
@@ -139,6 +143,9 @@ TextListComponent<T>::TextListComponent(Window* window) :
139143
mSelectedColor = 0;
140144
mColors[0] = 0x0000FFFF;
141145
mColors[1] = 0x00FF00FF;
146+
147+
mFavoriteIcon.setImage(":/heart_filled.svg");
148+
mFavoriteIconVisible = true;
142149
}
143150

144151
template <typename T>
@@ -204,6 +211,12 @@ void TextListComponent<T>::render(const Transform4x4f& parentTrans)
204211

205212
entry.data.textCache->setColor(color);
206213

214+
const bool showFavorite = mFavoriteIconVisible && (mFavoriteIndicatorCallback ? mFavoriteIndicatorCallback(entry.object) : false);
215+
const float iconSize = entrySize * 0.60f;
216+
const float iconGap = showFavorite ? (iconSize * 0.35f) : 0.0f;
217+
const float textWidth = entry.data.textCache->metrics.size.x();
218+
const float lineWidth = showFavorite ? (iconSize + iconGap + textWidth) : textWidth;
219+
207220
Vector3f offset(0, y, 0);
208221

209222
switch(mAlignment)
@@ -212,26 +225,41 @@ void TextListComponent<T>::render(const Transform4x4f& parentTrans)
212225
offset[0] = mHorizontalMargin;
213226
break;
214227
case ALIGN_CENTER:
215-
offset[0] = (int)((mSize.x() - entry.data.textCache->metrics.size.x()) / 2);
228+
offset[0] = (int)((mSize.x() - lineWidth) / 2);
216229
if(offset[0] < mHorizontalMargin)
217230
offset[0] = mHorizontalMargin;
218231
break;
219232
case ALIGN_RIGHT:
220-
offset[0] = (mSize.x() - entry.data.textCache->metrics.size.x());
233+
offset[0] = (mSize.x() - lineWidth);
221234
offset[0] -= mHorizontalMargin;
222235
if(offset[0] < mHorizontalMargin)
223236
offset[0] = mHorizontalMargin;
224237
break;
225238
}
226239

240+
float rowScrollOffset = 0.0f;
241+
if((mCursor == i) && (mMarqueeOffset > 0))
242+
rowScrollOffset = (float)mMarqueeOffset;
243+
244+
if(showFavorite)
245+
{
246+
Transform4x4f iconTrans = trans;
247+
iconTrans.translate(offset - Vector3f(rowScrollOffset, 0, 0));
248+
mFavoriteIcon.setPosition(0, (entrySize - iconSize) * 0.5f, 0);
249+
mFavoriteIcon.setResize(iconSize, iconSize);
250+
mFavoriteIcon.render(iconTrans);
251+
}
252+
253+
Vector3f textOffset = offset + Vector3f(showFavorite ? (iconSize + iconGap) : 0.0f, 0, 0);
254+
227255
// render text
228256
Transform4x4f drawTrans = trans;
229257

230258
// currently selected item text might be scrolling
231259
if((mCursor == i) && (mMarqueeOffset > 0))
232-
drawTrans.translate(offset - Vector3f((float)mMarqueeOffset, 0, 0));
260+
drawTrans.translate(textOffset - Vector3f((float)mMarqueeOffset, 0, 0));
233261
else
234-
drawTrans.translate(offset);
262+
drawTrans.translate(textOffset);
235263

236264
Renderer::setMatrix(drawTrans);
237265
font->renderTextCache(entry.data.textCache.get());
@@ -240,8 +268,16 @@ void TextListComponent<T>::render(const Transform4x4f& parentTrans)
240268
// marquee is scrolled far enough for it to repeat
241269
if((mCursor == i) && (mMarqueeOffset2 < 0))
242270
{
271+
// also render the favorite icon alongside the wrap-around copy of the text
272+
if(showFavorite)
273+
{
274+
Transform4x4f iconTrans2 = trans;
275+
iconTrans2.translate(offset - Vector3f((float)mMarqueeOffset2, 0, 0));
276+
mFavoriteIcon.render(iconTrans2);
277+
}
278+
243279
drawTrans = trans;
244-
drawTrans.translate(offset - Vector3f((float)mMarqueeOffset2, 0, 0));
280+
drawTrans.translate(textOffset - Vector3f((float)mMarqueeOffset2, 0, 0));
245281
Renderer::setMatrix(drawTrans);
246282
font->renderTextCache(entry.data.textCache.get());
247283
}
@@ -481,6 +517,21 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme, c
481517
} else {
482518
mSelectorImage.setImage("");
483519
}
520+
521+
if (elem->has("favoriteIconPath"))
522+
mFavoriteIcon.setImage(elem->get<std::string>("favoriteIconPath"));
523+
else
524+
mFavoriteIcon.setImage(":/heart_filled.svg");
525+
526+
if (elem->has("favoriteIconColor"))
527+
mFavoriteIcon.setColorShift(elem->get<unsigned int>("favoriteIconColor"));
528+
else
529+
mFavoriteIcon.setColorShift(0xFFFFFFFF);
530+
531+
if (elem->has("favoriteIconVisible"))
532+
mFavoriteIconVisible = elem->get<bool>("favoriteIconVisible");
533+
else
534+
mFavoriteIconVisible = true;
484535
}
485536

486537
#endif // ES_APP_COMPONENTS_TEXT_LIST_COMPONENT_H

es-app/src/views/gamelist/BasicGameListView.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ BasicGameListView::BasicGameListView(Window* window, FileData* root)
1313
mList.setSize(mSize.x(), mSize.y() * 0.8f);
1414
mList.setPosition(0, mSize.y() * 0.2f);
1515
mList.setDefaultZIndex(20);
16+
mList.setFavoriteIndicatorCallback([](FileData* file) {
17+
return file != nullptr && file->getType() == GAME && file->metadata.get("favorite") == "true";
18+
});
1619
addChild(&mList);
1720

1821
populateList(root->getChildrenListToDisplay());

es-core/src/ThemeData.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>> The
9292
{ "horizontalMargin", RESOLUTION_FLOAT },
9393
{ "forceUppercase", BOOLEAN },
9494
{ "lineSpacing", FLOAT },
95+
{ "favoriteIconPath", PATH },
96+
{ "favoriteIconColor", COLOR },
97+
{ "favoriteIconVisible", BOOLEAN },
9598
{ "zIndex", FLOAT } } },
9699
{ "container", {
97100
{ "pos", RESOLUTION_PAIR },

resources/heart_filled.svg

Lines changed: 8 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)