Migrated the carousel code from SystemView to CarouselComponent.

This commit is contained in:
Leon Styhre 2022-02-06 14:01:40 +01:00
parent d564a234c1
commit b5d49e9b43
5 changed files with 756 additions and 676 deletions

View file

@ -11,7 +11,6 @@
#include "Log.h"
#include "Settings.h"
#include "Sound.h"
#include "SystemData.h"
#include "UIModeController.h"
#include "Window.h"
#include "animations/LambdaAnimation.h"
@ -31,211 +30,120 @@ namespace
} // namespace
SystemView::SystemView()
: IList<SystemViewData, SystemData*> {LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP}
, mSystemInfo {"SYSTEM INFO", Font::get(FONT_SIZE_SMALL), 0x33333300, ALIGN_CENTER}
, mPreviousScrollVelocity {0}
: mCamOffset {0.0f}
, mFadeOpacity {0.0f}
, mUpdatedGameCount {false}
, mViewNeedsReload {true}
, mLegacyMode {false}
{
mCamOffset = 0;
mExtrasCamOffset = 0;
mExtrasFadeOpacity = 0.0f;
mCarousel = std::make_unique<CarouselComponent>();
mSystemInfo = std::make_unique<TextComponent>("SYSTEM INFO", Font::get(FONT_SIZE_SMALL),
0x33333300, ALIGN_CENTER);
setSize(static_cast<float>(Renderer::getScreenWidth()),
static_cast<float>(Renderer::getScreenHeight()));
populate();
mCarousel->setCursorChangedCallback([&](const CursorState& state) { onCursorChanged(state); });
mCarousel->setCancelTransitionsCallback(
[&] { ViewController::getInstance()->cancelViewTransitions(); });
}
SystemView::~SystemView()
{
// TEMPORARY
// if (mLegacyMode) {
// Delete any existing extras.
for (auto entry : mEntries) {
for (auto extra : entry.data.backgroundExtras)
delete extra;
entry.data.backgroundExtras.clear();
if (mLegacyMode) {
// Delete any existing extras.
for (auto& entry : mElements) {
for (auto extra : entry.legacyExtras)
delete extra;
entry.legacyExtras.clear();
}
}
// }
}
void SystemView::populate()
{
mEntries.clear();
auto themeSets = ThemeData::getThemeSets();
std::map<std::string, ThemeData::ThemeSet>::const_iterator selectedSet =
themeSets.find(Settings::getInstance()->getString("ThemeSet"));
assert(selectedSet != themeSets.cend());
mLegacyMode = selectedSet->second.capabilities.legacyTheme;
for (auto it : SystemData::sSystemVector) {
const std::shared_ptr<ThemeData>& theme = it->getTheme();
mLegacyMode = theme->isLegacyTheme();
const std::shared_ptr<ThemeData>& theme {it->getTheme()};
if (mViewNeedsReload)
getViewElements(theme);
Entry e;
e.name = it->getName();
e.object = it;
// Component offset. Used for positioning placeholders.
glm::vec3 offsetLogo {0.0f, 0.0f, 0.0f};
glm::vec3 offsetLogoPlaceholderText {0.0f, 0.0f, 0.0f};
// Make logo.
const ThemeData::ThemeElement* logoElem {
theme->getElement("system", "image_logo", "image")};
if (logoElem) {
std::string path;
if (logoElem->has("path"))
path = logoElem->get<std::string>("path");
std::string defaultPath {
logoElem->has("default") ? logoElem->get<std::string>("default") : ""};
if ((!path.empty() && ResourceManager::getInstance().fileExists(path)) ||
(!defaultPath.empty() && ResourceManager::getInstance().fileExists(defaultPath))) {
auto* logo = new ImageComponent(false, false);
logo->setMaxSize(glm::round(mCarousel.logoSize * mCarousel.logoScale));
logo->applyTheme(theme, "system", "image_logo",
ThemeFlags::PATH | ThemeFlags::COLOR);
logo->setRotateByTargetSize(true);
e.data.logo = std::shared_ptr<GuiComponent>(logo);
}
}
// No logo available? Make placeholder.
if (!e.data.logo) {
glm::vec2 resolution {static_cast<float>(Renderer::getScreenWidth()),
static_cast<float>(Renderer::getScreenHeight())};
glm::vec3 center {resolution.x / 2.0f, resolution.y / 2.0f, 1.0f};
// Placeholder Image.
logoElem = theme->getElement("system", "image_logoPlaceholderImage", "image");
if (logoElem) {
auto path = logoElem->get<std::string>("path");
std::string defaultPath =
logoElem->has("default") ? logoElem->get<std::string>("default") : "";
if ((!path.empty() && ResourceManager::getInstance().fileExists(path)) ||
(!defaultPath.empty() &&
ResourceManager::getInstance().fileExists(defaultPath))) {
auto* logo = new ImageComponent(false, false);
logo->applyTheme(theme, "system", "image_logoPlaceholderImage",
ThemeFlags::ALL);
if (!logoElem->has("size"))
logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale);
offsetLogo = logo->getPosition() - center;
logo->setRotateByTargetSize(true);
e.data.logo = std::shared_ptr<GuiComponent>(logo);
}
}
// Placeholder Text.
const ThemeData::ThemeElement* logoPlaceholderText =
theme->getElement("system", "text_logoPlaceholderText", "text");
if (logoPlaceholderText) {
// Element 'logoPlaceholderText' found in theme: place text
auto* text = new TextComponent(it->getName(), Font::get(FONT_SIZE_LARGE),
0x000000FF, ALIGN_CENTER);
text->setSize(mCarousel.logoSize * mCarousel.logoScale);
if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) {
text->setHorizontalAlignment(mCarousel.logoAlignment);
text->setVerticalAlignment(ALIGN_CENTER);
}
else {
text->setHorizontalAlignment(ALIGN_CENTER);
text->setVerticalAlignment(mCarousel.logoAlignment);
}
text->applyTheme(it->getTheme(), "system", "text_logoPlaceholderText",
ThemeFlags::ALL);
if (!e.data.logo) {
e.data.logo = std::shared_ptr<GuiComponent>(text);
offsetLogo = text->getPosition() - center;
// text->setVisible(true);
}
else {
e.data.logoPlaceholderText = std::shared_ptr<GuiComponent>(text);
offsetLogoPlaceholderText = text->getPosition() - center;
}
}
else {
// Fallback to legacy centered placeholder text.
auto* text = new TextComponent(it->getName(), Font::get(FONT_SIZE_LARGE),
0x000000FF, ALIGN_CENTER);
text->setSize(mCarousel.logoSize * mCarousel.logoScale);
text->applyTheme(it->getTheme(), "system", "logoText",
ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR |
ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING |
ThemeFlags::TEXT);
e.data.logo = std::shared_ptr<GuiComponent>(text);
if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) {
text->setHorizontalAlignment(mCarousel.logoAlignment);
text->setVerticalAlignment(ALIGN_CENTER);
}
else {
text->setHorizontalAlignment(ALIGN_CENTER);
text->setVerticalAlignment(mCarousel.logoAlignment);
}
}
}
if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) {
if (mCarousel.logoAlignment == ALIGN_LEFT)
e.data.logo->setOrigin(0, 0.5);
else if (mCarousel.logoAlignment == ALIGN_RIGHT)
e.data.logo->setOrigin(1.0, 0.5);
else
e.data.logo->setOrigin(0.5, 0.5);
}
else {
if (mCarousel.logoAlignment == ALIGN_TOP)
e.data.logo->setOrigin(0.5, 0);
else if (mCarousel.logoAlignment == ALIGN_BOTTOM)
e.data.logo->setOrigin(0.5, 1);
else
e.data.logo->setOrigin(0.5, 0.5);
}
glm::vec2 denormalized {mCarousel.logoSize * e.data.logo->getOrigin()};
glm::vec3 v = {denormalized.x, denormalized.y, 0.0f};
e.data.logo->setPosition(v + offsetLogo);
if (e.data.logoPlaceholderText)
e.data.logoPlaceholderText->setPosition(v + offsetLogoPlaceholderText);
if (mLegacyMode) {
// Make background extras.
e.data.backgroundExtras = ThemeData::makeExtras(it->getTheme(), "system");
SystemViewElements elements;
elements.name = it->getName();
elements.legacyExtras = ThemeData::makeExtras(theme, "system");
// Sort the extras by z-index.
std::stable_sort(
e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(),
elements.legacyExtras.begin(), elements.legacyExtras.end(),
[](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); });
mElements.emplace_back(std::move(elements));
}
else {
if (!mLegacyMode) {
SystemViewElements elements;
if (theme->hasView("system")) {
elements.name = it->getName();
for (auto& element : theme->getViewElements("system").elements) {
if (element.second.type == "image") {
e.data.imageComponents.push_back(std::make_shared<ImageComponent>());
e.data.imageComponents.back()->setDefaultZIndex(30.0f);
e.data.imageComponents.back()->applyTheme(theme, "system", element.first,
ThemeFlags::ALL);
if (e.data.imageComponents.back()->getMetadataField() != "")
e.data.imageComponents.back()->setScrollHide(true);
elements.imageComponents.emplace_back(std::make_unique<ImageComponent>());
elements.imageComponents.back()->setDefaultZIndex(30.0f);
elements.imageComponents.back()->applyTheme(theme, "system", element.first,
ThemeFlags::ALL);
if (elements.imageComponents.back()->getMetadataField() != "")
elements.imageComponents.back()->setScrollHide(true);
elements.children.emplace_back(elements.imageComponents.back().get());
}
else if (element.second.type == "text") {
e.data.textComponents.push_back(std::make_unique<TextComponent>());
e.data.textComponents.back()->setDefaultZIndex(40.0f);
e.data.textComponents.back()->applyTheme(theme, "system", element.first,
ThemeFlags::ALL);
if (e.data.textComponents.back()->getMetadataField() != "")
e.data.textComponents.back()->setScrollHide(true);
elements.textComponents.push_back(std::make_unique<TextComponent>());
elements.textComponents.back()->setDefaultZIndex(40.0f);
elements.textComponents.back()->applyTheme(theme, "system", element.first,
ThemeFlags::ALL);
if (elements.textComponents.back()->getMetadataField() != "")
elements.textComponents.back()->setScrollHide(true);
elements.children.emplace_back(elements.textComponents.back().get());
}
}
}
elements.children.emplace_back(mCarousel.get());
elements.children.emplace_back(mSystemInfo.get());
std::stable_sort(
elements.children.begin(), elements.children.end(),
[](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); });
std::stable_sort(elements.imageComponents.begin(), elements.imageComponents.end(),
[](const std::unique_ptr<ImageComponent>& a,
const std::unique_ptr<ImageComponent>& b) {
return b->getZIndex() > a->getZIndex();
});
std::stable_sort(elements.textComponents.begin(), elements.textComponents.end(),
[](const std::unique_ptr<TextComponent>& a,
const std::unique_ptr<TextComponent>& b) {
return b->getZIndex() > a->getZIndex();
});
mElements.emplace_back(std::move(elements));
}
this->add(e);
CarouselComponent::Entry entry;
entry.name = it->getName();
entry.object = it;
mCarousel->addEntry(theme, entry);
}
if (mEntries.empty()) {
if (mCarousel->getNumEntries() == 0) {
// Something is wrong, there is not a single system to show, check if UI mode is not full.
if (!UIModeController::getInstance()->isUIModeFull()) {
Settings::getInstance()->setString("UIMode", "full");
@ -249,32 +157,35 @@ void SystemView::populate()
void SystemView::updateGameCount()
{
std::pair<unsigned int, unsigned int> gameCount = getSelected()->getDisplayedGameCount();
std::pair<unsigned int, unsigned int> gameCount =
mCarousel->getSelected()->getDisplayedGameCount();
std::stringstream ss;
if (!getSelected()->isGameSystem())
if (!mCarousel->getSelected()->isGameSystem())
ss << "CONFIGURATION";
else if (getSelected()->isCollection() && (getSelected()->getName() == "favorites"))
else if (mCarousel->getSelected()->isCollection() &&
(mCarousel->getSelected()->getName() == "favorites"))
ss << gameCount.first << " GAME" << (gameCount.first == 1 ? " " : "S");
// The "recent" gamelist has probably been trimmed after sorting, so we'll cap it at
// its maximum limit of 50 games.
else if (getSelected()->isCollection() && (getSelected()->getName() == "recent"))
else if (mCarousel->getSelected()->isCollection() &&
(mCarousel->getSelected()->getName() == "recent"))
ss << (gameCount.first > 50 ? 50 : gameCount.first) << " GAME"
<< (gameCount.first == 1 ? " " : "S");
else
ss << gameCount.first << " GAME" << (gameCount.first == 1 ? " " : "S ") << "("
<< gameCount.second << " FAVORITE" << (gameCount.second == 1 ? ")" : "S)");
mSystemInfo.setText(ss.str());
mSystemInfo->setText(ss.str());
}
void SystemView::goToSystem(SystemData* system, bool animate)
{
setCursor(system);
mCarousel->setCursor(system);
updateGameCount();
if (!animate)
finishAnimation(0);
finishSystemAnimation(0);
}
bool SystemView::input(InputConfig* config, Input input)
@ -287,39 +198,9 @@ bool SystemView::input(InputConfig* config, Input input)
return true;
}
switch (mCarousel.type) {
case VERTICAL:
case VERTICAL_WHEEL:
if (config->isMappedLike("up", input)) {
ViewController::getInstance()->cancelViewTransitions();
listInput(-1);
return true;
}
if (config->isMappedLike("down", input)) {
ViewController::getInstance()->cancelViewTransitions();
listInput(1);
return true;
}
break;
case HORIZONTAL:
case HORIZONTAL_WHEEL:
default:
if (config->isMappedLike("left", input)) {
ViewController::getInstance()->cancelViewTransitions();
listInput(-1);
return true;
}
if (config->isMappedLike("right", input)) {
ViewController::getInstance()->cancelViewTransitions();
listInput(1);
return true;
}
break;
}
if (config->isMappedTo("a", input)) {
stopScrolling();
ViewController::getInstance()->goToGamelist(getSelected());
mCarousel->stopScrolling();
ViewController::getInstance()->goToGamelist(mCarousel->getSelected());
NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND);
return true;
}
@ -328,7 +209,7 @@ bool SystemView::input(InputConfig* config, Input input)
config->isMappedTo("rightthumbstickclick", input))) {
// Get a random system and jump to it.
NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND);
setCursor(SystemData::getRandomSystem(getSelected()));
mCarousel->setCursor(SystemData::getRandomSystem(mCarousel->getSelected()));
return true;
}
@ -343,18 +224,13 @@ bool SystemView::input(InputConfig* config, Input input)
return true;
}
}
else {
if (config->isMappedLike("left", input) || config->isMappedLike("right", input) ||
config->isMappedLike("up", input) || config->isMappedLike("down", input))
listInput(0);
}
return GuiComponent::input(config, input);
return mCarousel->input(config, input);
}
void SystemView::update(int deltaTime)
{
listUpdate(deltaTime);
mCarousel->update(deltaTime);
GuiComponent::update(deltaTime);
}
@ -363,52 +239,29 @@ void SystemView::onCursorChanged(const CursorState& /*state*/)
// Update help style.
updateHelpPrompts();
float startPos = mCamOffset;
float posMax = static_cast<float>(mEntries.size());
float target = static_cast<float>(mCursor);
int scrollVelocity {mCarousel->getScrollingVelocity()};
float startPos {mCamOffset};
float posMax {static_cast<float>(mCarousel->getNumEntries())};
float target {static_cast<float>(mCarousel->getCursor())};
// Find the shortest path to the target.
float endPos = target; // Directly.
float dist = fabs(endPos - startPos);
float endPos {target}; // Directly.
float dist {fabs(endPos - startPos)};
if (fabs(target + posMax - startPos - mScrollVelocity) < dist)
if (fabs(target + posMax - startPos - scrollVelocity) < dist)
endPos = target + posMax; // Loop around the end (0 -> max).
if (fabs(target - posMax - startPos - mScrollVelocity) < dist)
if (fabs(target - posMax - startPos - scrollVelocity) < dist)
endPos = target - posMax; // Loop around the start (max - 1 -> -1).
// This logic is only needed when there are two game systems, to prevent ugly jumps back
// an forth when selecting the same direction rapidly several times in a row.
if (posMax == 2) {
if (mPreviousScrollVelocity == 0)
mPreviousScrollVelocity = mScrollVelocity;
else if (mScrollVelocity < 0 && startPos < endPos)
mPreviousScrollVelocity = -1;
else if (mScrollVelocity > 0 && startPos > endPos)
mPreviousScrollVelocity = 1;
}
std::string transition_style = Settings::getInstance()->getString("TransitionStyle");
// To prevent ugly jumps with two systems when quickly repeating the same direction.
if (mPreviousScrollVelocity != 0 && posMax == 2 && mScrollVelocity == mPreviousScrollVelocity) {
if (fabs(endPos - startPos) < 0.5 || fabs(endPos - startPos) > 1.5) {
(mScrollVelocity < 0) ? endPos -= 1 : endPos += 1;
(mCursor == 0) ? mCursor = 1 : mCursor = 0;
updateGameCount();
return;
}
}
// No need to animate transition, we're not going anywhere (probably mEntries.size() == 1).
if (endPos == mCamOffset && endPos == mExtrasCamOffset)
return;
std::string transition_style {Settings::getInstance()->getString("TransitionStyle")};
Animation* anim;
if (transition_style == "fade") {
float startExtrasFade = mExtrasFadeOpacity;
float startFade {mFadeOpacity};
anim = new LambdaAnimation(
[this, startExtrasFade, startPos, endPos, posMax](float t) {
[this, startFade, startPos, endPos, posMax](float t) {
t -= 1;
float f = glm::mix(startPos, endPos, t * t * t + 1);
if (f < 0)
@ -416,23 +269,21 @@ void SystemView::onCursorChanged(const CursorState& /*state*/)
if (f >= posMax)
f -= posMax;
this->mCamOffset = f;
t += 1;
if (t < 0.3f)
this->mExtrasFadeOpacity =
glm::mix(0.0f, 1.0f, glm::clamp(t / 0.2f + startExtrasFade, 0.0f, 1.0f));
this->mFadeOpacity =
glm::mix(0.0f, 1.0f, glm::clamp(t / 0.2f + startFade, 0.0f, 1.0f));
else if (t < 0.7f)
this->mExtrasFadeOpacity = 1.0f;
this->mFadeOpacity = 1.0f;
else
this->mExtrasFadeOpacity =
this->mFadeOpacity =
glm::mix(1.0f, 0.0f, glm::clamp((t - 0.6f) / 0.3f, 0.0f, 1.0f));
if (t > 0.5f)
this->mExtrasCamOffset = endPos;
this->mCamOffset = endPos;
// Update the game count when the entire animation has been completed.
if (mExtrasFadeOpacity == 1.0f)
if (mFadeOpacity == 1.0f)
updateGameCount();
},
500);
@ -449,10 +300,9 @@ void SystemView::onCursorChanged(const CursorState& /*state*/)
f -= posMax;
this->mCamOffset = f;
this->mExtrasCamOffset = f;
// Hack to make the game count being updated in the middle of the animation.
bool update = false;
bool update {false};
if (endPos == -1.0f && fabs(fabs(posMax) - fabs(mCamOffset)) > 0.5f &&
!mUpdatedGameCount) {
update = true;
@ -484,8 +334,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/)
if (f >= posMax)
f -= posMax;
this->mCamOffset = f;
this->mExtrasCamOffset = endPos;
this->mCamOffset = endPos;
},
500);
}
@ -495,46 +344,80 @@ void SystemView::onCursorChanged(const CursorState& /*state*/)
void SystemView::render(const glm::mat4& parentTrans)
{
if (size() == 0)
if (mCarousel->getNumEntries() == 0)
return; // Nothing to render.
glm::mat4 trans {getTransform() * parentTrans};
if (mCarousel.legacyZIndexMode) {
// Render all extras.
renderExtras(trans, INT16_MIN, INT16_MAX);
// Adding texture loading buffers depending on scrolling speed and status.
int bufferIndex {mCarousel->getScrollingVelocity() + 1};
// Fade the screen if we're using fade transitions and we're currently transitioning.
// This basically renders a black rectangle on top of the currently visible extras
// (and beneath the carousel and help prompts).
if (mExtrasFadeOpacity)
renderFade(trans);
Renderer::pushClipRect(glm::ivec2 {},
glm::ivec2 {static_cast<int>(mSize.x), static_cast<int>(mSize.y)});
// Always render the carousel on top so that it's not faded.
renderCarousel(trans);
for (int i = static_cast<int>(mCamOffset) + logoBuffersLeft[bufferIndex];
i <= static_cast<int>(mCamOffset) + logoBuffersRight[bufferIndex]; ++i) {
int index {i};
while (index < 0)
index += static_cast<int>(mCarousel->getNumEntries());
while (index >= static_cast<int>(mCarousel->getNumEntries()))
index -= static_cast<int>(mCarousel->getNumEntries());
if (mCarousel->isAnimationPlaying(0) || index == mCarousel->getCursor()) {
glm::mat4 elementTrans {trans};
if (mCarousel->getType() == CarouselComponent::HORIZONTAL ||
mCarousel->getType() == CarouselComponent::HORIZONTAL_WHEEL)
elementTrans = glm::translate(elementTrans,
glm::vec3 {(i - mCamOffset) * mSize.x, 0.0f, 0.0f});
else
elementTrans = glm::translate(elementTrans,
glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f});
Renderer::pushClipRect(
glm::ivec2 {static_cast<int>(elementTrans[3].x),
static_cast<int>(elementTrans[3].y)},
glm::ivec2 {static_cast<int>(mSize.x), static_cast<int>(mSize.y)});
if (mLegacyMode && mElements.size() > static_cast<size_t>(index)) {
for (auto element : mElements[index].legacyExtras)
element->render(elementTrans);
}
else if (mElements.size() > static_cast<size_t>(index)) {
for (auto child : mElements[index].children) {
if (child == mCarousel.get()) {
// Render black above anything lower than the zIndex of the carousel
// if fade transitions are in use and we're transitioning.
if (mFadeOpacity)
renderFade(trans);
child->render(trans);
}
else {
child->render(elementTrans);
}
}
}
if (mLegacyMode)
mSystemInfo->render(elementTrans);
Renderer::popClipRect();
}
}
else {
// Render the extras that are below the carousel.
renderExtras(trans, INT16_MIN, mCarousel.zIndex);
// Fade the screen if we're using fade transitions and we're currently transitioning.
// This basically renders a black rectangle on top of the currently visible extras
// (and beneath the carousel and help prompts).
if (mExtrasFadeOpacity)
if (mLegacyMode) {
if (mFadeOpacity)
renderFade(trans);
// Render the carousel.
renderCarousel(trans);
// Render the rest of the extras.
renderExtras(trans, mCarousel.zIndex, INT16_MAX);
mCarousel->render(trans);
}
Renderer::popClipRect();
}
std::vector<HelpPrompt> SystemView::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL)
if (mCarousel->getType() == CarouselComponent::VERTICAL ||
mCarousel->getType() == CarouselComponent::VERTICAL_WHEEL)
prompts.push_back(HelpPrompt("up/down", "choose"));
else
prompts.push_back(HelpPrompt("left/right", "choose"));
@ -554,7 +437,7 @@ std::vector<HelpPrompt> SystemView::getHelpPrompts()
HelpStyle SystemView::getHelpStyle()
{
HelpStyle style;
style.applyTheme(mEntries.at(mCursor).object->getTheme(), "system");
style.applyTheme(mCarousel->getEntry(mCarousel->getCursor()).object->getTheme(), "system");
return style;
}
@ -569,323 +452,33 @@ void SystemView::getViewElements(const std::shared_ptr<ThemeData>& theme)
{
LOG(LogDebug) << "SystemView::getViewElements()";
getDefaultElements();
if (theme->hasView("system"))
mViewNeedsReload = false;
else
mViewNeedsReload = true;
if (!theme->hasView("system"))
return;
mCarousel->applyTheme(theme, "system", "carousel_systemcarousel", ThemeFlags::ALL);
const ThemeData::ThemeElement* carouselElem =
theme->getElement("system", "carousel_systemcarousel", "carousel");
// System info bar.
mSystemInfo->setSize(mSize.x, mSystemInfo->getFont()->getLetterHeight() * 2.2f);
mSystemInfo->setPosition(0.0f, mCarousel->getPosition().y + mCarousel->getSize().y);
mSystemInfo->setBackgroundColor(0xDDDDDDD8);
mSystemInfo->setRenderBackground(true);
mSystemInfo->setFont(Font::get(static_cast<int>(0.035f * mSize.y), Font::getDefaultPath()));
mSystemInfo->setColor(0x000000FF);
mSystemInfo->setZIndex(49.0f);
mSystemInfo->setDefaultZIndex(49.0f);
if (carouselElem)
getCarouselFromTheme(carouselElem);
const ThemeData::ThemeElement* sysInfoElem =
theme->getElement("system", "text_systemInfo", "text");
const ThemeData::ThemeElement* sysInfoElem {
theme->getElement("system", "text_systemInfo", "text")};
if (sysInfoElem)
mSystemInfo.applyTheme(theme, "system", "text_systemInfo", ThemeFlags::ALL);
mViewNeedsReload = false;
}
void SystemView::renderCarousel(const glm::mat4& trans)
{
// Background box behind logos.
glm::mat4 carouselTrans {trans};
carouselTrans =
glm::translate(carouselTrans, glm::vec3 {mCarousel.pos.x, mCarousel.pos.y, 0.0f});
carouselTrans = glm::translate(carouselTrans,
glm::vec3 {mCarousel.origin.x * mCarousel.size.x * -1.0f,
mCarousel.origin.y * mCarousel.size.y * -1.0f, 0.0f});
glm::vec2 clipPos {carouselTrans[3].x, carouselTrans[3].y};
Renderer::pushClipRect(glm::ivec2 {static_cast<int>(std::round(clipPos.x)),
static_cast<int>(std::round(clipPos.y))},
glm::ivec2 {static_cast<int>(std::round(mCarousel.size.x)),
static_cast<int>(std::round(mCarousel.size.y))});
Renderer::setMatrix(carouselTrans);
Renderer::drawRect(0.0f, 0.0f, mCarousel.size.x, mCarousel.size.y, mCarousel.color,
mCarousel.colorEnd, mCarousel.colorGradientHorizontal);
// Draw logos.
// Note: logoSpacing will also include the size of the logo itself.
glm::vec2 logoSpacing {};
float xOff = 0.0f;
float yOff = 0.0f;
switch (mCarousel.type) {
case VERTICAL_WHEEL: {
yOff = (mCarousel.size.y - mCarousel.logoSize.y) / 2.0f - (mCamOffset * logoSpacing.y);
if (mCarousel.logoAlignment == ALIGN_LEFT)
xOff = mCarousel.logoSize.x / 10.0f;
else if (mCarousel.logoAlignment == ALIGN_RIGHT)
xOff = mCarousel.size.x - (mCarousel.logoSize.x * 1.1f);
else
xOff = (mCarousel.size.x - mCarousel.logoSize.x) / 2.0f;
break;
}
case VERTICAL: {
logoSpacing.y = ((mCarousel.size.y - (mCarousel.logoSize.y * mCarousel.maxLogoCount)) /
(mCarousel.maxLogoCount)) +
mCarousel.logoSize.y;
yOff = (mCarousel.size.y - mCarousel.logoSize.y) / 2.0f - (mCamOffset * logoSpacing.y);
if (mCarousel.logoAlignment == ALIGN_LEFT)
xOff = mCarousel.logoSize.x / 10.0f;
else if (mCarousel.logoAlignment == ALIGN_RIGHT)
xOff = mCarousel.size.x - (mCarousel.logoSize.x * 1.1f);
else
xOff = (mCarousel.size.x - mCarousel.logoSize.x) / 2.0f;
break;
}
case HORIZONTAL_WHEEL: {
xOff = std::round((mCarousel.size.x - mCarousel.logoSize.x) / 2.0f -
(mCamOffset * logoSpacing.y));
if (mCarousel.logoAlignment == ALIGN_TOP)
yOff = mCarousel.logoSize.y / 10.0f;
else if (mCarousel.logoAlignment == ALIGN_BOTTOM)
yOff = mCarousel.size.y - (mCarousel.logoSize.y * 1.1f);
else
yOff = (mCarousel.size.y - mCarousel.logoSize.y) / 2.0f;
break;
}
case HORIZONTAL: {
}
default: {
logoSpacing.x = ((mCarousel.size.x - (mCarousel.logoSize.x * mCarousel.maxLogoCount)) /
(mCarousel.maxLogoCount)) +
mCarousel.logoSize.x;
xOff = std::round((mCarousel.size.x - mCarousel.logoSize.x) / 2.0f -
(mCamOffset * logoSpacing.x));
if (mCarousel.logoAlignment == ALIGN_TOP)
yOff = mCarousel.logoSize.y / 10.0f;
else if (mCarousel.logoAlignment == ALIGN_BOTTOM)
yOff = mCarousel.size.y - (mCarousel.logoSize.y * 1.1f);
else
yOff = (mCarousel.size.y - mCarousel.logoSize.y) / 2.0f;
break;
}
}
int center = static_cast<int>(mCamOffset);
int logoCount = std::min(mCarousel.maxLogoCount, static_cast<int>(mEntries.size()));
// Adding texture loading buffers depending on scrolling speed and status.
int bufferIndex = getScrollingVelocity() + 1;
int bufferLeft = logoBuffersLeft[bufferIndex];
int bufferRight = logoBuffersRight[bufferIndex];
if (logoCount == 1) {
bufferLeft = 0;
bufferRight = 0;
}
for (int i = center - logoCount / 2 + bufferLeft; // Line break.
i <= center + logoCount / 2 + bufferRight; ++i) {
int index = i;
while (index < 0)
index += static_cast<int>(mEntries.size());
while (index >= static_cast<int>(mEntries.size()))
index -= static_cast<int>(mEntries.size());
glm::mat4 logoTrans {carouselTrans};
logoTrans = glm::translate(
logoTrans, glm::vec3 {i * logoSpacing.x + xOff, i * logoSpacing.y + yOff, 0.0f});
float distance = i - mCamOffset;
float scale = 1.0f + ((mCarousel.logoScale - 1.0f) * (1.0f - fabs(distance)));
scale = std::min(mCarousel.logoScale, std::max(1.0f, scale));
scale /= mCarousel.logoScale;
int opacity =
static_cast<int>(std::round(0x80 + ((0xFF - 0x80) * (1.0f - fabs(distance)))));
opacity = std::max(static_cast<int>(0x80), opacity);
const std::shared_ptr<GuiComponent>& comp = mEntries.at(index).data.logo;
if (mCarousel.type == VERTICAL_WHEEL || mCarousel.type == HORIZONTAL_WHEEL) {
comp->setRotationDegrees(mCarousel.logoRotation * distance);
comp->setRotationOrigin(mCarousel.logoRotationOrigin);
}
// When running at lower resolutions, prevent the scale-down to go all the way to the
// minimum value. This avoids potential single-pixel alignment issues when the logo
// can't be vertically placed exactly in the middle of the carousel. Although the
// problem theoretically exists at all resolutions, it's not visble at around 1080p
// and above.
if (std::min(mSize.x, mSize.y) < 1080.0f)
scale = glm::clamp(scale, 1.0f / mCarousel.logoScale + 0.01f, 1.0f);
comp->setScale(scale);
comp->setOpacity(static_cast<unsigned char>(opacity));
comp->render(logoTrans);
if (mEntries.at(index).data.logoPlaceholderText) {
const std::shared_ptr<GuiComponent>& comp = mEntries.at(index).data.logoPlaceholderText;
if (mCarousel.type == VERTICAL_WHEEL || mCarousel.type == HORIZONTAL_WHEEL) {
comp->setRotationDegrees(mCarousel.logoRotation * distance);
comp->setRotationOrigin(mCarousel.logoRotationOrigin);
}
comp->setScale(scale);
comp->setOpacity(static_cast<unsigned char>(opacity));
comp->render(logoTrans);
}
}
Renderer::popClipRect();
}
void SystemView::renderExtras(const glm::mat4& trans, float lower, float upper)
{
int extrasCenter = static_cast<int>(mExtrasCamOffset);
// Adding texture loading buffers depending on scrolling speed and status.
int bufferIndex {getScrollingVelocity() + 1};
Renderer::pushClipRect(glm::ivec2 {},
glm::ivec2 {static_cast<int>(mSize.x), static_cast<int>(mSize.y)});
for (int i = extrasCenter + logoBuffersLeft[bufferIndex];
i <= extrasCenter + logoBuffersRight[bufferIndex]; ++i) {
int index = i;
while (index < 0)
index += static_cast<int>(mEntries.size());
while (index >= static_cast<int>(mEntries.size()))
index -= static_cast<int>(mEntries.size());
// Only render selected system when not showing.
if (mShowing || index == mCursor) {
glm::mat4 extrasTrans {trans};
if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL)
extrasTrans = glm::translate(
extrasTrans, glm::vec3 {(i - mExtrasCamOffset) * mSize.x, 0.0f, 0.0f});
else
extrasTrans = glm::translate(
extrasTrans, glm::vec3 {0.0f, (i - mExtrasCamOffset) * mSize.y, 0.0f});
Renderer::pushClipRect(
glm::ivec2 {static_cast<int>(extrasTrans[3].x), static_cast<int>(extrasTrans[3].y)},
glm::ivec2 {static_cast<int>(mSize.x), static_cast<int>(mSize.y)});
SystemViewData data = mEntries.at(index).data;
// Quick hack to get the new theme engine to work without using extras.
for (auto& image : data.imageComponents) {
image->render(extrasTrans);
}
for (auto& text : data.textComponents) {
text->render(extrasTrans);
}
for (unsigned int j = 0; j < data.backgroundExtras.size(); ++j) {
GuiComponent* extra = data.backgroundExtras[j];
if (extra->getZIndex() >= lower && extra->getZIndex() < upper)
extra->render(extrasTrans);
}
mSystemInfo.render(extrasTrans);
Renderer::popClipRect();
}
}
Renderer::popClipRect();
mSystemInfo->applyTheme(theme, "system", "text_systemInfo", ThemeFlags::ALL);
}
void SystemView::renderFade(const glm::mat4& trans)
{
unsigned int fadeColor = 0x00000000 | static_cast<unsigned char>(mExtrasFadeOpacity * 255.0f);
unsigned int fadeColor {0x00000000 | static_cast<unsigned int>(mFadeOpacity * 255.0f)};
Renderer::setMatrix(trans);
Renderer::drawRect(0.0f, 0.0f, mSize.x, mSize.y, fadeColor, fadeColor);
}
void SystemView::getDefaultElements(void)
{
// Carousel.
mCarousel.type = HORIZONTAL;
mCarousel.logoAlignment = ALIGN_CENTER;
mCarousel.size.x = mSize.x;
mCarousel.size.y = floorf(0.2325f * mSize.y);
mCarousel.pos.x = 0.0f;
mCarousel.pos.y = floorf(0.5f * (mSize.y - mCarousel.size.y));
mCarousel.origin.x = 0.0f;
mCarousel.origin.y = 0.0f;
mCarousel.color = 0xFFFFFFD8;
mCarousel.colorEnd = 0xFFFFFFD8;
mCarousel.colorGradientHorizontal = true;
mCarousel.logoScale = 1.2f;
mCarousel.logoRotation = 7.5f;
mCarousel.logoRotationOrigin.x = -5.0f;
mCarousel.logoRotationOrigin.y = 0.5f;
mCarousel.logoSize.x = 0.25f * mSize.x;
mCarousel.logoSize.y = 0.155f * mSize.y;
mCarousel.maxLogoCount = 3;
mCarousel.zIndex = 40.0f;
mCarousel.legacyZIndexMode = true;
// System info bar.
mSystemInfo.setSize(mSize.x, mSystemInfo.getFont()->getLetterHeight() * 2.2f);
mSystemInfo.setPosition(0.0f, mCarousel.pos.y + mCarousel.size.y);
mSystemInfo.setBackgroundColor(0xDDDDDDD8);
mSystemInfo.setRenderBackground(true);
mSystemInfo.setFont(Font::get(static_cast<int>(0.035f * mSize.y), Font::getDefaultPath()));
mSystemInfo.setColor(0x000000FF);
mSystemInfo.setZIndex(50.0f);
mSystemInfo.setDefaultZIndex(50.0f);
}
void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem)
{
if (elem->has("type")) {
if (!(elem->get<std::string>("type").compare("vertical")))
mCarousel.type = VERTICAL;
else if (!(elem->get<std::string>("type").compare("vertical_wheel")))
mCarousel.type = VERTICAL_WHEEL;
else if (!(elem->get<std::string>("type").compare("horizontal_wheel")))
mCarousel.type = HORIZONTAL_WHEEL;
else
mCarousel.type = HORIZONTAL;
}
if (elem->has("size"))
mCarousel.size = elem->get<glm::vec2>("size") * mSize;
if (elem->has("pos"))
mCarousel.pos = elem->get<glm::vec2>("pos") * mSize;
if (elem->has("origin"))
mCarousel.origin = elem->get<glm::vec2>("origin");
if (elem->has("color")) {
mCarousel.color = elem->get<unsigned int>("color");
mCarousel.colorEnd = mCarousel.color;
}
if (elem->has("colorEnd"))
mCarousel.colorEnd = elem->get<unsigned int>("colorEnd");
if (elem->has("gradientType"))
mCarousel.colorGradientHorizontal =
!(elem->get<std::string>("gradientType").compare("horizontal"));
if (elem->has("logoScale"))
mCarousel.logoScale = elem->get<float>("logoScale");
if (elem->has("logoSize"))
mCarousel.logoSize = elem->get<glm::vec2>("logoSize") * mSize;
if (elem->has("maxLogoCount"))
mCarousel.maxLogoCount = static_cast<int>(std::round(elem->get<float>("maxLogoCount")));
if (elem->has("zIndex"))
mCarousel.zIndex = elem->get<float>("zIndex");
if (elem->has("logoRotation"))
mCarousel.logoRotation = elem->get<float>("logoRotation");
if (elem->has("logoRotationOrigin"))
mCarousel.logoRotationOrigin = elem->get<glm::vec2>("logoRotationOrigin");
if (elem->has("logoAlignment")) {
if (!(elem->get<std::string>("logoAlignment").compare("left")))
mCarousel.logoAlignment = ALIGN_LEFT;
else if (!(elem->get<std::string>("logoAlignment").compare("right")))
mCarousel.logoAlignment = ALIGN_RIGHT;
else if (!(elem->get<std::string>("logoAlignment").compare("top")))
mCarousel.logoAlignment = ALIGN_TOP;
else if (!(elem->get<std::string>("logoAlignment").compare("bottom")))
mCarousel.logoAlignment = ALIGN_BOTTOM;
else
mCarousel.logoAlignment = ALIGN_CENTER;
}
if (elem->has("legacyZIndexMode")) {
mCarousel.legacyZIndexMode =
elem->get<std::string>("legacyZIndexMode").compare("true") == 0 ? true : false;
}
else {
mCarousel.legacyZIndexMode = true;
}
}

