diff --git a/THEMES-DEV.md b/THEMES-DEV.md index 1b998b9da..b2a659cf0 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -366,6 +366,8 @@ Below are the default zIndex values per element type: * `image name="logo"` * Gamelist information - 50 * `text name="gamelistInfo"` +* Badges - 50 + * `badges name="md_badges"` ### Theme variables @@ -474,6 +476,8 @@ or to specify only a portion of the value of a theme property: - The "genre" metadata. * `text name="md_players"` - ALL - The "players" metadata (number of players the game supports). + * `badges name="md_badges"` - ALL + - The "badges" metadata. Displayed as a group of badges that indicate metadata such as favorites and completed games. * `datetime name="md_lastplayed"` - ALL - The "lastplayed" metadata. Displayed as a string representing the time relative to "now" (e.g. "3 hours ago"). * `text name="md_playcount"` - ALL @@ -529,6 +533,8 @@ or to specify only a portion of the value of a theme property: - The "genre" metadata. * `text name="md_players"` - ALL - The "players" metadata (number of players the game supports). + * `badges name="md_badges"` - ALL + - The "badges" metadata. Displayed as a group of badges that indicate metadata such as favorites and completed games. * `datetime name="md_lastplayed"` - ALL - The "lastplayed" metadata. Displayed as a string representing the time relative to "now" (e.g. "3 hours ago"). * `text name="md_playcount"` - ALL @@ -582,6 +588,8 @@ or to specify only a portion of the value of a theme property: - The "genre" metadata. * `text name="md_players"` - ALL - The "players" metadata (number of players the game supports). + * `badges name="md_badges"` - ALL + - The "badges" metadata. Displayed as a group of badges that indicate metadata such as favorites and completed games. * `datetime name="md_lastplayed"` - ALL - The "lastplayed" metadata. Displayed as a string representing the time relative to "now" (e.g. "3 hours ago"). * `text name="md_playcount"` - ALL @@ -900,6 +908,47 @@ ES-DE borrows the concept of "nine patches" from Android (or "9-Slices"). Curren `button_back_XBOX360`, `button_start_XBOX360`. +#### badges + +* `pos` - type: NORMALIZED_PAIR. +* `size` - type: NORMALIZED_PAIR. + - Possible combinations: + - `w h` - Dimensions of the badges container. The badges will be scaled to fit within these dimensions. +* `origin` - type: NORMALIZED_PAIR. + - Where on the component `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen. If the "POSITION" and "SIZE" attributes are themeable, "ORIGIN" is implied. +* `rotation` - type: FLOAT. + - angle in degrees that the image should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. +* `rotationOrigin` - type: NORMALIZED_PAIR. + - Point around which the image will be rotated. Defaults to `0.5 0.5`. +* `itemsPerRow` - type: FLOAT. + - Number of badges that fit on a row. When more badges are available a new row will be started. +* `rows` - type: FLOAT. + - The number of rows available. +* `itemPlacement` - type: STRING. + - Valid values are "top", "center", "bottom", or "stretch". Controls vertical alignment of each badge if images of different heights are used. "Stretch" will stretch the badge to the full height. +* `itemMargin` - type: NORMALIZED_PAIR. + - The margins between badges. Possible combinations: + - `x y` - horizontal and vertical margins. +* `slots` - type: STRING. + - The badge types that should be displayed. Should be specified as a list of strings separated by spaces. The order will be followed when placing badges on the screen. + - Available badges are: + - "favorite": Will be shown when the game is marked as favorite. + - "completed": Will be shown when the game is marked as completed. + - "kidgame": Will be shown when the game is marked as a kids game. + - "broken": Will be shown when the game is marked as broken. + - "altemulator": Will be shown when an alternative emulator is setup for the game. +* `customBadgeIcon` - type: PATH. + - A badge icon override. Specify the badge type in the attribute `badge`. The available badges are: + `favorite`, + `completed`, + `kidgame`, + `broken`, + `altemulator` +* `visible` - type: BOOLEAN. + - If true, component will be rendered, otherwise rendering will be skipped. Can be used to hide elements from a particular view. +* `zIndex` - type: FLOAT. + - z-index value for component. Components will be rendered in order of z-index value from low to high. + #### carousel * `type` - type: STRING. diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index a02e31878..35808dd29 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -38,6 +38,7 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) , mLastPlayed(window) , mPlayCount(window) , mName(window) + , mBadges(window) , mDescContainer(window) , mDescription(window) , mGamelistInfo(window) @@ -101,6 +102,13 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) addChild(&mLblPlayCount); addChild(&mPlayCount); + // Badges. + addChild(&mBadges); + mBadges.setOrigin(0.0f, 0.0f); + mBadges.setPosition(mSize.x * 0.8f, mSize.y * 0.7f); + mBadges.setSize(mSize.x * 0.15, mSize.y * 0.2f); + mBadges.setDefaultZIndex(50.0f); + mName.setPosition(mSize.x, mSize.y); mName.setDefaultZIndex(40.0f); mName.setColor(0xAAAAAAFF); @@ -141,6 +149,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE); mName.applyTheme(theme, getName(), "md_name", ALL); + mBadges.applyTheme(theme, getName(), "md_badges", ALL); initMDLabels(); std::vector labels = getMDLabels(); @@ -297,6 +306,7 @@ void DetailedGameListView::updateInfoPanel() mLastPlayed.setVisible(false); mLblPlayCount.setVisible(false); mPlayCount.setVisible(false); + mBadges.setVisible(false); } else { mLblRating.setVisible(true); @@ -315,6 +325,7 @@ void DetailedGameListView::updateInfoPanel() mLastPlayed.setVisible(true); mLblPlayCount.setVisible(true); mPlayCount.setVisible(true); + mBadges.setVisible(true); } bool fadingOut = false; @@ -397,6 +408,21 @@ void DetailedGameListView::updateInfoPanel() mPublisher.setValue(file->metadata.get("publisher")); mGenre.setValue(file->metadata.get("genre")); mPlayers.setValue(file->metadata.get("players")); + + // Populate the badge slots based on game metadata. + std::vector badgeSlots; + for (auto badge : mBadges.getBadgeTypes()) { + if (badge == "altemulator") { + if (file->metadata.get(badge).compare("") != 0) + badgeSlots.push_back(badge); + } + else { + if (file->metadata.get(badge).compare("true") == 0) + badgeSlots.push_back(badge); + } + } + mBadges.setBadges(badgeSlots); + mName.setValue(file->metadata.get("name")); if (file->getType() == GAME) { @@ -422,6 +448,7 @@ void DetailedGameListView::updateInfoPanel() comps.push_back(&mImage); comps.push_back(&mDescription); comps.push_back(&mName); + comps.push_back(&mBadges); std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index f0b0202a9..a6033b2f6 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -9,6 +9,7 @@ #ifndef ES_APP_VIEWS_GAME_LIST_DETAILED_GAME_LIST_VIEW_H #define ES_APP_VIEWS_GAME_LIST_DETAILED_GAME_LIST_VIEW_H +#include "components/BadgesComponent.h" #include "components/DateTimeComponent.h" #include "components/RatingComponent.h" #include "components/ScrollableContainer.h" @@ -55,6 +56,7 @@ private: DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; + BadgesComponent mBadges; std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index efb322d9a..1efa09469 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -32,6 +32,7 @@ GridGameListView::GridGameListView(Window* window, FileData* root) , mLblPlayers(window) , mLblLastPlayed(window) , mLblPlayCount(window) + , mBadges(window) , mRating(window) , mReleaseDate(window) , mDeveloper(window) @@ -55,6 +56,7 @@ GridGameListView::GridGameListView(Window* window, FileData* root) populateList(root->getChildrenListToDisplay(), root); // Metadata labels + values. + addChild(&mBadges); mLblRating.setText("Rating: ", false); addChild(&mLblRating); addChild(&mRating); diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index 9a0bf0714..2c2f50458 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -9,6 +9,7 @@ #ifndef ES_APP_VIEWS_GAME_LIST_GRID_GAME_LIST_VIEW_H #define ES_APP_VIEWS_GAME_LIST_GRID_GAME_LIST_VIEW_H +#include "components/BadgesComponent.h" #include "components/DateTimeComponent.h" #include "components/ImageGridComponent.h" #include "components/RatingComponent.h" @@ -88,6 +89,7 @@ private: TextComponent mLblLastPlayed; TextComponent mLblPlayCount; + BadgesComponent mBadges; RatingComponent mRating; DateTimeComponent mReleaseDate; TextComponent mDeveloper; diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 33f50e600..6e98be2e1 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -45,6 +45,7 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) , mLastPlayed(window) , mPlayCount(window) , mName(window) + , mBadges(window) , mDescContainer(window) , mDescription(window) , mGamelistInfo(window) @@ -118,6 +119,13 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) addChild(&mLblPlayCount); addChild(&mPlayCount); + // Badges. + addChild(&mBadges); + mBadges.setOrigin(0.0f, 0.0f); + mBadges.setPosition(mSize.x * 0.8f, mSize.y * 0.7f); + mBadges.setSize(mSize.x * 0.15, mSize.y * 0.2f); + mBadges.setDefaultZIndex(50.0f); + mName.setPosition(mSize.x, mSize.y); mName.setDefaultZIndex(40.0f); mName.setColor(0xAAAAAAFF); @@ -163,6 +171,7 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION | VISIBLE); mName.applyTheme(theme, getName(), "md_name", ALL); + mBadges.applyTheme(theme, getName(), "md_badges", ALL); initMDLabels(); std::vector labels = getMDLabels(); @@ -319,6 +328,7 @@ void VideoGameListView::updateInfoPanel() mLastPlayed.setVisible(false); mLblPlayCount.setVisible(false); mPlayCount.setVisible(false); + mBadges.setVisible(false); } else { mLblRating.setVisible(true); @@ -337,6 +347,7 @@ void VideoGameListView::updateInfoPanel() mLastPlayed.setVisible(true); mLblPlayCount.setVisible(true); mPlayCount.setVisible(true); + mBadges.setVisible(true); } bool fadingOut = false; @@ -437,6 +448,21 @@ void VideoGameListView::updateInfoPanel() mPublisher.setValue(file->metadata.get("publisher")); mGenre.setValue(file->metadata.get("genre")); mPlayers.setValue(file->metadata.get("players")); + + // Populate the badge slots based on game metadata. + std::vector badgeSlots; + for (auto badge : mBadges.getBadgeTypes()) { + if (badge == "altemulator") { + if (file->metadata.get(badge).compare("") != 0) + badgeSlots.push_back(badge); + } + else { + if (file->metadata.get(badge).compare("true") == 0) + badgeSlots.push_back(badge); + } + } + mBadges.setBadges(badgeSlots); + mName.setValue(file->metadata.get("name")); if (file->getType() == GAME) { @@ -462,6 +488,7 @@ void VideoGameListView::updateInfoPanel() comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); + comps.push_back(&mBadges); std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); diff --git a/es-app/src/views/gamelist/VideoGameListView.h b/es-app/src/views/gamelist/VideoGameListView.h index ff1129742..f5cae00d0 100644 --- a/es-app/src/views/gamelist/VideoGameListView.h +++ b/es-app/src/views/gamelist/VideoGameListView.h @@ -9,6 +9,7 @@ #ifndef ES_APP_VIEWS_GAME_LIST_VIDEO_GAME_LIST_VIEW_H #define ES_APP_VIEWS_GAME_LIST_VIDEO_GAME_LIST_VIEW_H +#include "components/BadgesComponent.h" #include "components/DateTimeComponent.h" #include "components/RatingComponent.h" #include "components/ScrollableContainer.h" @@ -59,6 +60,7 @@ private: DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; + BadgesComponent mBadges; std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 9cc65d92c..6949f2a32 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -34,12 +34,14 @@ set(CORE_HEADERS # GUI components ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgesComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentGrid.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentList.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeEditComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/FlexboxComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GridTileComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/HelpComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/IList.h @@ -110,12 +112,14 @@ set(CORE_SOURCES # GUI components ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgesComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentGrid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeEditComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/FlexboxComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GridTileComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/HelpComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ImageComponent.cpp diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 026897b3a..603a0131b 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -146,6 +146,21 @@ std::map> The {"unfilledPath", PATH}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, + {"badges", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"rotation", FLOAT}, + {"rotationOrigin", NORMALIZED_PAIR}, + {"alignment", STRING}, + {"itemsPerRow", FLOAT}, + {"rows", FLOAT}, + {"itemPlacement", STRING}, + {"itemMargin", NORMALIZED_PAIR}, + {"slots", STRING}, + {"customBadgeIcon", PATH}, + {"visible", BOOLEAN}, + {"zIndex", FLOAT}}}, {"sound", {{"path", PATH}}}, {"helpsystem", {{"pos", NORMALIZED_PAIR}, @@ -503,8 +518,8 @@ void ThemeData::parseElement(const pugi::xml_node& root, ""); } - // Special parsing instruction for customButtonIcon -> save node as it's button - // attribute to prevent nodes overwriting each other. + // Special parsing instruction for recurring options. + // Store as it's attribute to prevent nodes overwriting each other. if (strcmp(node.name(), "customButtonIcon") == 0) { const auto btn = node.attribute("button").as_string(""); if (strcmp(btn, "") == 0) @@ -513,6 +528,13 @@ void ThemeData::parseElement(const pugi::xml_node& root, else element.properties[btn] = path; } + else if (strcmp(node.name(), "customBadgeIcon") == 0) { + const auto btn = node.attribute("badge").as_string(""); + if (strcmp(btn, "") == 0) + LOG(LogError) << " element requires the `badge` property."; + else + element.properties[btn] = path; + } else element.properties[node.name()] = path; diff --git a/es-core/src/components/BadgesComponent.cpp b/es-core/src/components/BadgesComponent.cpp new file mode 100644 index 000000000..bce5eaeca --- /dev/null +++ b/es-core/src/components/BadgesComponent.cpp @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// BadgesComponent.cpp +// +// Game badges icons. +// Used by the gamelist views. +// + +#define SLOT_FAVORITE "favorite" +#define SLOT_COMPLETED "completed" +#define SLOT_KIDGAME "kidgame" +#define SLOT_BROKEN "broken" +#define SLOT_ALTEMULATOR "altemulator" + +#include "components/BadgesComponent.h" + +#include "Log.h" +#include "ThemeData.h" +#include "utils/StringUtil.h" + +BadgesComponent::BadgesComponent(Window* window) + : GuiComponent{window} + , mFlexboxComponent{window, mBadgeImages} + , mBadgeTypes{{SLOT_FAVORITE, SLOT_COMPLETED, SLOT_KIDGAME, SLOT_BROKEN, SLOT_ALTEMULATOR}} +{ + mBadgeIcons[SLOT_FAVORITE] = ":/graphics/badge_favorite.svg"; + mBadgeIcons[SLOT_COMPLETED] = ":/graphics/badge_completed.svg"; + mBadgeIcons[SLOT_KIDGAME] = ":/graphics/badge_kidgame.svg"; + mBadgeIcons[SLOT_BROKEN] = ":/graphics/badge_broken.svg"; + mBadgeIcons[SLOT_ALTEMULATOR] = ":/graphics/badge_altemulator.svg"; +} + +void BadgesComponent::setBadges(const std::vector& badges) +{ + std::map prevVisibility; + + // Save the visibility status to know whether any badges changed. + for (auto& image : mBadgeImages) { + prevVisibility[image.first] = image.second.isVisible(); + image.second.setVisible(false); + } + + for (auto& badge : badges) { + auto it = std::find_if( + mBadgeImages.begin(), mBadgeImages.end(), + [badge](std::pair image) { return image.first == badge; }); + + if (it != mBadgeImages.cend()) + it->second.setVisible(true); + } + + // Only recalculate the flexbox if any badges changed. + for (auto& image : mBadgeImages) { + if (prevVisibility[image.first] != image.second.isVisible()) { + mFlexboxComponent.onSizeChanged(); + break; + } + } +} + +void BadgesComponent::render(const glm::mat4& parentTrans) +{ + if (!isVisible()) + return; + + if (mOpacity == 255) { + mFlexboxComponent.render(parentTrans); + } + else { + mFlexboxComponent.setOpacity(mOpacity); + mFlexboxComponent.render(parentTrans); + mFlexboxComponent.setOpacity(255); + } +} + +void BadgesComponent::applyTheme(const std::shared_ptr& theme, + const std::string& view, + const std::string& element, + unsigned int properties) +{ + using namespace ThemeFlags; + + const ThemeData::ThemeElement* elem{theme->getElement(view, element, "badges")}; + if (!elem) + return; + + if (elem->has("alignment")) { + const std::string alignment{elem->get("alignment")}; + if (alignment != "left" && alignment != "right") { + LOG(LogWarning) << "BadgesComponent: Invalid theme configuration, set to \"" + << alignment << "\""; + } + else { + mFlexboxComponent.setAlignment(alignment); + } + } + + if (elem->has("itemsPerRow")) { + const float itemsPerRow{elem->get("itemsPerRow")}; + if (itemsPerRow < 1.0f || itemsPerRow > 10.0f) { + LOG(LogWarning) + << "BadgesComponent: Invalid theme configuration, set to \"" + << itemsPerRow << "\""; + } + else { + mFlexboxComponent.setItemsPerLine(static_cast(itemsPerRow)); + } + } + + if (elem->has("rows")) { + const float rows{elem->get("rows")}; + if (rows < 1.0f || rows > 10.0f) { + LOG(LogWarning) << "BadgesComponent: Invalid theme configuration, set to \"" + << rows << "\""; + } + else { + mFlexboxComponent.setLines(static_cast(rows)); + } + } + + if (elem->has("itemPlacement")) { + std::string itemPlacement{elem->get("itemPlacement")}; + if (itemPlacement != "top" && itemPlacement != "center" && itemPlacement != "bottom" && + itemPlacement != "stretch") { + LOG(LogWarning) + << "BadgesComponent: Invalid theme configuration, set to \"" + << itemPlacement << "\""; + } + else { + if (itemPlacement == "top") + itemPlacement = "start"; + else if (itemPlacement == "bottom") + itemPlacement = "end"; + mFlexboxComponent.setItemPlacement(itemPlacement); + } + } + + if (elem->has("itemMargin")) { + const glm::vec2 itemMargin = elem->get("itemMargin"); + if (itemMargin.x < 0.0f || itemMargin.x > 100.0f || itemMargin.y < 0.0f || + itemMargin.y > 100.0f) { + LOG(LogWarning) + << "BadgesComponent: Invalid theme configuration, set to \"" + << itemMargin.x << "x" << itemMargin.y << "\""; + } + else { + mFlexboxComponent.setItemMargin(itemMargin); + } + } + + if (elem->has("slots")) { + std::vector slots = Utils::String::delimitedStringToVector( + Utils::String::toLower(elem->get("slots")), " "); + + for (auto slot : slots) { + if (std::find(mBadgeTypes.cbegin(), mBadgeTypes.cend(), slot) != mBadgeTypes.end()) { + if (properties & PATH && elem->has(slot)) + mBadgeIcons[slot] = elem->get(slot); + + ImageComponent badgeImage{mWindow}; + + badgeImage.setImage(mBadgeIcons[slot]); + badgeImage.setVisible(false); + mBadgeImages.push_back(std::make_pair(slot, badgeImage)); + } + else { + LOG(LogError) << "Invalid badge slot \"" << slot << "\" defined"; + } + } + + GuiComponent::applyTheme(theme, view, element, properties); + + mFlexboxComponent.setPosition(mPosition); + mFlexboxComponent.setSize(mSize); + mFlexboxComponent.setOrigin(mOrigin); + mFlexboxComponent.setRotation(mRotation); + mFlexboxComponent.setRotationOrigin(mRotationOrigin); + mFlexboxComponent.setVisible(mVisible); + mFlexboxComponent.setDefaultZIndex(mDefaultZIndex); + mFlexboxComponent.setZIndex(mZIndex); + } +} diff --git a/es-core/src/components/BadgesComponent.h b/es-core/src/components/BadgesComponent.h new file mode 100644 index 000000000..12cbd5927 --- /dev/null +++ b/es-core/src/components/BadgesComponent.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// BadgesComponent.h +// +// Game badges icons. +// Used by the gamelist views. +// + +#ifndef ES_CORE_COMPONENTS_BADGES_COMPONENT_H +#define ES_CORE_COMPONENTS_BADGES_COMPONENT_H + +#include "FlexboxComponent.h" +#include "GuiComponent.h" + +class BadgesComponent : public GuiComponent +{ +public: + BadgesComponent(Window* window); + + std::vector getBadgeTypes() { return mBadgeTypes; } + void setBadges(const std::vector& badges); + + void render(const glm::mat4& parentTrans) override; + void onSizeChanged() override { mFlexboxComponent.onSizeChanged(); } + + virtual void applyTheme(const std::shared_ptr& theme, + const std::string& view, + const std::string& element, + unsigned int properties) override; + +private: + FlexboxComponent mFlexboxComponent; + + std::vector mBadgeTypes; + std::map mBadgeIcons; + std::vector> mBadgeImages; +}; + +#endif // ES_CORE_COMPONENTS_BADGES_COMPONENT_H diff --git a/es-core/src/components/FlexboxComponent.cpp b/es-core/src/components/FlexboxComponent.cpp new file mode 100644 index 000000000..4d921e47f --- /dev/null +++ b/es-core/src/components/FlexboxComponent.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// FlexboxComponent.cpp +// +// Flexbox layout component. +// + +#define DEFAULT_DIRECTION "row" +#define DEFAULT_ALIGNMENT "left" +#define DEFAULT_ITEMS_PER_LINE 4 +#define DEFAULT_LINES 2 +#define DEFAULT_ITEM_PLACEMENT "center" +#define DEFAULT_MARGIN_X 10.0f +#define DEFAULT_MARGIN_Y 10.0f + +#include "components/FlexboxComponent.h" + +#include "Settings.h" +#include "ThemeData.h" + +FlexboxComponent::FlexboxComponent(Window* window, + std::vector>& images) + : GuiComponent{window} + , mImages(images) + , mDirection{DEFAULT_DIRECTION} + , mAlignment{DEFAULT_ALIGNMENT} + , mItemsPerLine{DEFAULT_ITEMS_PER_LINE} + , mLines{DEFAULT_LINES} + , mItemPlacement{DEFAULT_ITEM_PLACEMENT} + , mItemMargin{glm::vec2{DEFAULT_MARGIN_X, DEFAULT_MARGIN_Y}} + , mLayoutValid{false} +{ +} + +void FlexboxComponent::computeLayout() +{ + // Start placing items in the top-left. + float anchorX{0.0f}; + float anchorY{0.0f}; + + // Translation directions when placing items. + glm::vec2 directionLine{1, 0}; + glm::vec2 directionRow{0, 1}; + + // Change direction. + if (mDirection == "column") { + directionLine = {0, 1}; + directionRow = {1, 0}; + } + + // Compute maximum image dimensions. + glm::vec2 grid; + if (mDirection == "row") + grid = {mItemsPerLine, mLines}; + else + grid = {mLines, mItemsPerLine}; + glm::vec2 maxItemSize{(mSize + mItemMargin - grid * mItemMargin) / grid}; + + if (grid.x * grid.y < static_cast(mImages.size())) { + LOG(LogWarning) << "FlexboxComponent: Invalid theme configuration, the number of badges " + "exceeds the product of times "; + } + + // Set final image dimensions. + for (auto& image : mImages) { + if (!image.second.isVisible()) + continue; + auto oldSize{image.second.getSize()}; + if (oldSize.x == 0) + oldSize.x = maxItemSize.x; + glm::vec2 sizeMaxX{maxItemSize.x, oldSize.y * (maxItemSize.x / oldSize.x)}; + glm::vec2 sizeMaxY{oldSize.x * (maxItemSize.y / oldSize.y), maxItemSize.y}; + glm::vec2 newSize; + if (sizeMaxX.y > maxItemSize.y) + newSize = sizeMaxY; + else if (sizeMaxY.x > maxItemSize.x) + newSize = sizeMaxX; + else + newSize = sizeMaxX.x * sizeMaxX.y >= sizeMaxY.x * sizeMaxY.y ? sizeMaxX : sizeMaxY; + image.second.setResize(newSize.x, newSize.y); + } + + // Pre-compute layout parameters. + float anchorXStart{anchorX}; + float anchorYStart{anchorY}; + + int i = 0; + + // Iterate through the images. + for (auto& image : mImages) { + if (!image.second.isVisible()) + continue; + + auto size{image.second.getSize()}; + + // Top-left anchor position. + float x{anchorX}; + float y{anchorY}; + + // Apply alignment. + if (mItemPlacement == "end") { + x += directionLine.x == 0 ? (maxItemSize.x - size.x) : 0; + y += directionLine.y == 0 ? (maxItemSize.y - size.y) : 0; + } + else if (mItemPlacement == "center") { + x += directionLine.x == 0 ? (maxItemSize.x - size.x) / 2 : 0; + y += directionLine.y == 0 ? (maxItemSize.y - size.y) / 2 : 0; + } + else if (mItemPlacement == "stretch" && mDirection == "row") { + image.second.setSize(image.second.getSize().x, maxItemSize.y); + } + + // TODO: Doesn't work correctly. + // Apply overall container alignment. + if (mAlignment == "right") + x += (mSize.x - size.x * grid.x) - mItemMargin.x; + + // Store final item position. + image.second.setPosition(x, y); + + // Translate anchor. + if ((i++ + 1) % std::max(1, static_cast(mItemsPerLine)) != 0) { + // Translate on same line. + anchorX += (size.x + mItemMargin.x) * static_cast(directionLine.x); + anchorY += (size.y + mItemMargin.y) * static_cast(directionLine.y); + } + else { + // Translate to first position of next line. + if (directionRow.x == 0) { + anchorY += size.y + mItemMargin.y; + anchorX = anchorXStart; + } + else { + anchorX += size.x + mItemMargin.x; + anchorY = anchorYStart; + } + } + } + + mLayoutValid = true; +} + +void FlexboxComponent::render(const glm::mat4& parentTrans) +{ + if (!isVisible()) + return; + + if (!mLayoutValid) + computeLayout(); + + glm::mat4 trans{parentTrans * getTransform()}; + Renderer::setMatrix(trans); + + if (Settings::getInstance()->getBool("DebugImage")) + Renderer::drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0xFF000033, 0xFF000033); + + for (auto& image : mImages) { + if (mOpacity == 255) { + image.second.render(trans); + } + else { + image.second.setOpacity(mOpacity); + image.second.render(trans); + image.second.setOpacity(255); + } + } +} diff --git a/es-core/src/components/FlexboxComponent.h b/es-core/src/components/FlexboxComponent.h new file mode 100644 index 000000000..bbe385aba --- /dev/null +++ b/es-core/src/components/FlexboxComponent.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// FlexboxComponent.h +// +// Flexbox layout component. +// + +#ifndef ES_CORE_COMPONENTS_FLEXBOX_COMPONENT_H +#define ES_CORE_COMPONENTS_FLEXBOX_COMPONENT_H + +#include "GuiComponent.h" +#include "components/ImageComponent.h" + +class FlexboxComponent : public GuiComponent +{ +public: + explicit FlexboxComponent(Window* window, + std::vector>& images); + + // Getters/setters for the layout. + void setDirection(const std::string& direction) + { + assert(direction == "row" || direction == "column"); + mDirection = direction; + } + + std::string getAlignment() const { return mAlignment; } + void setAlignment(const std::string& value) + { + assert(value == "left" || value == "right"); + mAlignment = value; + mLayoutValid = false; + } + + unsigned int getItemsPerLine() const { return mItemsPerLine; } + void setItemsPerLine(unsigned int value) + { + mItemsPerLine = value; + mLayoutValid = false; + } + + unsigned int getLines() const { return mLines; } + void setLines(unsigned int value) + { + mLines = value; + mLayoutValid = false; + } + + std::string getItemPlacement() const { return mItemPlacement; } + void setItemPlacement(const std::string& value) + { + assert(value == "start" || value == "center" || value == "end" || value == "stretch"); + mItemPlacement = value; + mLayoutValid = false; + } + + glm::vec2 getItemMargin() const { return mItemMargin; } + void setItemMargin(glm::vec2 value) + { + mItemMargin.x = std::roundf(value.x * Renderer::getScreenWidth()); + mItemMargin.y = std::roundf(value.y * Renderer::getScreenHeight()); + mLayoutValid = false; + } + + void onSizeChanged() override { mLayoutValid = false; } + void render(const glm::mat4& parentTrans) override; + +private: + // Calculate flexbox layout. + void computeLayout(); + + std::vector>& mImages; + + // Layout options. + std::string mDirection; + std::string mAlignment; + unsigned int mItemsPerLine; + unsigned int mLines; + std::string mItemPlacement; + glm::vec2 mItemMargin; + + bool mLayoutValid; +}; + +#endif // ES_CORE_COMPONENTS_FLEXBOX_COMPONENT_H diff --git a/resources/graphics/badge_altemulator.svg b/resources/graphics/badge_altemulator.svg new file mode 100644 index 000000000..0a07c9872 --- /dev/null +++ b/resources/graphics/badge_altemulator.svg @@ -0,0 +1,431 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/graphics/badge_broken.svg b/resources/graphics/badge_broken.svg new file mode 100644 index 000000000..a1d7dbc3c --- /dev/null +++ b/resources/graphics/badge_broken.svg @@ -0,0 +1,246 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/graphics/badge_completed.svg b/resources/graphics/badge_completed.svg new file mode 100644 index 000000000..a09e55f4c --- /dev/null +++ b/resources/graphics/badge_completed.svg @@ -0,0 +1,255 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/graphics/badge_favorite.svg b/resources/graphics/badge_favorite.svg new file mode 100644 index 000000000..4b8236cae --- /dev/null +++ b/resources/graphics/badge_favorite.svg @@ -0,0 +1,247 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/graphics/badge_kidgame.svg b/resources/graphics/badge_kidgame.svg new file mode 100644 index 000000000..565806a74 --- /dev/null +++ b/resources/graphics/badge_kidgame.svg @@ -0,0 +1,250 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/themes/rbsimple-DE/theme.xml b/themes/rbsimple-DE/theme.xml index 137af1b89..2f20ddca6 100644 --- a/themes/rbsimple-DE/theme.xml +++ b/themes/rbsimple-DE/theme.xml @@ -236,6 +236,16 @@ based on: 'recalbox-multi' by the Recalbox community 0.873 0.212 right + + 0.8125 0.675 + 0.15 0.21 + 0 0 + left + 3 + 2 + 0.0028125 0.005 + favorite completed kidgame broken altemulator +