diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 1c75bdffb..065338197 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -21,6 +21,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGameScraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiLaunchScreen.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMediaViewerOptions.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.h @@ -73,6 +74,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGameScraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiLaunchScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMediaViewerOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.cpp diff --git a/es-app/src/guis/GuiLaunchScreen.cpp b/es-app/src/guis/GuiLaunchScreen.cpp new file mode 100644 index 000000000..a34a16489 --- /dev/null +++ b/es-app/src/guis/GuiLaunchScreen.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiLaunchScreen.cpp +// +// Screen shown when launching a game. +// + +#include "guis/GuiLaunchScreen.h" + +#include "components/ComponentGrid.h" +#include "components/TextComponent.h" +#include "math/Misc.h" +#include "utils/StringUtil.h" +#include "FileData.h" +#include "SystemData.h" + +GuiLaunchScreen::GuiLaunchScreen( + Window* window) + : GuiComponent(window), + mBackground(window, ":/graphics/frame.svg"), + mGrid(nullptr), + mMarquee(nullptr), + mWindow(window) +{ + addChild(&mBackground); + mWindow->setLaunchScreen(this); +} + +GuiLaunchScreen::~GuiLaunchScreen() +{ + closeLaunchScreen(); +} + +void GuiLaunchScreen::displayLaunchScreen(FileData* game) +{ + mGrid = new ComponentGrid(mWindow, Vector2i(3, 8)); + addChild(mGrid); + + mImagePath = game->getMarqueePath(); + + // We need to unload the image first as it may be cached at a modified resolution + // which would lead to the wrong size when using the image here. + if (mImagePath != "") { + TextureResource::manualUnload(mImagePath, false); + mMarquee = new ImageComponent(mWindow); + } + + mScaleUp = 0.5f; + const float titleFontSize = 0.060f; + const float gameNameFontSize = 0.073f; + + // Spacer row. + mGrid->setEntry(std::make_shared(mWindow), Vector2i(1, 0), + false, false, Vector2i(1, 1)); + + // Title. + mTitle = std::make_shared(mWindow, "LAUNCHING GAME", + Font::get(titleFontSize * std::min(Renderer::getScreenHeight(), + Renderer::getScreenWidth())), 0x777777FF, ALIGN_CENTER); + mGrid->setEntry(mTitle, Vector2i(1, 1), false, true, Vector2i(1, 1)); + + // Spacer row. + mGrid->setEntry(std::make_shared(mWindow), Vector2i(1, 2), + false, false, Vector2i(1, 1)); + + // Row for the marquee. + mGrid->setEntry(std::make_shared(mWindow), Vector2i(1, 3), + false, false, Vector2i(1, 1)); + + // Spacer row. + mGrid->setEntry(std::make_shared(mWindow), Vector2i(1, 4), + false, false, Vector2i(1, 1)); + + // Game name. + mGameName = std::make_shared(mWindow, "GAME NAME", + Font::get(gameNameFontSize * std::min(Renderer::getScreenHeight(), + Renderer::getScreenWidth())), 0x555555FF, ALIGN_CENTER); + mGrid->setEntry(mGameName, Vector2i(1, 5), false, true, Vector2i(1, 1)); + + // System name. + mSystemName = std::make_shared(mWindow, "SYSTEM NAME", + Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + mGrid->setEntry(mSystemName, Vector2i(1, 6), false, true, Vector2i(1, 1)); + + // Spacer row. + mGrid->setEntry(std::make_shared(mWindow), Vector2i(1, 7), + false, false, Vector2i(1, 1)); + + // Left spacer. + mGrid->setEntry(std::make_shared(mWindow), Vector2i(0, 0), + false, false, Vector2i(1, 8)); + + // Right spacer. + mGrid->setEntry(std::make_shared(mWindow), Vector2i(2, 0), + false, false, Vector2i(1, 8)); + + // Adjust the width depending on the aspect ratio of the screen, to make the screen look + // somewhat coherent regardless of screen type. The 1.778 aspect ratio value is the 16:9 + // reference. + float aspectValue = 1.778f / Renderer::getScreenAspectRatio(); + + float maxWidthModifier = Math::clamp(0.78f * aspectValue, 0.78f, 0.90f); + float minWidthModifier = Math::clamp(0.40f * aspectValue, 0.40f, 0.56f); + + float maxWidth = static_cast(Renderer::getScreenWidth()) * maxWidthModifier; + float minWidth = static_cast(Renderer::getScreenWidth()) * minWidthModifier; + + float fontWidth = Font::get(gameNameFontSize * std::min(Renderer::getScreenHeight(), + Renderer::getScreenWidth()))->sizeText(Utils::String::toUpper(game->getName())).x(); + + // Add a bit of width to compensate for the left and right spacers. + fontWidth += static_cast(Renderer::getScreenWidth()) * 0.05f; + + float width = Math::clamp(fontWidth, minWidth, maxWidth); + + if (mImagePath != "") + setSize(width, static_cast(Renderer::getScreenHeight()) * 0.60f); + else + setSize(width, static_cast(Renderer::getScreenHeight()) * 0.35f); + + // Set row heights. + mGrid->setRowHeightPerc(0, 0.09f, false); + mGrid->setRowHeightPerc(1, mTitle->getFont()->getLetterHeight() * 1.70f / mSize.y(), false); + mGrid->setRowHeightPerc(2, 0.05f, false); + if (mImagePath != "") + mGrid->setRowHeightPerc(3, 0.35f, false); + else + mGrid->setRowHeightPerc(3, 0.08f, false); + mGrid->setRowHeightPerc(4, 0.05f, false); + mGrid->setRowHeightPerc(5, mGameName->getFont()->getHeight() * 0.80f / mSize.y(), false); + mGrid->setRowHeightPerc(6, mSystemName->getFont()->getHeight() * 0.90f / mSize.y(), false); + + // Set left and right spacers column widths. + mGrid->setColWidthPerc(0, 0.025f); + mGrid->setColWidthPerc(2, 0.025f); + + mGrid->setSize(mSize); + + float totalRowHeight = 0.0f; + + // Hack to adjust the window height to the row boundary. + for (int i = 0; i < 7; i++) + totalRowHeight += mGrid->getRowHeight(i); + + setSize(mSize.x(), totalRowHeight); + + mGameName->setText(Utils::String::toUpper(game->getName())); + mSystemName->setText(Utils::String::toUpper(game->getSystem()->getFullName())); + + // For the marquee we strip away any transparent padding around the actual image. + // When doing this, we restrict the scale-up to a certain percentage of the screen + // width so that the sizes look somewhat consistent regardless of the aspect ratio + // of the images. + if (mImagePath != "") { + mMarquee->setImage(game->getMarqueePath(), false); + mMarquee->cropTransparentPadding(static_cast(Renderer::getScreenWidth()) * + (0.25f * (1.778f / Renderer::getScreenAspectRatio())), mGrid->getRowHeight(3)); + + mMarquee->setOrigin(0.5f, 0.5f); + Vector3f currentPos = mMarquee->getPosition(); + Vector2f currentSize = mMarquee->getSize(); + + // Position the image in the middle of row four. + currentPos.x() = mSize.x() / 2.0f; + currentPos.y() = mGrid->getRowHeight(0) + mGrid->getRowHeight(1) + + mGrid->getRowHeight(2) + mGrid->getRowHeight(3) / 2.0f; + mMarquee->setPosition(currentPos); + } + + mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + + // Center the screen both on the X and Y axes. + setPosition((static_cast(Renderer::getScreenWidth()) - mSize.x()) / 2.0f, + (static_cast(Renderer::getScreenHeight()) - mSize.y()) / 2.0f); +} + +void GuiLaunchScreen::closeLaunchScreen() +{ + if (mGrid) { + delete mGrid; + mGrid = nullptr; + } + + if (mMarquee) { + delete mMarquee; + mMarquee = nullptr; + } + + // An extra precaution. + if (mImagePath != "") { + TextureResource::manualUnload(mImagePath, false); + mImagePath = ""; + } +} + +void GuiLaunchScreen::onSizeChanged() +{ + mGrid->setSize(mSize); +} + +void GuiLaunchScreen::update(int deltaTime) +{ + if (mScaleUp < 1.0f) + mScaleUp = Math::clamp(mScaleUp + 0.07f, 0.0f, 1.0f); +} + +void GuiLaunchScreen::render() +{ + // Scale up animation. + if (mScaleUp < 1.0f) { + setOrigin({0.5f, 0.5f}); + setPosition(static_cast(Renderer::getScreenWidth()) / 2.0f, + static_cast(Renderer::getScreenHeight()) / 2.0f); + setScale(mScaleUp); + } + + Transform4x4f trans = Transform4x4f::Identity() * getTransform(); + Renderer::setMatrix(trans); + + GuiComponent::renderChildren(trans); + + if (mMarquee) + mMarquee->render(trans); +} diff --git a/es-app/src/guis/GuiLaunchScreen.h b/es-app/src/guis/GuiLaunchScreen.h new file mode 100644 index 000000000..56a844fc9 --- /dev/null +++ b/es-app/src/guis/GuiLaunchScreen.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiLaunchScreen.h +// +// Screen shown when launching a game. +// + +#ifndef ES_APP_GUIS_GUI_LAUNCH_SCREEN_H +#define ES_APP_GUIS_GUI_LAUNCH_SCREEN_H + +#include "components/ComponentGrid.h" +#include "components/ImageComponent.h" +#include "components/NinePatchComponent.h" +#include "components/TextComponent.h" +#include "GuiComponent.h" +#include "Window.h" + +class FileData; + +class GuiLaunchScreen : public Window::GuiLaunchScreen, GuiComponent +{ +public: + GuiLaunchScreen(Window* window); + virtual ~GuiLaunchScreen(); + + virtual void displayLaunchScreen(FileData* game) override; + virtual void closeLaunchScreen() override; + + void onSizeChanged() override; + + virtual void update(int deltaTime) override; + virtual void render() override; + +private: + Window* mWindow; + ComponentGrid* mGrid; + + NinePatchComponent mBackground; + + std::shared_ptr mTitle; + std::shared_ptr mGameName; + std::shared_ptr mSystemName; + + ImageComponent* mMarquee; + std::string mImagePath; + + float mScaleUp; +}; + +#endif // ES_APP_GUIS_GUI_LAUNCH_SCREEN_H diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 3340126ce..50931afc4 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -350,7 +350,31 @@ void GuiMenu::openUIOptions() s->setNeedsSaving(); } }); + #endif + // Launch screen duration. + auto launch_screen_duration = std::make_shared> + (mWindow, getHelpStyle(), "LAUNCH SCREEN DURATION", false); + std::string selectedDuration = Settings::getInstance()->getString("LaunchScreenDuration"); + launch_screen_duration->add("NORMAL", "normal", selectedDuration == "normal"); + launch_screen_duration->add("BRIEF", "brief", selectedDuration == "brief"); + launch_screen_duration->add("LONG", "long", selectedDuration == "long"); + launch_screen_duration->add("DISABLED", "disabled", selectedDuration == "disabled"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the duration to "normal" in this case. + if (launch_screen_duration->getSelectedObjects().size() == 0) + launch_screen_duration->selectEntry(0); + s->addWithLabel("LAUNCH SCREEN DURATION", launch_screen_duration); + s->addSaveFunc([launch_screen_duration, s] { + if (launch_screen_duration->getSelected() != + Settings::getInstance()->getString("LaunchScreenDuration")) { + Settings::getInstance()->setString("LaunchScreenDuration", + launch_screen_duration->getSelected()); + s->setNeedsSaving(); + } + }); + + #if defined(USE_OPENGL_21) // Blur background when the menu is open. auto menu_blur_background = std::make_shared(mWindow); menu_blur_background->setState(Settings::getInstance()->getBool("MenuBlurBackground")); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 0651a53c8..a0d4a864e 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -21,6 +21,7 @@ #include "guis/GuiDetectDevice.h" #include "guis/GuiMsgBox.h" #include "guis/GuiComplexTextEditPopup.h" +#include "guis/GuiLaunchScreen.h" #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" #include "views/ViewController.h" @@ -479,6 +480,7 @@ int main(int argc, char* argv[]) Window window; SystemScreensaver screensaver(&window); MediaViewer mediaViewer(&window); + GuiLaunchScreen guiLaunchScreen(&window); ViewController::init(&window); CollectionSystemsManager::init(&window); window.pushGui(ViewController::get()); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 1f8d736e5..29cca419d 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -640,24 +640,43 @@ void ViewController::launch(FileData* game) stopAnimation(1); // Make sure the fade in isn't still playing. mWindow->stopInfoPopup(); // Make sure we disable any existing info popup. - // Until a proper game launch screen is implemented, at least this will let the - // user know that something is actually happening (in addition to the launch sound, - // if navigation sounds are enabled). - GuiInfoPopup* s = new GuiInfoPopup(mWindow, "LAUNCHING GAME '" + - Utils::String::toUpper(game->metadata.get("name") + "'"), 10000); - mWindow->setInfoPopup(s); + int duration = 0; + std::string durationString = Settings::getInstance()->getString("LaunchScreenDuration"); + + if (durationString == "disabled") { + // If the game launch screen has been set as disabled, show a simple info popup + // notification instead. + GuiInfoPopup* s = new GuiInfoPopup(mWindow, "LAUNCHING GAME '" + + Utils::String::toUpper(game->metadata.get("name") + "'"), 10000); + mWindow->setInfoPopup(s); + duration = 1700; + } + else if (durationString == "brief") { + duration = 1700; + } + else if (durationString == "long") { + duration = 4500; + } + else { + // Normal duration. + duration = 3000; + } + + if (durationString != "disabled") + mWindow->displayLaunchScreen(game->getSourceFileData()); NavigationSounds::getInstance()->playThemeNavigationSound(LAUNCHSOUND); - // This is just a dummy animation in order for the launch notification popup to be - // displayed briefly, and for the navigation sound playing to be able to complete. + // This is just a dummy animation in order for the launch screen or notification popup + // to be displayed briefly, and for the navigation sound playing to be able to complete. // During this time period, all user input is blocked. - setAnimation(new LambdaAnimation([](float t){}, 1700), 0, [this, game] { - while (NavigationSounds::getInstance()->isPlayingThemeNavigationSound(LAUNCHSOUND)); + setAnimation(new LambdaAnimation([](float t){}, duration), 0, [this, game] { game->launchGame(mWindow); + // If the launch screen is disabled then this will do nothing. + mWindow->closeLaunchScreen(); onFileChanged(game, true); - // This is a workaround so that any key or button presses used for exiting the emulator - // are not captured upon returning to ES. + // This is a workaround so that any keys or button presses used for exiting the emulator + // are not captured upon returning. setAnimation(new LambdaAnimation([](float t){}, 1), 0, [this] { mLockInput = false; }); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 89d93f7af..2ed9579ba 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -138,6 +138,7 @@ void Settings::setDefaults() mStringMap["UIMode"] = { "full", "full" }; mStringMap["DefaultSortOrder"] = { "filename, ascending", "filename, ascending" }; mStringMap["MenuOpeningEffect"] = { "scale-up", "scale-up" }; + mStringMap["LaunchScreenDuration"] = { "normal", "normal" }; mBoolMap["MenuBlurBackground"] = { true, true }; mBoolMap["GamelistVideoPillarbox"] = { true, true }; mBoolMap["GamelistVideoScanlines"] = { false, false }; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 2759ac08d..eca63e27a 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -25,6 +25,7 @@ Window::Window() : mScreensaver(nullptr), mMediaViewer(nullptr), + mLaunchScreen(nullptr), mInfoPopup(nullptr), mNormalizeNextUpdate(false), mFrameTimeElapsed(0), @@ -35,6 +36,7 @@ Window::Window() mTimeSinceLastInput(0), mRenderScreensaver(false), mRenderMediaViewer(false), + mRenderLaunchScreen(false), mGameLaunchedState(false), mAllowTextScrolling(true), mCachedBackground(false), @@ -159,6 +161,13 @@ void Window::input(InputConfig* config, Input input) return; } + if (mGameLaunchedState && mLaunchScreen && mRenderLaunchScreen) { + if (input.value != 0) { + mLaunchScreen->closeLaunchScreen(); + mRenderLaunchScreen = false; + } + } + if (mScreensaver) { if (mScreensaver->isScreensaverActive() && Settings::getInstance()->getBool("ScreensaverControls") && @@ -310,8 +319,8 @@ void Window::update(int deltaTime) // If the theme set changed, we need to update the background once so that the camera // will be moved. This is required as theme set changes always makes a transition to - // the system view. If we wouldn't make this update, the camera movement would only - // take place once the menu has been closed. + // the system view. If we wouldn't make this update, the camera movement would take + // place once the menu has been closed. if (mChangedThemeSet && mGuiStack.size() > 1) { mGuiStack.front()->update(deltaTime); mChangedThemeSet = false; @@ -320,6 +329,9 @@ void Window::update(int deltaTime) if (mMediaViewer && mRenderMediaViewer) mMediaViewer->update(deltaTime); + if (mLaunchScreen && mRenderLaunchScreen) + mLaunchScreen->update(deltaTime); + if (mScreensaver && mRenderScreensaver) mScreensaver->update(deltaTime); } @@ -358,7 +370,7 @@ void Window::render() if (renderBottom) bottom->render(transform); - if (bottom != top) { + if (bottom != top || mRenderLaunchScreen) { #if defined(USE_OPENGL_21) if (!mCachedBackground) { // Generate a cache texture of the shaded background when opening the menu, which @@ -443,23 +455,25 @@ void Window::render() #if defined(USE_OPENGL_21) // Menu opening effects (scale-up and fade-in). if (Settings::getInstance()->getString("MenuOpeningEffect") == "scale-up") { - if (mTopScale < 1.0) + if (mTopScale < 1.0f) { mTopScale = Math::clamp(mTopScale + 0.07f, 0.0f, 1.0f); - Vector2f topCenter = top->getCenter(); - top->setOrigin({0.5, 0.5}); - top->setPosition({topCenter.x(), topCenter.y(), 0}); - top->setScale(mTopScale); + Vector2f topCenter = top->getCenter(); + top->setOrigin({0.5, 0.5}); + top->setPosition({topCenter.x(), topCenter.y(), 0}); + top->setScale(mTopScale); + } } if (Settings::getInstance()->getString("MenuOpeningEffect") == "fade-in") { // Fade-in menu. if (mTopOpacity < 255) { - mTopOpacity = Math::clamp(mTopOpacity+15, 0, 255); + mTopOpacity = Math::clamp(mTopOpacity + 15, 0, 255); top->setOpacity(mTopOpacity); } } #endif - top->render(transform); + if (!mRenderLaunchScreen) + top->render(transform); } else { mCachedBackground = false; @@ -523,6 +537,9 @@ void Window::render() if (mRenderMediaViewer) mMediaViewer->render(); + if (mRenderLaunchScreen) + mLaunchScreen->render(); + if (Settings::getInstance()->getBool("DisplayGPUStatistics") && mFrameDataText) { Renderer::setMatrix(Transform4x4f::Identity()); mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); @@ -711,6 +728,22 @@ void Window::stopMediaViewer() mRenderMediaViewer = false; } +void Window::displayLaunchScreen(FileData* game) +{ + if (mLaunchScreen) { + mLaunchScreen->displayLaunchScreen(game); + mRenderLaunchScreen = true; + } +} + +void Window::closeLaunchScreen() +{ + if (mLaunchScreen) + mLaunchScreen->closeLaunchScreen(); + + mRenderLaunchScreen = false; +} + void Window::increaseVideoPlayerCount() { mVideoCountMutex.lock(); diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 2ccaa778a..907155978 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -64,6 +64,15 @@ public: virtual void render() = 0; }; + class GuiLaunchScreen + { + public: + virtual void displayLaunchScreen(FileData* game) = 0; + virtual void closeLaunchScreen() = 0; + virtual void update(int deltaTime) = 0; + virtual void render() = 0; + }; + class InfoPopup { public: @@ -118,6 +127,11 @@ public: void setMediaViewer(MediaViewer* mediaViewer) { mMediaViewer = mediaViewer; } bool isMediaViewerActive() { return mRenderMediaViewer; } + void displayLaunchScreen(FileData* game); + void closeLaunchScreen(); + void setLaunchScreen(GuiLaunchScreen* launchScreen) { mLaunchScreen = launchScreen; } + bool isLaunchScreenDisplayed() { return mRenderLaunchScreen; } + void increaseVideoPlayerCount(); void decreaseVideoPlayerCount(); int getVideoPlayerCount(); @@ -151,6 +165,9 @@ private: MediaViewer* mMediaViewer; bool mRenderMediaViewer; + GuiLaunchScreen* mLaunchScreen; + bool mRenderLaunchScreen; + std::string mListScrollText; std::shared_ptr mListScrollFont; unsigned char mListScrollOpacity; diff --git a/es-core/src/components/ScrollableContainer.cpp b/es-core/src/components/ScrollableContainer.cpp index f5a3805aa..f2d12ff54 100644 --- a/es-core/src/components/ScrollableContainer.cpp +++ b/es-core/src/components/ScrollableContainer.cpp @@ -91,7 +91,7 @@ void ScrollableContainer::update(int deltaTime) // Don't scroll if the media viewer or screensaver is active or if text scrolling is disabled; if (mWindow->isMediaViewerActive() || mWindow->isScreensaverActive() || !mWindow->getAllowTextScrolling()) { - if (mScrollPos != 0) + if (mScrollPos != 0 && !mWindow->isLaunchScreenDisplayed()) reset(); return; }