View file

@ -11,11 +11,10 @@
#include "GuiComponent.h"
#include "Sound.h"
#include "components/BadgeComponent.h"
#include "SystemData.h"
#include "components/CarouselComponent.h"
#include "components/DateTimeComponent.h"
#include "components/IList.h"
#include "components/LottieComponent.h"
#include "components/RatingComponent.h"
#include "components/ScrollableContainer.h"
#include "components/TextComponent.h"
#include "components/TextListComponent.h"
@ -26,105 +25,72 @@
class SystemData;
enum CarouselType : unsigned int {
HORIZONTAL = 0,
VERTICAL = 1,
VERTICAL_WHEEL = 2,
HORIZONTAL_WHEEL = 3
struct SystemViewElements {
std::string name;
std::vector<GuiComponent*> legacyExtras;
std::vector<GuiComponent*> children;
std::vector<std::unique_ptr<TextComponent>> textComponents;
std::vector<std::unique_ptr<ImageComponent>> imageComponents;
std::vector<std::unique_ptr<VideoFFmpegComponent>> videoComponents;
std::vector<std::unique_ptr<LottieComponent>> lottieAnimComponents;
std::vector<std::unique_ptr<ScrollableContainer>> containerComponents;
std::vector<std::unique_ptr<TextComponent>> containerTextComponents;
};
struct SystemViewData {
std::shared_ptr<GuiComponent> logo;
std::shared_ptr<GuiComponent> logoPlaceholderText;
std::vector<GuiComponent*> backgroundExtras;
std::vector<std::shared_ptr<TextComponent>> textComponents;
std::vector<std::shared_ptr<DateTimeComponent>> dateTimeComponents;
std::vector<std::shared_ptr<ImageComponent>> imageComponents;
std::vector<std::shared_ptr<VideoFFmpegComponent>> videoComponents;
std::vector<std::shared_ptr<LottieComponent>> lottieAnimComponents;
std::vector<std::shared_ptr<BadgeComponent>> badgeComponents;
std::vector<std::shared_ptr<RatingComponent>> ratingComponents;
std::vector<std::shared_ptr<ScrollableContainer>> containerComponents;
std::vector<std::shared_ptr<TextComponent>> containerTextComponents;
std::vector<std::shared_ptr<TextComponent>> gamelistInfoComponents;
};
struct SystemViewCarousel {
CarouselType type;
glm::vec2 pos;
glm::vec2 size;
glm::vec2 origin;
float logoScale;
float logoRotation;
glm::vec2 logoRotationOrigin;
Alignment logoAlignment;
unsigned int color;
unsigned int colorEnd;
bool colorGradientHorizontal;
int maxLogoCount; // Number of logos shown on the carousel.
glm::vec2 logoSize;
float zIndex;
bool legacyZIndexMode;
};
class SystemView : public IList<SystemViewData, SystemData*>
class SystemView : public GuiComponent
{
public:
SystemView();
~SystemView();
void onShow() override { mShowing = true; }
void onHide() override { mShowing = false; }
void goToSystem(SystemData* system, bool animate);
bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override;
void render(const glm::mat4& parentTrans) override;
bool isScrolling() { return mCarousel->isScrolling(); }
void stopScrolling() { mCarousel->stopScrolling(); }
bool isSystemAnimationPlaying(unsigned char slot)
{
return mCarousel->isAnimationPlaying(slot);
}
void finishSystemAnimation(unsigned char slot)
{
finishAnimation(slot);
mCarousel->finishAnimation(slot);
}
CarouselComponent::CarouselType getCarouselType() { return mCarousel->getType(); }
SystemData* getFirstSystem() { return mCarousel->getFirst(); }
void onThemeChanged(const std::shared_ptr<ThemeData>& theme);
std::vector<HelpPrompt> getHelpPrompts() override;
HelpStyle getHelpStyle() override;
CarouselType getCarouselType() { return mCarousel.type; }
protected:
void onCursorChanged(const CursorState& state) override;
void onScroll() override
{
NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND);
}
void onCursorChanged(const CursorState& state);
private:
void populate();
void updateGameCount();
// Get the ThemeElements that make up the SystemView.
void getViewElements(const std::shared_ptr<ThemeData>& theme);
// Populate the system carousel with the legacy values.
void getDefaultElements(void);
void getCarouselFromTheme(const ThemeData::ThemeElement* elem);
// Render system carousel.
void renderCarousel(const glm::mat4& parentTrans);
// Draw background extras.
void renderExtras(const glm::mat4& parentTrans, float lower, float upper);
void renderFade(const glm::mat4& trans);
SystemViewCarousel mCarousel;
TextComponent mSystemInfo;
std::unique_ptr<CarouselComponent> mCarousel;
std::unique_ptr<TextComponent> mSystemInfo;
std::vector<SystemViewElements> mElements;
// Unit is list index.
float mCamOffset;
float mExtrasCamOffset;
float mExtrasFadeOpacity;
float mFadeOpacity;
int mPreviousScrollVelocity;
bool mUpdatedGameCount;
bool mViewNeedsReload;
bool mShowing;
bool mLegacyMode;
};

View file

@ -233,7 +233,7 @@ void ViewController::goToStart(bool playTransition)
Settings::getInstance()->setString("StartupSystem", "");
}
// Get the first system entry.
goToSystemView(getSystemListView()->getFirst(), false);
goToSystemView(getSystemListView()->getFirstSystem(), false);
}
void ViewController::ReloadAndGoToStart()
@ -293,8 +293,8 @@ void ViewController::stopScrolling()
mSystemListView->stopScrolling();
mCurrentView->stopListScrolling();
if (mSystemListView->isAnimationPlaying(0))
mSystemListView->finishAnimation(0);
if (mSystemListView->isSystemAnimationPlaying(0))
mSystemListView->finishSystemAnimation(0);
}
int ViewController::getSystemId(SystemData* system)
@ -351,16 +351,16 @@ void ViewController::goToSystemView(SystemData* system, bool playTransition)
if (applicationStartup) {
mCamera = glm::translate(mCamera, -mCurrentView->getPosition());
if (Settings::getInstance()->getString("TransitionStyle") == "slide") {
if (getSystemListView()->getCarouselType() == CarouselType::HORIZONTAL ||
getSystemListView()->getCarouselType() == CarouselType::HORIZONTAL_WHEEL)
if (getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL ||
getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL_WHEEL)
mCamera[3].y += static_cast<float>(Renderer::getScreenHeight());
else
mCamera[3].x -= static_cast<float>(Renderer::getScreenWidth());
updateHelpPrompts();
}
else if (Settings::getInstance()->getString("TransitionStyle") == "fade") {
if (getSystemListView()->getCarouselType() == CarouselType::HORIZONTAL ||
getSystemListView()->getCarouselType() == CarouselType::HORIZONTAL_WHEEL)
if (getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL ||
getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL_WHEEL)
mCamera[3].y += static_cast<float>(Renderer::getScreenHeight());
else
mCamera[3].x += static_cast<float>(Renderer::getScreenWidth());
@ -463,8 +463,8 @@ void ViewController::goToGamelist(SystemData* system)
// Stop any scrolling, animations and camera movements.
if (mState.viewing == SYSTEM_SELECT) {
mSystemListView->stopScrolling();
if (mSystemListView->isAnimationPlaying(0))
mSystemListView->finishAnimation(0);
if (mSystemListView->isSystemAnimationPlaying(0))
mSystemListView->finishSystemAnimation(0);
}
if (slideTransitions)
@ -825,8 +825,8 @@ bool ViewController::input(InputConfig* config, Input input)
mSystemListView->stopScrolling();
// Finish the animation too, so that it doesn't continue
// to play when we've closed the menu.
if (mSystemListView->isAnimationPlaying(0))
mSystemListView->finishAnimation(0);
if (mSystemListView->isSystemAnimationPlaying(0))
mSystemListView->finishSystemAnimation(0);
// Stop the gamelist scrolling as well as it would otherwise
// also continue to run after closing the menu.
mCurrentView->stopListScrolling();

View file

@ -8,7 +8,451 @@
#include "components/CarouselComponent.h"
CarouselComponent::CarouselComponent()
#include "Log.h"
#include "animations/LambdaAnimation.h"
namespace
{
// Buffer values for scrolling velocity (left, stopped, right).
const int logoBuffersLeft[] = {-5, -2, -1};
const int logoBuffersRight[] = {1, 2, 5};
} // namespace
CarouselComponent::CarouselComponent()
: IList<CarouselElement, SystemData*> {LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP}
, mCamOffset {0.0f}
, mPreviousScrollVelocity {0}
, mType {HORIZONTAL}
, mLogoAlignment {ALIGN_CENTER}
, mMaxLogoCount {3}
, mLogoSize {Renderer::getScreenWidth() * 0.25f, Renderer::getScreenHeight() * 0.155f}
, mLogoScale {1.2f}
, mLogoRotation {7.5f}
, mLogoRotationOrigin {-5.0f, 0.5f}
, mCarouselColor {0}
, mCarouselColorEnd {0}
, mColorGradientHorizontal {true}
{
//
}
void CarouselComponent::addEntry(const std::shared_ptr<ThemeData>& theme, Entry& entry)
{
// Make logo.
const ThemeData::ThemeElement* logoElem {theme->getElement("system", "image_logo", "image")};
if (logoElem) {
std::string path;
if (logoElem->has("path"))
path = logoElem->get<std::string>("path");
std::string defaultPath {logoElem->has("default") ? logoElem->get<std::string>("default") :
""};
if ((!path.empty() && ResourceManager::getInstance().fileExists(path)) ||
(!defaultPath.empty() && ResourceManager::getInstance().fileExists(defaultPath))) {
auto logo = std::make_shared<ImageComponent>(false, false);
logo->setMaxSize(glm::round(mLogoSize * mLogoScale));
logo->applyTheme(theme, "system", "image_logo", ThemeFlags::PATH | ThemeFlags::COLOR);
logo->setRotateByTargetSize(true);
entry.data.logo = logo;
}
}
if (!entry.data.logo) {
glm::vec2 resolution {static_cast<float>(Renderer::getScreenWidth()),
static_cast<float>(Renderer::getScreenHeight())};
glm::vec3 center {resolution.x / 2.0f, resolution.y / 2.0f, 1.0f};
// Placeholder Image.
logoElem = theme->getElement("system", "image_logoPlaceholderImage", "image");
if (logoElem) {
auto path = logoElem->get<std::string>("path");
std::string defaultPath {
logoElem->has("default") ? logoElem->get<std::string>("default") : ""};
if ((!path.empty() && ResourceManager::getInstance().fileExists(path)) ||
(!defaultPath.empty() && ResourceManager::getInstance().fileExists(defaultPath))) {
auto logo = std::make_shared<ImageComponent>(false, false);
logo->applyTheme(theme, "system", "image_logoPlaceholderImage", ThemeFlags::ALL);
if (!logoElem->has("size"))
logo->setMaxSize(mLogoSize * mLogoScale);
logo->setRotateByTargetSize(true);
entry.data.logo = logo;
}
}
// Placeholder Text.
const ThemeData::ThemeElement* logoPlaceholderText =
theme->getElement("system", "text_logoPlaceholderText", "text");
if (logoPlaceholderText) {
// Element 'logoPlaceholderText' found in theme configuration.
auto text = std::make_shared<TextComponent>(entry.name, Font::get(FONT_SIZE_LARGE),
0x000000FF, ALIGN_CENTER);
text->setSize(mLogoSize * mLogoScale);
if (mType == VERTICAL || mType == VERTICAL_WHEEL) {
text->setHorizontalAlignment(mLogoAlignment);
text->setVerticalAlignment(ALIGN_CENTER);
}
else {
text->setHorizontalAlignment(ALIGN_CENTER);
text->setVerticalAlignment(mLogoAlignment);
}
text->applyTheme(theme, "system", "text_logoPlaceholderText",
ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR |
ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING |
ThemeFlags::TEXT);
if (!entry.data.logo)
entry.data.logo = text;
}
else {
// Fallback to legacy centered placeholder text.
auto text = std::make_shared<TextComponent>(entry.name, Font::get(FONT_SIZE_LARGE),
0x000000FF, ALIGN_CENTER);
text->setSize(mLogoSize * mLogoScale);
text->applyTheme(theme, "system", "text_logoText",
ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR |
ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING |
ThemeFlags::TEXT);
entry.data.logo = text;
if (mType == VERTICAL || mType == VERTICAL_WHEEL) {
text->setHorizontalAlignment(mLogoAlignment);
text->setVerticalAlignment(ALIGN_CENTER);
}
else {
text->setHorizontalAlignment(ALIGN_CENTER);
text->setVerticalAlignment(mLogoAlignment);
}
}
}
if (mType == VERTICAL || mType == VERTICAL_WHEEL) {
if (mLogoAlignment == ALIGN_LEFT)
entry.data.logo->setOrigin(0, 0.5);
else if (mLogoAlignment == ALIGN_RIGHT)
entry.data.logo->setOrigin(1.0, 0.5);
else
entry.data.logo->setOrigin(0.5, 0.5);
}
else {
if (mLogoAlignment == ALIGN_TOP)
entry.data.logo->setOrigin(0.5, 0);
else if (mLogoAlignment == ALIGN_BOTTOM)
entry.data.logo->setOrigin(0.5, 1);
else
entry.data.logo->setOrigin(0.5, 0.5);
}
glm::vec2 denormalized {mLogoSize * entry.data.logo->getOrigin()};
entry.data.logo->setPosition(glm::vec3 {denormalized.x, denormalized.y, 0.0f});
add(entry);
}
void CarouselComponent::update(int deltaTime)
{
listUpdate(deltaTime);
GuiComponent::update(deltaTime);
}
bool CarouselComponent::input(InputConfig* config, Input input)
{
if (input.value != 0) {
switch (mType) {
case VERTICAL:
case VERTICAL_WHEEL:
if (config->isMappedLike("up", input)) {
if (mCancelTransitionsCallback)
mCancelTransitionsCallback();
listInput(-1);
return true;
}
if (config->isMappedLike("down", input)) {
if (mCancelTransitionsCallback)
mCancelTransitionsCallback();
listInput(1);
return true;
}
break;
case HORIZONTAL:
case HORIZONTAL_WHEEL:
default:
if (config->isMappedLike("left", input)) {
if (mCancelTransitionsCallback)
mCancelTransitionsCallback();
listInput(-1);
return true;
}
if (config->isMappedLike("right", input)) {
if (mCancelTransitionsCallback)
mCancelTransitionsCallback();
listInput(1);
return true;
}
break;
}
}
else {
if (config->isMappedLike("left", input) || config->isMappedLike("right", input) ||
config->isMappedLike("up", input) || config->isMappedLike("down", input)) {
listInput(0);
}
}
return GuiComponent::input(config, input);
}
void CarouselComponent::render(const glm::mat4& parentTrans)
{
// Background box behind logos.
glm::mat4 carouselTrans {parentTrans};
carouselTrans = glm::translate(carouselTrans, glm::vec3 {mPosition.x, mPosition.y, 0.0f});
carouselTrans = glm::translate(
carouselTrans, glm::vec3 {mOrigin.x * mSize.x * -1.0f, mOrigin.y * mSize.y * -1.0f, 0.0f});
glm::vec2 clipPos {carouselTrans[3].x, carouselTrans[3].y};
Renderer::pushClipRect(
glm::ivec2 {static_cast<int>(std::round(clipPos.x)),
static_cast<int>(std::round(clipPos.y))},
glm::ivec2 {static_cast<int>(std::round(mSize.x)), static_cast<int>(std::round(mSize.y))});
Renderer::setMatrix(carouselTrans);
Renderer::drawRect(0.0f, 0.0f, mSize.x, mSize.y, mCarouselColor, mCarouselColorEnd,
mColorGradientHorizontal);
// Draw logos.
// logoSpacing will also include the size of the logo itself.
glm::vec2 logoSpacing {};
float xOff {0.0f};
float yOff {0.0f};
switch (mType) {
case VERTICAL_WHEEL: {
yOff = (mSize.y - mLogoSize.y) / 2.0f - (mCamOffset * logoSpacing.y);
if (mLogoAlignment == ALIGN_LEFT)
xOff = mLogoSize.x / 10.0f;
else if (mLogoAlignment == ALIGN_RIGHT)
xOff = mSize.x - (mLogoSize.x * 1.1f);
else
xOff = (mSize.x - mLogoSize.x) / 2.0f;
break;
}
case VERTICAL: {
logoSpacing.y =
((mSize.y - (mLogoSize.y * mMaxLogoCount)) / (mMaxLogoCount)) + mLogoSize.y;
yOff = (mSize.y - mLogoSize.y) / 2.0f - (mCamOffset * logoSpacing.y);
if (mLogoAlignment == ALIGN_LEFT)
xOff = mLogoSize.x / 10.0f;
else if (mLogoAlignment == ALIGN_RIGHT)
xOff = mSize.x - (mLogoSize.x * 1.1f);
else
xOff = (mSize.x - mLogoSize.x) / 2.0f;
break;
}
case HORIZONTAL_WHEEL: {
xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mCamOffset * logoSpacing.y));
if (mLogoAlignment == ALIGN_TOP)
yOff = mLogoSize.y / 10.0f;
else if (mLogoAlignment == ALIGN_BOTTOM)
yOff = mSize.y - (mLogoSize.y * 1.1f);
else
yOff = (mSize.y - mLogoSize.y) / 2.0f;
break;
}
case HORIZONTAL: {
}
default: {
logoSpacing.x =
((mSize.x - (mLogoSize.x * mMaxLogoCount)) / (mMaxLogoCount)) + mLogoSize.x;
xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mCamOffset * logoSpacing.x));
if (mLogoAlignment == ALIGN_TOP)
yOff = mLogoSize.y / 10.0f;
else if (mLogoAlignment == ALIGN_BOTTOM)
yOff = mSize.y - (mLogoSize.y * 1.1f);
else
yOff = (mSize.y - mLogoSize.y) / 2.0f;
break;
}
}
int center {static_cast<int>(mCamOffset)};
int logoCount {std::min(mMaxLogoCount, static_cast<int>(mEntries.size()))};
// Adding texture loading buffers depending on scrolling speed and status.
int bufferIndex {getScrollingVelocity() + 1};
int bufferLeft {logoBuffersLeft[bufferIndex]};
int bufferRight {logoBuffersRight[bufferIndex]};
if (logoCount == 1) {
bufferLeft = 0;
bufferRight = 0;
}
for (int i = center - logoCount / 2 + bufferLeft; // Line break.
i <= center + logoCount / 2 + bufferRight; ++i) {
int index {i};
while (index < 0)
index += static_cast<int>(mEntries.size());
while (index >= static_cast<int>(mEntries.size()))
index -= static_cast<int>(mEntries.size());
glm::mat4 logoTrans {carouselTrans};
logoTrans = glm::translate(
logoTrans, glm::vec3 {i * logoSpacing.x + xOff, i * logoSpacing.y + yOff, 0.0f});
float distance = i - mCamOffset;
float scale {1.0f + ((mLogoScale - 1.0f) * (1.0f - fabs(distance)))};
scale = std::min(mLogoScale, std::max(1.0f, scale));
scale /= mLogoScale;
int opacity {
static_cast<int>(std::round(0x80 + ((0xFF - 0x80) * (1.0f - fabs(distance)))))};
opacity = std::max(static_cast<int>(0x80), opacity);
const std::shared_ptr<GuiComponent>& comp = mEntries.at(index).data.logo;
if (comp == nullptr)
continue;
if (mType == VERTICAL_WHEEL || mType == HORIZONTAL_WHEEL) {
comp->setRotationDegrees(mLogoRotation * distance);
comp->setRotationOrigin(mLogoRotationOrigin);
}
// When running at lower resolutions, prevent the scale-down to go all the way to the
// minimum value. This avoids potential single-pixel alignment issues when the logo
// can't be vertically placed exactly in the middle of the carousel. Although the
// problem theoretically exists at all resolutions, it's not visble at around 1080p
// and above.
if (std::min(Renderer::getScreenWidth(), Renderer::getScreenHeight()) < 1080.0f)
scale = glm::clamp(scale, 1.0f / mLogoScale + 0.01f, 1.0f);
comp->setScale(scale);
comp->setOpacity(static_cast<unsigned char>(opacity));
comp->render(logoTrans);
}
Renderer::popClipRect();
}
void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
const std::string& view,
const std::string& element,
unsigned int properties)
{
using namespace ThemeFlags;
const ThemeData::ThemeElement* elem {theme->getElement(view, element, "carousel")};
mSize.x = Renderer::getScreenWidth();
mSize.y = Renderer::getScreenHeight() * 0.2325f;
mPosition.x = 0.0f;
mPosition.y = floorf(0.5f * (Renderer::getScreenHeight() - mSize.y));
mCarouselColor = 0xFFFFFFD8;
mCarouselColorEnd = 0xFFFFFFD8;
mDefaultZIndex = 50.0f;
if (!elem)
return;
if (elem->has("type")) {
if (!(elem->get<std::string>("type").compare("vertical")))
mType = VERTICAL;
else if (!(elem->get<std::string>("type").compare("vertical_wheel")))
mType = VERTICAL_WHEEL;
else if (!(elem->get<std::string>("type").compare("horizontal_wheel")))
mType = HORIZONTAL_WHEEL;
else
mType = HORIZONTAL;
}
if (elem->has("color")) {
mCarouselColor = elem->get<unsigned int>("color");
mCarouselColorEnd = mCarouselColor;
}
if (elem->has("colorEnd"))
mCarouselColorEnd = elem->get<unsigned int>("colorEnd");
if (elem->has("gradientType"))
mColorGradientHorizontal = !(elem->get<std::string>("gradientType").compare("horizontal"));
if (elem->has("logoScale"))
mLogoScale = elem->get<float>("logoScale");
if (elem->has("logoSize"))
mLogoSize = elem->get<glm::vec2>("logoSize") *
glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight());
if (elem->has("maxLogoCount"))
mMaxLogoCount = static_cast<int>(std::round(elem->get<float>("maxLogoCount")));
if (elem->has("logoRotation"))
mLogoRotation = elem->get<float>("logoRotation");
if (elem->has("logoRotationOrigin"))
mLogoRotationOrigin = elem->get<glm::vec2>("logoRotationOrigin");
if (elem->has("logoAlignment")) {
if (!(elem->get<std::string>("logoAlignment").compare("left")))
mLogoAlignment = ALIGN_LEFT;
else if (!(elem->get<std::string>("logoAlignment").compare("right")))
mLogoAlignment = ALIGN_RIGHT;
else if (!(elem->get<std::string>("logoAlignment").compare("top")))
mLogoAlignment = ALIGN_TOP;
else if (!(elem->get<std::string>("logoAlignment").compare("bottom")))
mLogoAlignment = ALIGN_BOTTOM;
else
mLogoAlignment = ALIGN_CENTER;
}
GuiComponent::applyTheme(theme, view, element, ALL);
}
void CarouselComponent::onCursorChanged(const CursorState& state)
{
float startPos {mCamOffset};
float posMax {static_cast<float>(mEntries.size())};
float target {static_cast<float>(mCursor)};
// Find the shortest path to the target.
float endPos {target}; // Directly.
float dist {fabs(endPos - startPos)};
if (fabs(target + posMax - startPos - mScrollVelocity) < dist)
endPos = target + posMax; // Loop around the end (0 -> max).
if (fabs(target - posMax - startPos - mScrollVelocity) < dist)
endPos = target - posMax; // Loop around the start (max - 1 -> -1).
// This logic is only needed when there are two game systems, to prevent ugly jumps back
// an forth when selecting the same direction rapidly several times in a row.
if (posMax == 2) {
if (mPreviousScrollVelocity == 0)
mPreviousScrollVelocity = mScrollVelocity;
else if (mScrollVelocity < 0 && startPos < endPos)
mPreviousScrollVelocity = -1;
else if (mScrollVelocity > 0 && startPos > endPos)
mPreviousScrollVelocity = 1;
}
if (mPreviousScrollVelocity != 0 && posMax == 2 && mScrollVelocity == mPreviousScrollVelocity) {
if (fabs(endPos - startPos) < 0.5 || fabs(endPos - startPos) > 1.5) {
(mScrollVelocity < 0) ? endPos -= 1 : endPos += 1;
(mCursor == 0) ? mCursor = 1 : mCursor = 0;
return;
}
}
// No need to animate transition, we're not going anywhere (probably mEntries.size() == 1).
if (endPos == mCamOffset)
return;
Animation* anim = new LambdaAnimation(
[this, startPos, endPos, posMax](float t) {
t -= 1;
float f = glm::mix(startPos, endPos, t * t * t + 1);
if (f < 0)
f += posMax;
if (f >= posMax)
f -= posMax;
this->mCamOffset = f;
},
500);
setAnimation(anim, 0, nullptr, false, 0);
if (mCursorChangedCallback)
mCursorChangedCallback(state);
}

View file

@ -7,16 +7,93 @@
//
#include "GuiComponent.h"
#include "Sound.h"
#include "components/IList.h"
#include "components/ImageComponent.h"
#include "components/TextComponent.h"
#include "resources/Font.h"
class SystemData;
#ifndef ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
#define ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
class CarouselComponent : public GuiComponent
struct CarouselElement {
std::shared_ptr<GuiComponent> logo;
};
class CarouselComponent : public IList<CarouselElement, SystemData*>
{
public:
CarouselComponent();
void addElement(const std::shared_ptr<GuiComponent>& component,
const std::string& name,
SystemData* object)
{
Entry listEntry;
listEntry.name = name;
listEntry.object = object;
listEntry.data.logo = component;
add(listEntry);
}
void addEntry(const std::shared_ptr<ThemeData>& theme, Entry& entry);
Entry& getEntry(int index) { return mEntries.at(index); }
enum CarouselType {
HORIZONTAL, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
VERTICAL,
VERTICAL_WHEEL,
HORIZONTAL_WHEEL
};
int getCursor() { return mCursor; }
const CarouselType getType() { return mType; }
size_t getNumEntries() { return mEntries.size(); }
void setCursorChangedCallback(const std::function<void(CursorState state)>& func)
{
mCursorChangedCallback = func;
}
void setCancelTransitionsCallback(const std::function<void()>& func)
{
mCancelTransitionsCallback = func;
}
bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override;
void render(const glm::mat4& parentTrans) override;
void applyTheme(const std::shared_ptr<ThemeData>& theme,
const std::string& view,
const std::string& element,
unsigned int properties) override;
protected:
void onCursorChanged(const CursorState& state) override;
void onScroll() override
{
NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND);
}
private:
std::function<void(CursorState state)> mCursorChangedCallback;
std::function<void()> mCancelTransitionsCallback;
float mCamOffset;
int mPreviousScrollVelocity;
CarouselType mType;
Alignment mLogoAlignment;
int mMaxLogoCount;
glm::vec2 mLogoSize;
float mLogoScale;
float mLogoRotation;
glm::vec2 mLogoRotationOrigin;
unsigned int mCarouselColor;
unsigned int mCarouselColorEnd;
bool mColorGradientHorizontal;
};
#endif // ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H