diff --git a/bin/resources/fullscreenui/applications-system.png b/bin/resources/fullscreenui/applications-system.png new file mode 100644 index 0000000000..2007dbb7d6 Binary files /dev/null and b/bin/resources/fullscreenui/applications-system.png differ diff --git a/bin/resources/fullscreenui/media-cdrom.png b/bin/resources/fullscreenui/media-cdrom.png new file mode 100644 index 0000000000..a85334c7ec Binary files /dev/null and b/bin/resources/fullscreenui/media-cdrom.png differ diff --git a/bin/resources/fullscreenui/placeholder.png b/bin/resources/fullscreenui/placeholder.png new file mode 100644 index 0000000000..064082d2c8 Binary files /dev/null and b/bin/resources/fullscreenui/placeholder.png differ diff --git a/pcsx2-qt/AboutDialog.cpp b/pcsx2-qt/AboutDialog.cpp index ffb224c32a..8e7cf67eb8 100644 --- a/pcsx2-qt/AboutDialog.cpp +++ b/pcsx2-qt/AboutDialog.cpp @@ -15,6 +15,8 @@ #include "PrecompiledHeader.h" +#include "pcsx2/SysForwardDefs.h" + #include "AboutDialog.h" #include "QtHost.h" #include "QtUtils.h" @@ -52,25 +54,25 @@ AboutDialog::~AboutDialog() = default; QString AboutDialog::getWebsiteUrl() { - return QStringLiteral("https://pcsx2.net/"); + return QString::fromUtf8(PCSX2_WEBSITE_URL); } QString AboutDialog::getSupportForumsUrl() { - return QStringLiteral("https://forums.pcsx2.net/"); + return QString::fromUtf8(PCSX2_FORUMS_URL); } QString AboutDialog::getGitHubRepositoryUrl() { - return QStringLiteral("https://github.com/PCSX2/pcsx2"); + return QString::fromUtf8(PCSX2_GITHUB_URL); } QString AboutDialog::getLicenseUrl() { - return QStringLiteral("https://github.com/PCSX2/pcsx2/blob/master/pcsx2/Docs/License.txt"); + return QString::fromUtf8(PCSX2_LICENSE_URL); } QString AboutDialog::getDiscordServerUrl() { - return QStringLiteral("https://discord.com/invite/TCz3t9k"); + return QString::fromUtf8(PCSX2_DISCORD_URL); } diff --git a/pcsx2-qt/DisplayWidget.cpp b/pcsx2-qt/DisplayWidget.cpp index cfd820eaff..80a3f06eb2 100644 --- a/pcsx2-qt/DisplayWidget.cpp +++ b/pcsx2-qt/DisplayWidget.cpp @@ -183,6 +183,24 @@ void DisplayWidget::updateCursor(bool master_enable) unsetCursor(); } +void DisplayWidget::handleCloseEvent(QCloseEvent* event) +{ + // Closing the separate widget will either cancel the close, or trigger shutdown. + // In the latter case, it's going to destroy us, so don't let Qt do it first. + if (QtHost::IsVMValid()) + { + QMetaObject::invokeMethod(g_main_window, "requestShutdown", Q_ARG(bool, true), + Q_ARG(bool, true), Q_ARG(bool, false)); + } + else + { + QMetaObject::invokeMethod(g_main_window, &MainWindow::requestExit); + } + + // Cancel the event from closing the window. + event->ignore(); +} + void DisplayWidget::updateCenterPos() { #ifdef _WIN32 @@ -330,6 +348,7 @@ bool DisplayWidget::event(QEvent* event) // don't toggle fullscreen when we're bound.. that wouldn't end well. if (event->type() == QEvent::MouseButtonDblClick && static_cast(event)->button() == Qt::LeftButton && + QtHost::IsVMValid() && !QtHost::IsVMPaused() && !InputManager::HasAnyBindingsForKey(InputManager::MakePointerButtonKey(0, 0)) && Host::GetBoolSettingValue("UI", "DoubleClickTogglesFullscreen", true)) { @@ -384,10 +403,7 @@ bool DisplayWidget::event(QEvent* event) case QEvent::Close: { - // Closing the separate widget will either cancel the close, or trigger shutdown. - // In the latter case, it's going to destroy us, so don't let Qt do it first. - QMetaObject::invokeMethod(g_main_window, "requestShutdown", Q_ARG(bool, true), Q_ARG(bool, true), Q_ARG(bool, false)); - event->ignore(); + handleCloseEvent(static_cast(event)); return true; } @@ -454,12 +470,9 @@ DisplayWidget* DisplayContainer::removeDisplayWidget() bool DisplayContainer::event(QEvent* event) { - if (event->type() == QEvent::Close) + if (event->type() == QEvent::Close && m_display_widget) { - // Closing the separate widget will either cancel the close, or trigger shutdown. - // In the latter case, it's going to destroy us, so don't let Qt do it first. - QMetaObject::invokeMethod(g_main_window, "requestShutdown", Q_ARG(bool, true), Q_ARG(bool, true), Q_ARG(bool, false)); - event->ignore(); + m_display_widget->handleCloseEvent(static_cast(event)); return true; } diff --git a/pcsx2-qt/DisplayWidget.h b/pcsx2-qt/DisplayWidget.h index d97969b310..34905ddda3 100644 --- a/pcsx2-qt/DisplayWidget.h +++ b/pcsx2-qt/DisplayWidget.h @@ -20,6 +20,8 @@ #include #include +class QCloseEvent; + class DisplayWidget final : public QWidget { Q_OBJECT @@ -41,6 +43,8 @@ public: void updateRelativeMode(bool master_enable); void updateCursor(bool master_enable); + void handleCloseEvent(QCloseEvent* event); + Q_SIGNALS: void windowResizedEvent(int width, int height, float scale); void windowRestoredEvent(); diff --git a/pcsx2-qt/EmuThread.cpp b/pcsx2-qt/EmuThread.cpp index 1395265f13..ac9a7b4d39 100644 --- a/pcsx2-qt/EmuThread.cpp +++ b/pcsx2-qt/EmuThread.cpp @@ -30,6 +30,7 @@ #include "pcsx2/Counters.h" #include "pcsx2/Frontend/InputManager.h" #include "pcsx2/Frontend/ImGuiManager.h" +#include "pcsx2/Frontend/FullscreenUI.h" #include "pcsx2/GS.h" #include "pcsx2/GS/GS.h" #include "pcsx2/GSDumpReplayer.h" @@ -87,6 +88,9 @@ void EmuThread::stopInThread() if (VMManager::HasValidVM()) destroyVM(); + if (m_run_fullscreen_ui) + stopFullscreenUI(); + m_event_loop->quit(); m_shutdown_flag.store(true); } @@ -126,6 +130,53 @@ bool EmuThread::confirmMessage(const QString& title, const QString& message) return result; } +void EmuThread::startFullscreenUI(bool fullscreen) +{ + if (!isOnEmuThread()) + { + QMetaObject::invokeMethod(this, "startFullscreenUI", Qt::QueuedConnection, Q_ARG(bool, fullscreen)); + return; + } + + if (VMManager::HasValidVM()) + return; + + m_run_fullscreen_ui = true; + if (fullscreen) + m_is_fullscreen = true; + + if (!GetMTGS().WaitForOpen()) + { + m_run_fullscreen_ui = false; + return; + } + + // poll more frequently so we don't lose events + stopBackgroundControllerPollTimer(); + startBackgroundControllerPollTimer(); +} + +void EmuThread::stopFullscreenUI() +{ + if (!isOnEmuThread()) + { + QMetaObject::invokeMethod(this, &EmuThread::stopFullscreenUI, Qt::QueuedConnection); + + // wait until the host display is gone + while (s_host_display) + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); + + return; + } + + if (!s_host_display) + return; + + pxAssertRel(!VMManager::HasValidVM(), "VM is not valid at FSUI shutdown time"); + m_run_fullscreen_ui = false; + GetMTGS().WaitForClose(); +} + void EmuThread::startVM(std::shared_ptr boot_params) { if (!isOnEmuThread()) @@ -136,15 +187,16 @@ void EmuThread::startVM(std::shared_ptr boot_params) } pxAssertRel(!VMManager::HasValidVM(), "VM is shut down"); - loadOurSettings(); + + // Only initialize fullscreen/render-to-main when we're not running big picture. + if (!m_run_fullscreen_ui) + loadOurInitialSettings(); + + if (boot_params->fullscreen.has_value()) + m_is_fullscreen = boot_params->fullscreen.value(); emit onVMStarting(); - // create the display, this may take a while... - m_is_fullscreen = boot_params->fullscreen.value_or(Host::GetBaseBoolSettingValue("UI", "StartFullscreen", false)); - m_is_rendering_to_main = shouldRenderToMain(); - m_is_surfaceless = false; - m_save_state_on_shutdown = false; if (!VMManager::Initialize(*boot_params)) return; @@ -265,6 +317,7 @@ void EmuThread::saveStateToSlot(qint32 slot) void EmuThread::run() { + Threading::SetNameOfCurrentThread("EmuThread"); PerformanceMetrics::SetCPUThread(Threading::ThreadHandle::GetForCallingThread()); m_event_loop = new QEventLoop(); m_started_semaphore.release(); @@ -273,8 +326,13 @@ void EmuThread::run() if (!VMManager::Internal::InitializeGlobals() || !VMManager::Internal::InitializeMemory()) pxFailRel("Failed to allocate memory map"); - // we need input sources ready for binding - reloadInputSources(); + // We want settings loaded so we choose the correct renderer for big picture mode. + // This also sorts out input sources. + loadOurSettings(); + loadOurInitialSettings(); + VMManager::LoadSettings(); + + // Start background polling because the VM won't do it for us. createBackgroundControllerPollTimer(); startBackgroundControllerPollTimer(); connectSignals(); @@ -362,7 +420,9 @@ void EmuThread::startBackgroundControllerPollTimer() if (m_background_controller_polling_timer->isActive()) return; - m_background_controller_polling_timer->start(BACKGROUND_CONTROLLER_POLLING_INTERVAL); + m_background_controller_polling_timer->start(FullscreenUI::IsInitialized() ? + FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL : + BACKGROUND_CONTROLLER_POLLING_INTERVAL); } void EmuThread::stopBackgroundControllerPollTimer() @@ -397,7 +457,7 @@ void EmuThread::setFullscreen(bool fullscreen) return; } - if (!VMManager::HasValidVM() || m_is_fullscreen == fullscreen) + if (!GetMTGS().IsOpen() || m_is_fullscreen == fullscreen) return; // This will call back to us on the MTGS thread. @@ -417,9 +477,13 @@ void EmuThread::setSurfaceless(bool surfaceless) return; } - if (!VMManager::HasValidVM() || m_is_surfaceless == surfaceless) + if (!GetMTGS().IsOpen() || m_is_surfaceless == surfaceless) return; + // If we went surfaceless and were running the fullscreen UI, stop MTGS running idle. + // Otherwise, we'll keep trying to present to nothing. + GetMTGS().SetRunIdle(!surfaceless && m_run_fullscreen_ui); + // This will call back to us on the MTGS thread. m_is_surfaceless = surfaceless; GetMTGS().UpdateDisplayWindow(); @@ -476,11 +540,19 @@ void EmuThread::connectSignals() connect(qApp, &QGuiApplication::applicationStateChanged, this, &EmuThread::onApplicationStateChanged); } +void EmuThread::loadOurInitialSettings() +{ + m_is_fullscreen = Host::GetBaseBoolSettingValue("UI", "StartFullscreen", false); + m_is_rendering_to_main = shouldRenderToMain(); + m_is_surfaceless = false; + m_save_state_on_shutdown = false; +} + void EmuThread::checkForSettingChanges() { QMetaObject::invokeMethod(g_main_window, &MainWindow::checkForSettingChanges, Qt::QueuedConnection); - if (VMManager::HasValidVM()) + if (s_host_display) { const bool render_to_main = shouldRenderToMain(); if (!m_is_fullscreen && m_is_rendering_to_main != render_to_main) @@ -772,6 +844,14 @@ HostDisplay* EmuThread::acquireHostDisplay(HostDisplay::RenderAPI api) Console.WriteLn(Color_StrongGreen, "%s Graphics Driver Info:", HostDisplay::RenderAPIToString(s_host_display->GetRenderAPI())); Console.Indent().WriteLn(s_host_display->GetDriverInfo()); + if (m_run_fullscreen_ui && !FullscreenUI::Initialize()) + { + Console.Error("Failed to initialize fullscreen UI"); + releaseHostDisplay(); + m_run_fullscreen_ui = false; + return nullptr; + } + return s_host_display.get(); } @@ -816,6 +896,7 @@ void Host::EndPresentFrame() if (GSDumpReplayer::IsReplayingDump()) GSDumpReplayer::RenderUI(); + FullscreenUI::Render(); ImGuiManager::RenderOSD(); s_host_display->EndPresent(); ImGuiManager::NewFrame(); @@ -1036,14 +1117,15 @@ void Host::RequestExit(bool save_state_if_running) QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection); } -void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state) +void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state) { if (!VMManager::HasValidVM()) return; // Run it on the host thread, that way we get the confirm prompt (if enabled). QMetaObject::invokeMethod(g_main_window, "requestShutdown", Qt::QueuedConnection, - Q_ARG(bool, allow_confirm), Q_ARG(bool, allow_save_state), Q_ARG(bool, false)); + Q_ARG(bool, allow_confirm), Q_ARG(bool, allow_save_state), + Q_ARG(bool, default_save_state), Q_ARG(bool, false)); } bool Host::IsFullscreen() diff --git a/pcsx2-qt/EmuThread.h b/pcsx2-qt/EmuThread.h index bb43889f7b..776971fcb3 100644 --- a/pcsx2-qt/EmuThread.h +++ b/pcsx2-qt/EmuThread.h @@ -47,6 +47,7 @@ public: __fi bool isFullscreen() const { return m_is_fullscreen; } __fi bool isRenderingToMain() const { return m_is_rendering_to_main; } __fi bool isSurfaceless() const { return m_is_surfaceless; } + __fi bool isRunningFullscreenUI() const { return m_run_fullscreen_ui; } bool isOnEmuThread() const; @@ -62,6 +63,8 @@ public: public Q_SLOTS: bool confirmMessage(const QString& title, const QString& message); + void startFullscreenUI(bool fullscreen); + void stopFullscreenUI(); void startVM(std::shared_ptr boot_params); void resetVM(); void setVMPaused(bool paused); @@ -133,8 +136,11 @@ protected: void run(); private: - static constexpr u32 BACKGROUND_CONTROLLER_POLLING_INTERVAL = - 100; /// Interval at which the controllers are polled when the system is not active. + /// Interval at which the controllers are polled when the system is not active. + static constexpr u32 BACKGROUND_CONTROLLER_POLLING_INTERVAL = 100; + + /// Poll at half the vsync rate for FSUI to reduce the chance of getting a press+release in the same frame. + static constexpr u32 FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL = 8; void destroyVM(); void executeVM(); @@ -143,8 +149,9 @@ private: void createBackgroundControllerPollTimer(); void destroyBackgroundControllerPollTimer(); - void loadOurSettings(); void connectSignals(); + void loadOurSettings(); + void loadOurInitialSettings(); private Q_SLOTS: void stopInThread(); @@ -162,6 +169,7 @@ private: std::atomic_bool m_shutdown_flag{false}; bool m_verbose_status = false; + bool m_run_fullscreen_ui = false; bool m_is_rendering_to_main = false; bool m_is_fullscreen = false; bool m_is_surfaceless = false; diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 5582777abe..879b8f64d1 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -251,8 +251,8 @@ void MainWindow::connectSignals() connect(m_ui.actionRemoveDisc, &QAction::triggered, this, &MainWindow::onRemoveDiscActionTriggered); connect(m_ui.menuChangeDisc, &QMenu::aboutToShow, this, &MainWindow::onChangeDiscMenuAboutToShow); connect(m_ui.menuChangeDisc, &QMenu::aboutToHide, this, &MainWindow::onChangeDiscMenuAboutToHide); - connect(m_ui.actionPowerOff, &QAction::triggered, this, [this]() { requestShutdown(true, true); }); - connect(m_ui.actionPowerOffWithoutSaving, &QAction::triggered, this, [this]() { requestShutdown(false, false); }); + connect(m_ui.actionPowerOff, &QAction::triggered, this, [this]() { requestShutdown(true, true, EmuConfig.SaveStateOnShutdown); }); + connect(m_ui.actionPowerOffWithoutSaving, &QAction::triggered, this, [this]() { requestShutdown(false, false, false); }); connect(m_ui.actionLoadState, &QAction::triggered, this, [this]() { m_ui.menuLoadState->exec(QCursor::pos()); }); connect(m_ui.actionSaveState, &QAction::triggered, this, [this]() { m_ui.menuSaveState->exec(QCursor::pos()); }); connect(m_ui.actionExit, &QAction::triggered, this, &MainWindow::close); @@ -354,6 +354,8 @@ void MainWindow::connectSignals() void MainWindow::connectVMThreadSignals(EmuThread* thread) { + connect(m_ui.actionStartFullscreenUI, &QAction::triggered, thread, &EmuThread::startFullscreenUI); + connect(m_ui.actionStartFullscreenUI2, &QAction::triggered, thread, &EmuThread::startFullscreenUI); connect(thread, &EmuThread::messageConfirmed, this, &MainWindow::confirmMessage, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onCreateDisplayRequested, this, &MainWindow::createDisplay, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onUpdateDisplayRequested, this, &MainWindow::updateDisplay, Qt::BlockingQueuedConnection); @@ -387,7 +389,7 @@ void MainWindow::connectVMThreadSignals(EmuThread* thread) void MainWindow::recreate() { if (s_vm_valid) - requestShutdown(false, true, true); + requestShutdown(false, true, EmuConfig.SaveStateOnShutdown); close(); g_main_window = nullptr; @@ -950,7 +952,7 @@ void MainWindow::switchToGameListView() return; } - if (s_vm_valid) + if (m_display_created) { m_was_paused_on_surface_loss = s_vm_paused; if (!s_vm_paused) @@ -965,7 +967,7 @@ void MainWindow::switchToGameListView() void MainWindow::switchToEmulationView() { - if (!s_vm_valid || !isShowingGameList()) + if (!m_display_created || !isShowingGameList()) return; // we're no longer surfaceless! this will call back to UpdateDisplay(), which will swap the widget out. @@ -1014,14 +1016,14 @@ void MainWindow::runOnUIThread(const std::function& func) func(); } -bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_save_to_state /* = true */, bool block_until_done /* = false */) +bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_save_to_state /* = true */, bool default_save_to_state /* = true */, bool block_until_done /* = false */) { if (!s_vm_valid) return true; // If we don't have a crc, we can't save state. allow_save_to_state &= (m_current_game_crc != 0); - bool save_state = allow_save_to_state && EmuConfig.SaveStateOnShutdown; + bool save_state = allow_save_to_state && default_save_to_state; // Only confirm on UI thread because we need to display a msgbox. if (!m_is_closing && allow_confirm && !GSDumpReplayer::IsReplayingDump() && Host::GetBaseBoolSettingValue("UI", "ConfirmShutdown", true)) @@ -1081,7 +1083,7 @@ bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_sav void MainWindow::requestExit() { // this is block, because otherwise closeEvent() will also prompt - if (!requestShutdown(true, true, true)) + if (!requestShutdown(true, true, EmuConfig.SaveStateOnShutdown, true)) return; // We could use close here, but if we're not visible (e.g. quitting from fullscreen), closing the window @@ -1334,7 +1336,7 @@ void MainWindow::onViewGameGridActionTriggered() void MainWindow::onViewSystemDisplayTriggered() { - if (s_vm_valid) + if (m_display_created) switchToEmulationView(); } @@ -1643,7 +1645,7 @@ void MainWindow::showEvent(QShowEvent* event) void MainWindow::closeEvent(QCloseEvent* event) { - if (!requestShutdown(true, true, true)) + if (!requestShutdown(true, true, EmuConfig.SaveStateOnShutdown, true)) { event->ignore(); return; @@ -1742,6 +1744,8 @@ DisplayWidget* MainWindow::createDisplay(bool fullscreen, bool render_to_main) return nullptr; } + m_display_created = true; + if (is_exclusive_fullscreen) setDisplayFullscreen(fullscreen_mode); @@ -1750,6 +1754,8 @@ DisplayWidget* MainWindow::createDisplay(bool fullscreen, bool render_to_main) m_ui.actionViewSystemDisplay->setEnabled(true); m_ui.actionFullscreen->setEnabled(true); + m_ui.actionStartFullscreenUI->setEnabled(false); + m_ui.actionStartFullscreenUI2->setEnabled(false); m_display_widget->setShouldHideCursor(shouldHideMouseCursor()); m_display_widget->updateRelativeMode(s_vm_valid && !s_vm_paused); @@ -1940,9 +1946,12 @@ void MainWindow::destroyDisplay() { // Now we can safely destroy the display window. destroyDisplayWidget(true); + m_display_created = false; m_ui.actionViewSystemDisplay->setEnabled(false); m_ui.actionFullscreen->setEnabled(false); + m_ui.actionStartFullscreenUI->setEnabled(true); + m_ui.actionStartFullscreenUI2->setEnabled(true); } void MainWindow::destroyDisplayWidget(bool show_game_list) diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 5060a44573..38a8e45ff3 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -104,7 +104,7 @@ public Q_SLOTS: void reportError(const QString& title, const QString& message); bool confirmMessage(const QString& title, const QString& message); void runOnUIThread(const std::function& func); - bool requestShutdown(bool allow_confirm = true, bool allow_save_to_state = true, bool block_until_done = false); + bool requestShutdown(bool allow_confirm = true, bool allow_save_to_state = true, bool default_save_to_state = true, bool block_until_done = false); void requestExit(); void checkForSettingChanges(); @@ -258,6 +258,7 @@ private: QString m_current_game_name; quint32 m_current_game_crc; + bool m_display_created = false; bool m_save_states_invalidated = false; bool m_was_paused_on_surface_loss = false; bool m_was_disc_change_request = false; diff --git a/pcsx2-qt/MainWindow.ui b/pcsx2-qt/MainWindow.ui index c4b0956119..227f90a089 100644 --- a/pcsx2-qt/MainWindow.ui +++ b/pcsx2-qt/MainWindow.ui @@ -66,6 +66,7 @@ + @@ -225,6 +226,7 @@ + @@ -829,6 +831,24 @@ Enable Log Timestamps + + + + .. + + + Start Big Picture Mode + + + + + + .. + + + Big Picture + + diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index b17c6051ea..c38391fc60 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -82,6 +82,8 @@ static std::unique_ptr s_settings_save_timer; static std::unique_ptr s_base_settings_interface; static bool s_batch_mode = false; static bool s_nogui_mode = false; +static bool s_start_fullscreen_ui = false; +static bool s_start_fullscreen_ui_fullscreen = false; ////////////////////////////////////////////////////////////////////////// // Initialization/Shutdown @@ -617,6 +619,7 @@ bool QtHost::ParseCommandLineOptions(int argc, char* argv[], std::shared_ptrfullscreen = true; + s_start_fullscreen_ui_fullscreen = true; continue; } else if (CHECK_ARG("-nofullscreen")) @@ -629,6 +632,11 @@ bool QtHost::ParseCommandLineOptions(int argc, char* argv[], std::shared_ptrshow(); + // Initialize big picture mode if requested. + if (s_start_fullscreen_ui) + g_emu_thread->startFullscreenUI(s_start_fullscreen_ui_fullscreen); + // Skip the update check if we're booting a game directly. if (autoboot) g_emu_thread->startVM(std::move(autoboot)); - else + else if (!s_nogui_mode) main_window->startupUpdateCheck(); // This doesn't return until we exit. diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index cc71b4f8a3..a9e4d3f4b3 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -1063,7 +1063,9 @@ endif() if(PCSX2_CORE) list(APPEND pcsx2FrontendSources + Frontend/FullscreenUI.cpp Frontend/GameList.cpp + Frontend/ImGuiFullscreen.cpp Frontend/INISettingsInterface.cpp Frontend/InputManager.cpp Frontend/InputSource.cpp @@ -1074,7 +1076,9 @@ if(PCSX2_CORE) VMManager.cpp ) list(APPEND pcsx2FrontendHeaders + Frontend/FullscreenUI.h Frontend/GameList.h + Frontend/ImGuiFullscreen.h Frontend/INISettingsInterface.h Frontend/InputManager.h Frontend/InputSource.h diff --git a/pcsx2/Frontend/FullscreenUI.cpp b/pcsx2/Frontend/FullscreenUI.cpp new file mode 100644 index 0000000000..9634f2d857 --- /dev/null +++ b/pcsx2/Frontend/FullscreenUI.cpp @@ -0,0 +1,4384 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCSX2. + * If not, see . + */ + +#include "PrecompiledHeader.h" + +#define IMGUI_DEFINE_MATH_OPERATORS + +#include "Frontend/FullscreenUI.h" +#include "Frontend/ImGuiManager.h" +#include "Frontend/ImGuiFullscreen.h" +#include "Frontend/INISettingsInterface.h" +#include "Frontend/InputManager.h" +#include "Frontend/GameList.h" +#include "IconsFontAwesome5.h" + +#include "common/FileSystem.h" +#include "common/Console.h" +#include "common/Image.h" +#include "common/Path.h" +#include "common/SettingsInterface.h" +#include "common/SettingsWrapper.h" +#include "common/StringUtil.h" +#include "common/Timer.h" + +#include "CDVD/CDVDcommon.h" +#include "CDVD/CDVDdiscReader.h" +#include "GS.h" +#include "Host.h" +#include "HostDisplay.h" +#include "HostSettings.h" +#include "MemoryCardFile.h" +#include "PAD/Host/PAD.h" +#include "ps2/BiosTools.h" +#include "Sio.h" +#include "VMManager.h" + +#include "svnrev.h" + +#include "imgui.h" +#include "imgui_internal.h" + +#include "fmt/core.h" + +#include +#include +#include + +static constexpr s32 MAX_SAVE_STATE_SLOTS = 10; + +using ImGuiFullscreen::g_large_font; +using ImGuiFullscreen::g_layout_padding_left; +using ImGuiFullscreen::g_layout_padding_top; +using ImGuiFullscreen::g_medium_font; +using ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE; +using ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE; +using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT; +using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY; +using ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING; +using ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING; +using ImGuiFullscreen::LAYOUT_SCREEN_HEIGHT; +using ImGuiFullscreen::LAYOUT_SCREEN_WIDTH; +using ImGuiFullscreen::UIBackgroundColor; +using ImGuiFullscreen::UIBackgroundHighlightColor; +using ImGuiFullscreen::UIBackgroundLineColor; +using ImGuiFullscreen::UIBackgroundTextColor; +using ImGuiFullscreen::UIDisabledColor; +using ImGuiFullscreen::UIPrimaryColor; +using ImGuiFullscreen::UIPrimaryDarkColor; +using ImGuiFullscreen::UIPrimaryLightColor; +using ImGuiFullscreen::UIPrimaryLineColor; +using ImGuiFullscreen::UIPrimaryTextColor; +using ImGuiFullscreen::UISecondaryColor; +using ImGuiFullscreen::UISecondaryDarkColor; +using ImGuiFullscreen::UISecondaryLightColor; +using ImGuiFullscreen::UISecondaryTextColor; +using ImGuiFullscreen::UITextHighlightColor; + +using ImGuiFullscreen::ActiveButton; +using ImGuiFullscreen::AddNotification; +using ImGuiFullscreen::BeginFullscreenColumns; +using ImGuiFullscreen::BeginFullscreenColumnWindow; +using ImGuiFullscreen::BeginFullscreenWindow; +using ImGuiFullscreen::BeginMenuButtons; +using ImGuiFullscreen::BeginNavBar; +using ImGuiFullscreen::CenterImage; +using ImGuiFullscreen::CloseChoiceDialog; +using ImGuiFullscreen::CloseFileSelector; +using ImGuiFullscreen::DPIScale; +using ImGuiFullscreen::EndFullscreenColumns; +using ImGuiFullscreen::EndFullscreenColumnWindow; +using ImGuiFullscreen::EndFullscreenWindow; +using ImGuiFullscreen::EndMenuButtons; +using ImGuiFullscreen::EndNavBar; +using ImGuiFullscreen::EnumChoiceButton; +using ImGuiFullscreen::FloatingButton; +using ImGuiFullscreen::GetCachedTexture; +using ImGuiFullscreen::GetCachedTextureAsync; +using ImGuiFullscreen::GetPlaceholderTexture; +using ImGuiFullscreen::LayoutScale; +using ImGuiFullscreen::LoadTexture; +using ImGuiFullscreen::MenuButton; +using ImGuiFullscreen::MenuButtonFrame; +using ImGuiFullscreen::MenuButtonWithValue; +using ImGuiFullscreen::MenuHeading; +using ImGuiFullscreen::MenuHeadingButton; +using ImGuiFullscreen::MenuImageButton; +using ImGuiFullscreen::NavButton; +using ImGuiFullscreen::NavTitle; +using ImGuiFullscreen::OpenChoiceDialog; +using ImGuiFullscreen::OpenFileSelector; +using ImGuiFullscreen::OpenInputStringDialog; +using ImGuiFullscreen::PopPrimaryColor; +using ImGuiFullscreen::PushPrimaryColor; +using ImGuiFullscreen::QueueResetFocus; +using ImGuiFullscreen::RangeButton; +using ImGuiFullscreen::ResetFocusHere; +using ImGuiFullscreen::RightAlignNavButtons; +using ImGuiFullscreen::ShowToast; +using ImGuiFullscreen::ThreeWayToggleButton; +using ImGuiFullscreen::ToggleButton; +using ImGuiFullscreen::WantsToCloseMenu; + +namespace FullscreenUI +{ + enum class MainWindowType + { + None, + Landing, + GameList, + Settings, + PauseMenu + }; + + enum class PauseSubMenu + { + None, + Exit, + }; + + enum class SettingsPage + { + Summary, + Interface, + GameList, + BIOS, + Emulation, + System, + Graphics, + Audio, + MemoryCard, + Controller, + Hotkey, + Achievements, + Advanced, + GameFixes, + Count + }; + + ////////////////////////////////////////////////////////////////////////// + // Utility + ////////////////////////////////////////////////////////////////////////// + static std::string TimeToPrintableString(time_t t); + + ////////////////////////////////////////////////////////////////////////// + // Main + ////////////////////////////////////////////////////////////////////////// + static void UpdateForcedVsync(bool should_force); + static void UpdateGameDetails(std::string path, std::string serial, std::string title, u32 crc); + static void PauseForMenuOpen(); + static void ClosePauseMenu(); + static void OpenPauseSubMenu(PauseSubMenu submenu); + static void ReturnToMainWindow(); + static void DrawLandingWindow(); + static void DrawPauseMenu(MainWindowType type); + static void ExitFullscreenAndOpenURL(const std::string_view& url); + static void CopyTextToClipboard(std::string title, const std::string_view& text); + static void DrawAboutWindow(); + static void OpenAboutWindow(); + + static MainWindowType s_current_main_window = MainWindowType::None; + static PauseSubMenu s_current_pause_submenu = PauseSubMenu::None; + static bool s_initialized = false; + static bool s_tried_to_initialize = false; + static bool s_pause_menu_was_open = false; + static bool s_was_paused_on_quick_menu_open = false; + static bool s_about_window_open = false; + + // local copies of the currently-running game + static std::string s_current_game_title; + static std::string s_current_game_subtitle; + static std::string s_current_game_serial; + static std::string s_current_game_path; + static u32 s_current_game_crc; + + ////////////////////////////////////////////////////////////////////////// + // Resources + ////////////////////////////////////////////////////////////////////////// + static bool LoadResources(); + static void DestroyResources(); + + static std::shared_ptr s_app_icon_texture; + static std::array, static_cast(GameDatabaseSchema::Compatibility::Perfect)> + s_game_compatibility_textures; + static std::shared_ptr s_fallback_disc_texture; + static std::shared_ptr s_fallback_exe_texture; + static std::vector> s_cleanup_textures; + + ////////////////////////////////////////////////////////////////////////// + // Landing + ////////////////////////////////////////////////////////////////////////// + static void SwitchToLanding(); + static ImGuiFullscreen::FileSelectorFilters GetDiscImageFilters(); + static void DoStartPath( + const std::string& path, std::optional state_index = std::nullopt, std::optional fast_boot = std::nullopt); + static void DoStartFile(); + static void DoStartBIOS(); + static void DoStartDisc(const std::string& drive); + static void DoStartDisc(); + static void DoToggleFrameLimit(); + static void DoToggleSoftwareRenderer(); + static void DoShutdown(bool save_state); + static void DoReset(); + static void DoChangeDiscFromFile(); + static void DoChangeDisc(); + static void DoRequestExit(); + static void DoToggleFullscreen(); + + ////////////////////////////////////////////////////////////////////////// + // Settings + ////////////////////////////////////////////////////////////////////////// + + static constexpr double INPUT_BINDING_TIMEOUT_SECONDS = 5.0; + static constexpr u32 NUM_MEMORY_CARD_PORTS = 2; + + static void SwitchToSettings(); + static void SwitchToGameSettings(); + static void SwitchToGameSettings(const std::string& path); + static void SwitchToGameSettings(const GameList::Entry* entry); + static void SwitchToGameSettings(const std::string_view& serial, u32 crc); + static void DrawSettingsWindow(); + static void DrawSummarySettingsPage(); + static void DrawInterfaceSettingsPage(); + static void DrawGameListSettingsPage(); + static void DrawBIOSSettingsPage(); + static void DrawEmulationSettingsPage(); + static void DrawSystemSettingsPage(); + static void DrawGraphicsSettingsPage(); + static void DrawAudioSettingsPage(); + static void DrawMemoryCardSettingsPage(); + static void DrawCreateMemoryCardWindow(); + static void DrawControllerSettingsPage(); + static void DrawHotkeySettingsPage(); + static void DrawAchievementsSettingsPage(); + static void DrawAdvancedSettingsPage(); + static void DrawGameFixesSettingsPage(); + + static bool IsEditingGameSettings(SettingsInterface* bsi); + static SettingsInterface* GetEditingSettingsInterface(); + static SettingsInterface* GetEditingSettingsInterface(bool game_settings); + static void SetSettingsChanged(SettingsInterface* bsi); + static bool GetEffectiveBoolSetting(SettingsInterface* bsi, const char* section, const char* key, bool default_value); + static s32 GetEffectiveIntSetting(SettingsInterface* bsi, const char* section, const char* key, s32 default_value); + static void DoCopyGameSettings(); + static void DoClearGameSettings(); + static void CopyGlobalControllerSettingsToGame(); + static void ResetControllerSettings(); + static void DoLoadInputProfile(); + static void DoSaveInputProfile(); + static void DoSaveInputProfile(const std::string& name); + + static bool DrawToggleSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + bool default_value, bool enabled = true, bool allow_tristate = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + static void DrawIntListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + int default_value, const char* const* options, size_t option_count, int option_offset = 0, bool enabled = true, + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + static void DrawIntRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + int default_value, int min_value, int max_value, const char* format = "%d", bool enabled = true, + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + static void DrawFloatRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + float default_value, float min_value, float max_value, const char* format = "%f", float multiplier = 1.0f, bool enabled = true, + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + static void DrawIntRectSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* left_key, int default_left, const char* top_key, int default_top, const char* right_key, int default_right, + const char* bottom_key, int default_bottom, int min_value, int max_value, const char* format = "%d", bool enabled = true, + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + static void DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + const char* default_value, const char* const* options, const char* const* option_values, size_t option_count, bool enabled = true, + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + static void DrawFloatListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + float default_value, const char* const* options, const float* option_values, size_t option_count, bool enabled = true, + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + static void DrawFolderSetting(SettingsInterface* bsi, const char* title, const char* section, const char* key, + const std::string& runtime_var, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, + ImFont* summary_font = g_medium_font); + static void DrawClampingModeSetting(SettingsInterface* bsi, const char* title, const char* summary, bool vu); + static void PopulateGraphicsAdapterList(); + static void PopulateGameListDirectoryCache(SettingsInterface* si); + static ImGuiFullscreen::ChoiceDialogOptions GetGameListDirectoryOptions(bool recursive_as_checked); + static void BeginInputBinding(SettingsInterface* bsi, PAD::ControllerBindingType type, const std::string_view& section, + const std::string_view& key, const std::string_view& display_name); + static void DrawInputBindingWindow(); + static void DrawInputBindingButton(SettingsInterface* bsi, PAD::ControllerBindingType type, const char* section, const char* name, + const char* display_name, bool show_type = true); + static void ClearInputBindingVariables(); + static void StartAutomaticBinding(u32 port); + + static SettingsPage s_settings_page = SettingsPage::Interface; + static std::unique_ptr s_game_settings_interface; + static std::unique_ptr s_game_settings_entry; + static std::vector> s_game_list_directories_cache; + static std::vector s_graphics_adapter_list_cache; + static std::vector s_fullscreen_mode_list_cache; + static std::vector s_hotkey_list_cache; + static std::atomic_bool s_settings_changed{false}; + static std::atomic_bool s_game_settings_changed{false}; + static PAD::ControllerBindingType s_input_binding_type = PAD::ControllerBindingType::Unknown; + static std::string s_input_binding_section; + static std::string s_input_binding_key; + static std::string s_input_binding_display_name; + static std::vector s_input_binding_new_bindings; + static Common::Timer s_input_binding_timer; + + ////////////////////////////////////////////////////////////////////////// + // Save State List + ////////////////////////////////////////////////////////////////////////// + struct SaveStateListEntry + { + std::string title; + std::string summary; + std::string path; + std::unique_ptr preview_texture; + s32 slot; + }; + + static void InitializePlaceholderSaveStateListEntry( + SaveStateListEntry* li, const std::string& title, const std::string& serial, u32 crc, s32 slot); + static bool InitializeSaveStateListEntry( + SaveStateListEntry* li, const std::string& title, const std::string& serial, u32 crc, s32 slot); + static void ClearSaveStateEntryList(); + static u32 PopulateSaveStateListEntries(const std::string& title, const std::string& serial, u32 crc); + static bool OpenLoadStateSelectorForGame(const std::string& game_path); + static bool OpenSaveStateSelector(bool is_loading); + static void CloseSaveStateSelector(); + static void DrawSaveStateSelector(bool is_loading, bool fullscreen); + static void DoLoadState(std::string path); + + static std::vector s_save_state_selector_slots; + static std::string s_save_state_selector_game_path; + static bool s_save_state_selector_open = false; + static bool s_save_state_selector_loading = true; + + ////////////////////////////////////////////////////////////////////////// + // Game List + ////////////////////////////////////////////////////////////////////////// + static void DrawGameListWindow(); + static void SwitchToGameList(); + static void PopulateGameListEntryList(); + static HostDisplayTexture* GetTextureForGameListEntryType(GameList::EntryType type); + static HostDisplayTexture* GetGameListCover(const GameList::Entry* entry); + static HostDisplayTexture* GetCoverForCurrentGame(); + static std::string GetNotificationImageForGame(const GameList::Entry* entry); + static std::string GetNotificationImageForGame(const std::string& game_path); + + // Lazily populated cover images. + static std::unordered_map s_cover_image_map; + static std::vector s_game_list_sorted_entries; +} // namespace FullscreenUI + +////////////////////////////////////////////////////////////////////////// +// Utility +////////////////////////////////////////////////////////////////////////// + +std::string FullscreenUI::TimeToPrintableString(time_t t) +{ + struct tm lt = {}; +#ifdef _MSC_VER + localtime_s(<, &t); +#else + localtime_r(&t, <); +#endif + + char buf[256]; + std::strftime(buf, sizeof(buf), "%c", <); + return std::string(buf); +} + +////////////////////////////////////////////////////////////////////////// +// Main +////////////////////////////////////////////////////////////////////////// + +bool FullscreenUI::Initialize() +{ + if (s_initialized) + return true; + + if (s_tried_to_initialize) + return false; + + ImGuiFullscreen::SetTheme(); + ImGuiFullscreen::UpdateLayoutScale(); + + if (!ImGuiManager::AddFullscreenFontsIfMissing() || !ImGuiFullscreen::Initialize("fullscreenui/placeholder.png") || !LoadResources()) + { + DestroyResources(); + ImGuiFullscreen::Shutdown(); + s_tried_to_initialize = true; + return false; + } + + s_initialized = true; + s_current_main_window = MainWindowType::None; + s_current_pause_submenu = PauseSubMenu::None; + s_pause_menu_was_open = false; + s_was_paused_on_quick_menu_open = false; + s_about_window_open = false; + s_hotkey_list_cache = InputManager::GetHotkeyList(); + GetMTGS().SetRunIdle(true); + + if (VMManager::HasValidVM()) + { + UpdateGameDetails(VMManager::GetDiscPath(), VMManager::GetGameSerial(), VMManager::GetGameName(), VMManager::GetGameCRC()); + } + else + { + SwitchToLanding(); + } + + // force vsync on so we don't run at thousands of fps + // Initialize is called on the GS thread, so we can access the display directly. + UpdateForcedVsync(VMManager::GetState() != VMState::Running); + + return true; +} + +bool FullscreenUI::IsInitialized() +{ + return s_initialized; +} + +bool FullscreenUI::HasActiveWindow() +{ + return s_current_main_window != MainWindowType::None || s_save_state_selector_open || ImGuiFullscreen::IsChoiceDialogOpen() || + ImGuiFullscreen::IsFileSelectorOpen(); +} + +void FullscreenUI::UpdateForcedVsync(bool should_force) +{ + // force vsync on so we don't run at thousands of fps + const VsyncMode mode = EmuConfig.GetEffectiveVsyncMode(); + + // toss it through regardless of the mode, because options can change it + Host::GetHostDisplay()->SetVSync((should_force && mode == VsyncMode::Off) ? VsyncMode::On : mode); +} + +void FullscreenUI::OnVMStarted() +{ + if (!IsInitialized()) + return; + + GetMTGS().RunOnGSThread([]() { + if (!IsInitialized()) + return; + + s_current_main_window = MainWindowType::None; + QueueResetFocus(); + }); +} + +void FullscreenUI::OnVMPaused() +{ + if (!IsInitialized()) + return; + + GetMTGS().RunOnGSThread([]() { + if (!IsInitialized()) + return; + + UpdateForcedVsync(true); + }); +} + +void FullscreenUI::OnVMResumed() +{ + if (!IsInitialized()) + return; + + GetMTGS().RunOnGSThread([]() { + if (!IsInitialized()) + return; + + UpdateForcedVsync(false); + }); +} + +void FullscreenUI::OnVMDestroyed() +{ + if (!IsInitialized()) + return; + + GetMTGS().RunOnGSThread([]() { + if (!IsInitialized()) + return; + + s_pause_menu_was_open = false; + SwitchToLanding(); + UpdateForcedVsync(true); + }); +} + +void FullscreenUI::OnRunningGameChanged(std::string path, std::string serial, std::string title, u32 crc) +{ + if (!IsInitialized()) + return; + + GetMTGS().RunOnGSThread([path = std::move(path), serial = std::move(serial), title = std::move(title), crc]() { + if (!IsInitialized()) + return; + + UpdateGameDetails(std::move(path), std::move(serial), std::move(title), crc); + }); +} + +void FullscreenUI::UpdateGameDetails(std::string path, std::string serial, std::string title, u32 crc) +{ + if (!serial.empty()) + s_current_game_subtitle = fmt::format("{0} - {1}", serial, Path::GetFileName(path)); + else + s_current_game_subtitle = {}; + + s_current_game_title = std::move(title); + s_current_game_serial = std::move(serial); + s_current_game_path = std::move(path); + s_current_game_crc = crc; +} + +void FullscreenUI::PauseForMenuOpen() +{ + s_was_paused_on_quick_menu_open = (VMManager::GetState() == VMState::Paused); + if (Host::GetBoolSettingValue("UI", "PauseOnMenu", true) && !s_was_paused_on_quick_menu_open) + Host::RunOnCPUThread([]() { VMManager::SetPaused(true); }); + + s_pause_menu_was_open = true; +} + +void FullscreenUI::OpenPauseMenu() +{ + if (!VMManager::HasValidVM()) + return; + + GetMTGS().RunOnGSThread([]() { + if (!Initialize() || s_current_main_window != MainWindowType::None) + return; + + PauseForMenuOpen(); + s_current_main_window = MainWindowType::PauseMenu; + s_current_pause_submenu = PauseSubMenu::None; + QueueResetFocus(); + }); +} + +void FullscreenUI::ClosePauseMenu() +{ + if (!IsInitialized() || !VMManager::HasValidVM()) + return; + + if (VMManager::GetState() == VMState::Paused && !s_was_paused_on_quick_menu_open) + Host::RunOnCPUThread([]() { VMManager::SetPaused(false); }); + + s_current_main_window = MainWindowType::None; + s_current_pause_submenu = PauseSubMenu::None; + s_pause_menu_was_open = false; + QueueResetFocus(); +} + +void FullscreenUI::OpenPauseSubMenu(PauseSubMenu submenu) +{ + s_current_main_window = MainWindowType::PauseMenu; + s_current_pause_submenu = submenu; + QueueResetFocus(); +} + +void FullscreenUI::Shutdown() +{ + CloseSaveStateSelector(); + s_cover_image_map.clear(); + s_game_list_sorted_entries = {}; + s_game_list_directories_cache = {}; + s_fullscreen_mode_list_cache = {}; + s_graphics_adapter_list_cache = {}; + s_hotkey_list_cache = {}; + s_current_game_title = {}; + s_current_game_subtitle = {}; + s_current_game_serial = {}; + s_current_game_path = {}; + s_current_game_crc = 0; + DestroyResources(); + ImGuiFullscreen::Shutdown(); + s_initialized = false; + s_tried_to_initialize = false; +} + +void FullscreenUI::Render() +{ + if (!s_initialized) + return; + + for (std::unique_ptr& tex : s_cleanup_textures) + tex.reset(); + s_cleanup_textures.clear(); + ImGuiFullscreen::UploadAsyncTextures(); + + ImGuiFullscreen::BeginLayout(); + + switch (s_current_main_window) + { + case MainWindowType::Landing: + DrawLandingWindow(); + break; + case MainWindowType::GameList: + DrawGameListWindow(); + break; + case MainWindowType::Settings: + DrawSettingsWindow(); + break; + case MainWindowType::PauseMenu: + DrawPauseMenu(s_current_main_window); + break; + default: + break; + } + + if (s_save_state_selector_open) + DrawSaveStateSelector(s_save_state_selector_loading, false); + + if (s_about_window_open) + DrawAboutWindow(); + + if (s_input_binding_type != PAD::ControllerBindingType::Unknown) + DrawInputBindingWindow(); + + ImGuiFullscreen::EndLayout(); + + if (s_settings_changed.load(std::memory_order_relaxed)) + { + Host::CommitBaseSettingChanges(); + Host::RunOnCPUThread([]() { VMManager::ApplySettings(); }); + s_settings_changed.store(false, std::memory_order_release); + } + if (s_game_settings_changed.load(std::memory_order_relaxed)) + { + if (s_game_settings_interface) + { + s_game_settings_interface->Save(); + if (VMManager::HasValidVM()) + Host::RunOnCPUThread([]() { VMManager::ReloadGameSettings(); }); + } + s_game_settings_changed.store(false, std::memory_order_release); + } + + ImGuiFullscreen::ResetCloseMenuIfNeeded(); +} + +void FullscreenUI::ReturnToMainWindow() +{ + if (s_pause_menu_was_open) + ClosePauseMenu(); + + s_current_main_window = VMManager::HasValidVM() ? MainWindowType::None : MainWindowType::Landing; +} + +bool FullscreenUI::LoadResources() +{ + s_app_icon_texture = LoadTexture("icons/AppIconLarge.png"); + + s_fallback_disc_texture = LoadTexture("fullscreenui/media-cdrom.png"); + s_fallback_exe_texture = LoadTexture("fullscreenui/applications-system.png"); + + for (u32 i = static_cast(GameDatabaseSchema::Compatibility::Nothing); + i <= static_cast(GameDatabaseSchema::Compatibility::Perfect); i++) + { + s_game_compatibility_textures[i - 1] = LoadTexture(fmt::format("icons/star-{}.png", i - 1).c_str()); + } + + return true; +} + +void FullscreenUI::DestroyResources() +{ + s_app_icon_texture.reset(); + s_fallback_exe_texture.reset(); + s_fallback_disc_texture.reset(); + for (auto& tex : s_game_compatibility_textures) + tex.reset(); + for (auto& tex : s_cleanup_textures) + tex.reset(); +} + +////////////////////////////////////////////////////////////////////////// +// Utility +////////////////////////////////////////////////////////////////////////// + +ImGuiFullscreen::FileSelectorFilters FullscreenUI::GetDiscImageFilters() +{ + return {"*.bin", "*.iso", "*.cue", "*.chd", "*.cso", "*.gz", "*.elf", "*.irx", "*.m3u", "*.gs", "*.gs.xz", "*.gs.zst"}; +} + +void FullscreenUI::DoStartPath(const std::string& path, std::optional state_index, std::optional fast_boot) +{ + VMBootParameters params; + params.filename = path; + params.state_index = state_index; + params.fast_boot = fast_boot; + + Host::RunOnCPUThread([params = std::move(params)]() { + if (VMManager::HasValidVM()) + return; + + if (VMManager::Initialize(params)) + VMManager::SetState(VMState::Running); + else + SwitchToLanding(); + }); + + // switch to nothing, we'll get brought back if init fails + s_current_main_window = MainWindowType::None; +} + +void FullscreenUI::DoStartFile() +{ + auto callback = [](const std::string& path) { + if (!path.empty()) + DoStartPath(path); + + QueueResetFocus(); + CloseFileSelector(); + }; + + OpenFileSelector(ICON_FA_FOLDER_OPEN " Select Disc Image", false, std::move(callback), GetDiscImageFilters()); +} + +void FullscreenUI::DoStartBIOS() +{ + Host::RunOnCPUThread([]() { + if (VMManager::HasValidVM()) + return; + + VMBootParameters params; + if (VMManager::Initialize(params)) + VMManager::SetState(VMState::Running); + else + SwitchToLanding(); + }); + + // switch to nothing, we'll get brought back if init fails + s_current_main_window = MainWindowType::None; +} + +void FullscreenUI::DoStartDisc(const std::string& drive) +{ + Host::RunOnCPUThread([drive]() { + if (VMManager::HasValidVM()) + return; + + VMBootParameters params; + params.filename = std::move(drive); + params.source_type = CDVD_SourceType::Disc; + if (VMManager::Initialize(params)) + VMManager::SetState(VMState::Running); + else + SwitchToLanding(); + }); +} + +void FullscreenUI::DoStartDisc() +{ + std::vector devices(GetOpticalDriveList()); + if (devices.empty()) + { + ShowToast(std::string(), + "Could not find any CD/DVD-ROM devices. Please ensure you have a drive connected and sufficient permissions to access it."); + return; + } + + // if there's only one, select it automatically + if (devices.size() == 1) + { + DoStartDisc(devices.front()); + return; + } + + ImGuiFullscreen::ChoiceDialogOptions options; + for (std::string& drive : devices) + options.emplace_back(std::move(drive), false); + OpenChoiceDialog(ICON_FA_COMPACT_DISC " Select Disc Drive", false, std::move(options), [](s32, const std::string& path, bool) { + DoStartDisc(path); + CloseChoiceDialog(); + QueueResetFocus(); + }); +} + +void FullscreenUI::DoToggleFrameLimit() +{ + Host::RunOnCPUThread([]() { + if (!VMManager::HasValidVM()) + return; + + VMManager::SetLimiterMode( + (EmuConfig.LimiterMode != LimiterModeType::Unlimited) ? LimiterModeType::Unlimited : LimiterModeType::Nominal); + }); +} + +void FullscreenUI::DoToggleSoftwareRenderer() +{ + Host::RunOnCPUThread([]() { + if (!VMManager::HasValidVM()) + return; + + GetMTGS().ToggleSoftwareRendering(); + }); +} + +void FullscreenUI::DoShutdown(bool save_state) +{ + Host::RunOnCPUThread([save_state]() { Host::RequestVMShutdown(false, save_state, save_state); }); +} + +void FullscreenUI::DoReset() +{ + Host::RunOnCPUThread([]() { + if (!VMManager::HasValidVM()) + return; + + VMManager::Reset(); + }); +} + +void FullscreenUI::DoChangeDiscFromFile() +{ + auto callback = [](const std::string& path) { + if (!path.empty()) + { + if (!GameList::IsScannableFilename(path)) + { + ShowToast({}, fmt::format("{} is not a valid disc image.", FileSystem::GetDisplayNameFromPath(path))); + } + else + { + Host::RunOnCPUThread([path]() { VMManager::ChangeDisc(CDVD_SourceType::Iso, std::move(path)); }); + } + } + + QueueResetFocus(); + CloseFileSelector(); + ReturnToMainWindow(); + }; + + OpenFileSelector(ICON_FA_COMPACT_DISC " Select Disc Image", false, std::move(callback), GetDiscImageFilters(), + std::string(Path::GetDirectory(s_current_game_path))); +} + +void FullscreenUI::DoChangeDisc() +{ + DoChangeDiscFromFile(); +} + +void FullscreenUI::DoRequestExit() +{ + Host::RunOnCPUThread([]() { Host::RequestExit(EmuConfig.SaveStateOnShutdown); }); +} + +void FullscreenUI::DoToggleFullscreen() +{ + Host::RunOnCPUThread([]() { Host::SetFullscreen(!Host::IsFullscreen()); }); +} + +////////////////////////////////////////////////////////////////////////// +// Landing Window +////////////////////////////////////////////////////////////////////////// + +void FullscreenUI::SwitchToLanding() +{ + s_current_main_window = MainWindowType::Landing; + QueueResetFocus(); +} + +void FullscreenUI::DrawLandingWindow() +{ + BeginFullscreenColumns(); + + if (BeginFullscreenColumnWindow(0.0f, 570.0f, "logo", UIPrimaryDarkColor)) + { + const float image_size = LayoutScale(380.f); + ImGui::SetCursorPos( + ImVec2((ImGui::GetWindowWidth() * 0.5f) - (image_size * 0.5f), (ImGui::GetWindowHeight() * 0.5f) - (image_size * 0.5f))); + ImGui::Image(s_app_icon_texture->GetHandle(), ImVec2(image_size, image_size)); + + ImGui::SetCursorPos(ImVec2(LayoutScale(20.0f), ImGui::GetWindowHeight() - g_medium_font->FontSize - LayoutScale(20.0f))); + ImGui::PushFont(g_medium_font); + ImGui::Text(GIT_REV); + ImGui::PopFont(); + } + EndFullscreenColumnWindow(); + + if (BeginFullscreenColumnWindow(570.0f, LAYOUT_SCREEN_WIDTH, "menu", UIBackgroundColor)) + { + ResetFocusHere(); + + BeginMenuButtons(6, 0.5f); + + if (MenuButton(" " ICON_FA_LIST " Game List", "Launch a game from images scanned from your game directories.")) + { + SwitchToGameList(); + } + + if (MenuButton(" " ICON_FA_FOLDER_OPEN " Start File", "Launch a game by selecting a file/disc image.")) + { + DoStartFile(); + } + + if (MenuButton(" " ICON_FA_TOOLBOX " Start BIOS", "Start the console without any disc inserted.")) + { + DoStartBIOS(); + } + + if (MenuButton(" " ICON_FA_COMPACT_DISC " Start Disc", "Start a game from a disc in your PC's DVD drive.")) + { + DoStartDisc(); + } + + if (MenuButton(" " ICON_FA_SLIDERS_H " Settings", "Change settings for the emulator.")) + SwitchToSettings(); + + if (MenuButton(" " ICON_FA_SIGN_OUT_ALT " Exit", "Exits the program.")) + { + DoRequestExit(); + } + + { + ImVec2 fullscreen_pos; + if (FloatingButton(ICON_FA_WINDOW_CLOSE, 0.0f, 0.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font, &fullscreen_pos)) + { + DoRequestExit(); + } + + if (FloatingButton(ICON_FA_EXPAND, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f, true, g_large_font, &fullscreen_pos)) + { + DoToggleFullscreen(); + } + + if (FloatingButton(ICON_FA_QUESTION_CIRCLE, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f)) + OpenAboutWindow(); + } + + EndMenuButtons(); + } + + EndFullscreenColumnWindow(); + + EndFullscreenColumns(); +} + +bool FullscreenUI::IsEditingGameSettings(SettingsInterface* bsi) +{ + return (bsi == s_game_settings_interface.get()); +} + +SettingsInterface* FullscreenUI::GetEditingSettingsInterface() +{ + return s_game_settings_interface ? s_game_settings_interface.get() : Host::Internal::GetBaseSettingsLayer(); +} + +SettingsInterface* FullscreenUI::GetEditingSettingsInterface(bool game_settings) +{ + return (game_settings && s_game_settings_interface) ? s_game_settings_interface.get() : Host::Internal::GetBaseSettingsLayer(); +} + +void FullscreenUI::SetSettingsChanged(SettingsInterface* bsi) +{ + if (bsi && bsi == s_game_settings_interface.get()) + s_game_settings_changed.store(true, std::memory_order_release); + else + s_settings_changed.store(true, std::memory_order_release); +} + +bool FullscreenUI::GetEffectiveBoolSetting(SettingsInterface* bsi, const char* section, const char* key, bool default_value) +{ + if (IsEditingGameSettings(bsi)) + { + std::optional value = bsi->GetOptionalBoolValue(section, key, std::nullopt); + if (value.has_value()) + return value.value(); + } + + return Host::Internal::GetBaseSettingsLayer()->GetBoolValue(section, key, default_value); +} + +s32 FullscreenUI::GetEffectiveIntSetting(SettingsInterface* bsi, const char* section, const char* key, s32 default_value) +{ + if (IsEditingGameSettings(bsi)) + { + std::optional value = bsi->GetOptionalIntValue(section, key, std::nullopt); + if (value.has_value()) + return value.value(); + } + + return Host::Internal::GetBaseSettingsLayer()->GetIntValue(section, key, default_value); +} + +void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, PAD::ControllerBindingType type, const char* section, const char* name, + const char* display_name, bool show_type) +{ + std::string title(fmt::format("{}/{}", section, name)); + + ImRect bb; + bool visible, hovered, clicked; + clicked = MenuButtonFrame(title.c_str(), true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max); + if (!visible) + return; + + const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max); + + if (show_type) + { + switch (type) + { + case PAD::ControllerBindingType::Button: + title = fmt::format(ICON_FA_DOT_CIRCLE " {}", display_name); + break; + case PAD::ControllerBindingType::Axis: + case PAD::ControllerBindingType::HalfAxis: + title = fmt::format(ICON_FA_BULLSEYE " {}", display_name); + break; + case PAD::ControllerBindingType::Motor: + title = fmt::format(ICON_FA_BELL " {}", display_name); + break; + case PAD::ControllerBindingType::Macro: + title = fmt::format(ICON_FA_PIZZA_SLICE " {}", display_name); + break; + default: + title = display_name; + break; + } + } + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped( + title_bb.Min, title_bb.Max, show_type ? title.c_str() : display_name, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + const std::string value(bsi->GetStringValue(section, name)); + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped( + summary_bb.Min, summary_bb.Max, value.empty() ? "No Binding" : value.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + + if (clicked) + { + BeginInputBinding(bsi, type, section, name, display_name); + } + else if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) + { + bsi->DeleteValue(section, name); + SetSettingsChanged(bsi); + } +} + +void FullscreenUI::ClearInputBindingVariables() +{ + s_input_binding_type = PAD::ControllerBindingType::Unknown; + s_input_binding_section = {}; + s_input_binding_key = {}; + s_input_binding_display_name = {}; + s_input_binding_new_bindings = {}; +} + +void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, PAD::ControllerBindingType type, const std::string_view& section, + const std::string_view& key, const std::string_view& display_name) +{ + if (s_input_binding_type != PAD::ControllerBindingType::Unknown) + { + InputManager::RemoveHook(); + ClearInputBindingVariables(); + } + + s_input_binding_type = type; + s_input_binding_section = section; + s_input_binding_key = key; + s_input_binding_display_name = display_name; + s_input_binding_new_bindings = {}; + s_input_binding_timer.Reset(); + + const bool game_settings = IsEditingGameSettings(bsi); + + InputManager::SetHook([game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult { + // holding the settings lock here will protect the input binding list + auto lock = Host::GetSettingsLock(); + + const float abs_value = std::abs(value); + + for (InputBindingKey other_key : s_input_binding_new_bindings) + { + if (other_key.MaskDirection() == key.MaskDirection()) + { + if (abs_value < 0.5f) + { + // if this key is in our new binding list, it's a "release", and we're done + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + const std::string new_binding(InputManager::ConvertInputBindingKeysToString( + s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size())); + bsi->SetStringValue(s_input_binding_section.c_str(), s_input_binding_key.c_str(), new_binding.c_str()); + SetSettingsChanged(bsi); + ClearInputBindingVariables(); + return InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent; + } + + // otherwise, keep waiting + return InputInterceptHook::CallbackResult::StopProcessingEvent; + } + } + + // new binding, add it to the list, but wait for a decent distance first, and then wait for release + if (abs_value >= 0.5f) + { + InputBindingKey key_to_add = key; + key_to_add.negative = (value < 0.0f); + s_input_binding_new_bindings.push_back(key_to_add); + } + + return InputInterceptHook::CallbackResult::StopProcessingEvent; + }); +} + +void FullscreenUI::DrawInputBindingWindow() +{ + pxAssert(s_input_binding_type != PAD::ControllerBindingType::Unknown); + + const double time_remaining = INPUT_BINDING_TIMEOUT_SECONDS - s_input_binding_timer.GetTimeSeconds(); + if (time_remaining <= 0.0) + { + InputManager::RemoveHook(); + ClearInputBindingVariables(); + return; + } + + const char* title = ICON_FA_GAMEPAD " Set Input Binding"; + ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(title); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + if (ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs)) + { + ImGui::TextWrapped("Setting %s binding %s.", s_input_binding_section.c_str(), s_input_binding_display_name.c_str()); + ImGui::TextUnformatted("Push a controller button or axis now."); + ImGui::NewLine(); + ImGui::Text("Timing out in %.0f seconds...", time_remaining); + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(3); + ImGui::PopFont(); +} + +bool FullscreenUI::DrawToggleSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + bool default_value, bool enabled, bool allow_tristate, float height, ImFont* font, ImFont* summary_font) +{ + if (!allow_tristate || !IsEditingGameSettings(bsi)) + { + bool value = bsi->GetBoolValue(section, key, default_value); + if (!ToggleButton(title, summary, &value, enabled, height, font, summary_font)) + return false; + + bsi->SetBoolValue(section, key, value); + } + else + { + std::optional value(false); + if (!bsi->GetBoolValue(section, key, &value.value())) + value.reset(); + if (!ThreeWayToggleButton(title, summary, &value, enabled, height, font, summary_font)) + return false; + + if (value.has_value()) + bsi->SetBoolValue(section, key, value.value()); + else + bsi->DeleteValue(section, key); + } + + SetSettingsChanged(bsi); + return true; +} + +void FullscreenUI::DrawIntListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + int default_value, const char* const* options, size_t option_count, int option_offset, bool enabled, float height, ImFont* font, + ImFont* summary_font) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional value = + bsi->GetOptionalIntValue(section, key, game_settings ? std::nullopt : std::optional(default_value)); + const int index = value.has_value() ? (value.value() - option_offset) : std::numeric_limits::min(); + const char* value_text = (value.has_value()) ? + ((index < 0 || static_cast(index) >= option_count) ? "Unknown" : options[index]) : + "Use Global Setting"; + + if (MenuButtonWithValue(title, summary, value_text, enabled, height, font, summary_font)) + { + ImGuiFullscreen::ChoiceDialogOptions cd_options; + cd_options.reserve(option_count + 1); + if (game_settings) + cd_options.emplace_back("Use Global Setting", !value.has_value()); + for (size_t i = 0; i < option_count; i++) + cd_options.emplace_back(options[i], (i == static_cast(index))); + OpenChoiceDialog(title, false, std::move(cd_options), + [game_settings, section, key, option_offset](s32 index, const std::string& title, bool checked) { + if (index >= 0) + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + if (game_settings) + { + if (index == 0) + bsi->DeleteValue(section, key); + else + bsi->SetIntValue(section, key, index - 1 + option_offset); + } + else + { + bsi->SetIntValue(section, key, index + option_offset); + } + + SetSettingsChanged(bsi); + } + + CloseChoiceDialog(); + }); + } +} + +void FullscreenUI::DrawIntRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, const char* key, + int default_value, int min_value, int max_value, const char* format, bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional value = + bsi->GetOptionalIntValue(section, key, game_settings ? std::nullopt : std::optional(default_value)); + const std::string value_text( + value.has_value() ? StringUtil::StdStringFromFormat(format, value.value()) : std::string("Use Global Setting")); + + if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) + ImGui::OpenPopup(title); + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f)); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + if (ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::SetNextItemWidth(LayoutScale(450.0f)); + s32 dlg_value = static_cast(value.value_or(default_value)); + if (ImGui::SliderInt("##value", &dlg_value, min_value, max_value, format, ImGuiSliderFlags_NoInput)) + { + if (IsEditingGameSettings(bsi) && dlg_value == default_value) + bsi->DeleteValue(section, key); + else + bsi->SetIntValue(section, key, dlg_value); + + SetSettingsChanged(bsi); + } + + BeginMenuButtons(); + if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::CloseCurrentPopup(); + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(3); + ImGui::PopFont(); +} + +void FullscreenUI::DrawFloatRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* key, float default_value, float min_value, float max_value, const char* format, float multiplier, bool enabled, + float height, ImFont* font, ImFont* summary_font) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional value = + bsi->GetOptionalFloatValue(section, key, game_settings ? std::nullopt : std::optional(default_value)); + const std::string value_text( + value.has_value() ? StringUtil::StdStringFromFormat(format, value.value() * multiplier) : std::string("Use Global Setting")); + + if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) + ImGui::OpenPopup(title); + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f)); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + if (ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::SetNextItemWidth(LayoutScale(450.0f)); + float dlg_value = value.value_or(default_value) * multiplier; + if (ImGui::SliderFloat("##value", &dlg_value, min_value, max_value, format, ImGuiSliderFlags_NoInput)) + { + dlg_value /= multiplier; + + if (IsEditingGameSettings(bsi) && dlg_value == default_value) + bsi->DeleteValue(section, key); + else + bsi->SetFloatValue(section, key, dlg_value); + + SetSettingsChanged(bsi); + } + + BeginMenuButtons(); + if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::CloseCurrentPopup(); + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(3); + ImGui::PopFont(); +} + +void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* left_key, int default_left, const char* top_key, int default_top, const char* right_key, int default_right, + const char* bottom_key, int default_bottom, int min_value, int max_value, const char* format, bool enabled, float height, ImFont* font, + ImFont* summary_font) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional left_value = + bsi->GetOptionalIntValue(section, left_key, game_settings ? std::nullopt : std::optional(default_left)); + const std::optional top_value = + bsi->GetOptionalIntValue(section, top_key, game_settings ? std::nullopt : std::optional(default_top)); + const std::optional right_value = + bsi->GetOptionalIntValue(section, right_key, game_settings ? std::nullopt : std::optional(default_right)); + const std::optional bottom_value = + bsi->GetOptionalIntValue(section, bottom_key, game_settings ? std::nullopt : std::optional(default_bottom)); + const std::string value_text(fmt::format("{}/{}/{}/{}", + left_value.has_value() ? StringUtil::StdStringFromFormat(format, left_value.value()) : std::string("Default"), + top_value.has_value() ? StringUtil::StdStringFromFormat(format, top_value.value()) : std::string("Default"), + right_value.has_value() ? StringUtil::StdStringFromFormat(format, right_value.value()) : std::string("Default"), + bottom_value.has_value() ? StringUtil::StdStringFromFormat(format, bottom_value.value()) : std::string("Default"))); + + if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) + ImGui::OpenPopup(title); + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 320.0f)); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + if (ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + s32 dlg_left_value = static_cast(left_value.value_or(default_left)); + s32 dlg_top_value = static_cast(top_value.value_or(default_top)); + s32 dlg_right_value = static_cast(right_value.value_or(default_right)); + s32 dlg_bottom_value = static_cast(bottom_value.value_or(default_bottom)); + + ImGui::TextUnformatted("Left: "); + ImGui::SameLine(); + const bool left_modified = ImGui::SliderInt("##left", &dlg_left_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::TextUnformatted("Top: "); + ImGui::SameLine(); + const bool top_modified = ImGui::SliderInt("##top", &dlg_top_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::TextUnformatted("Right: "); + ImGui::SameLine(); + const bool right_modified = ImGui::SliderInt("##right", &dlg_right_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::TextUnformatted("Bottom: "); + ImGui::SameLine(); + const bool bottom_modified = + ImGui::SliderInt("##bottom", &dlg_bottom_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + if (left_modified) + { + if (IsEditingGameSettings(bsi) && dlg_left_value == default_left) + bsi->DeleteValue(section, left_key); + else + bsi->SetIntValue(section, left_key, dlg_left_value); + } + if (top_modified) + { + if (IsEditingGameSettings(bsi) && dlg_top_value == default_top) + bsi->DeleteValue(section, top_key); + else + bsi->SetIntValue(section, top_key, dlg_top_value); + } + if (right_modified) + { + if (IsEditingGameSettings(bsi) && dlg_right_value == default_right) + bsi->DeleteValue(section, right_key); + else + bsi->SetIntValue(section, right_key, dlg_right_value); + } + if (bottom_modified) + { + if (IsEditingGameSettings(bsi) && dlg_bottom_value == default_bottom) + bsi->DeleteValue(section, bottom_key); + else + bsi->SetIntValue(section, bottom_key, dlg_bottom_value); + } + + if (left_modified || top_modified || right_modified || bottom_modified) + SetSettingsChanged(bsi); + + BeginMenuButtons(); + if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::CloseCurrentPopup(); + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(3); + ImGui::PopFont(); +} + +void FullscreenUI::DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* key, const char* default_value, const char* const* options, const char* const* option_values, size_t option_count, + bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional value( + bsi->GetOptionalStringValue(section, key, game_settings ? std::nullopt : std::optional(default_value))); + + if (option_count == 0) + { + // select from null entry + while (options && options[option_count] != nullptr) + option_count++; + } + + size_t index = option_count; + if (value.has_value()) + { + for (size_t i = 0; i < option_count; i++) + { + if (value == option_values[i]) + { + index = i; + break; + } + } + } + + if (MenuButtonWithValue(title, summary, + value.has_value() ? ((index < option_count) ? options[index] : "Unknown") : "Use Global Setting", enabled, height, font, + summary_font)) + { + ImGuiFullscreen::ChoiceDialogOptions cd_options; + cd_options.reserve(option_count + 1); + if (game_settings) + cd_options.emplace_back("Use Global Setting", !value.has_value()); + for (size_t i = 0; i < option_count; i++) + cd_options.emplace_back(options[i], (value.has_value() && i == static_cast(index))); + OpenChoiceDialog(title, false, std::move(cd_options), + [game_settings, section, key, option_values](s32 index, const std::string& title, bool checked) { + if (index >= 0) + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + if (game_settings) + { + if (index == 0) + bsi->DeleteValue(section, key); + else + bsi->SetStringValue(section, key, option_values[index - 1]); + } + else + { + bsi->SetStringValue(section, key, option_values[index]); + } + + SetSettingsChanged(bsi); + } + + CloseChoiceDialog(); + }); + } +} + +void FullscreenUI::DrawFloatListSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* key, float default_value, const char* const* options, const float* option_values, size_t option_count, bool enabled, + float height, ImFont* font, ImFont* summary_font) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional value( + bsi->GetOptionalFloatValue(section, key, game_settings ? std::nullopt : std::optional(default_value))); + + if (option_count == 0) + { + // select from null entry + while (options && options[option_count] != nullptr) + option_count++; + } + + size_t index = option_count; + if (value.has_value()) + { + for (size_t i = 0; i < option_count; i++) + { + if (value == option_values[i]) + { + index = i; + break; + } + } + } + + if (MenuButtonWithValue(title, summary, + value.has_value() ? ((index < option_count) ? options[index] : "Unknown") : "Use Global Setting", enabled, height, font, + summary_font)) + { + ImGuiFullscreen::ChoiceDialogOptions cd_options; + cd_options.reserve(option_count + 1); + if (game_settings) + cd_options.emplace_back("Use Global Setting", !value.has_value()); + for (size_t i = 0; i < option_count; i++) + cd_options.emplace_back(options[i], (value.has_value() && i == static_cast(index))); + OpenChoiceDialog(title, false, std::move(cd_options), + [game_settings, section, key, option_values](s32 index, const std::string& title, bool checked) { + if (index >= 0) + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + if (game_settings) + { + if (index == 0) + bsi->DeleteValue(section, key); + else + bsi->SetFloatValue(section, key, option_values[index - 1]); + } + else + { + bsi->SetFloatValue(section, key, option_values[index]); + } + + SetSettingsChanged(bsi); + } + + CloseChoiceDialog(); + }); + } +} + +void FullscreenUI::DrawFolderSetting(SettingsInterface* bsi, const char* title, const char* section, const char* key, + const std::string& runtime_var, float height /* = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT */, ImFont* font /* = g_large_font */, + ImFont* summary_font /* = g_medium_font */) +{ + if (MenuButton(title, runtime_var.c_str())) + { + OpenFileSelector(title, true, + [game_settings = IsEditingGameSettings(bsi), section = std::string(section), key = std::string(key)](const std::string& dir) { + if (dir.empty()) + return; + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + std::string relative_path(Path::MakeRelative(dir, EmuFolders::DataRoot)); + bsi->SetStringValue(section.c_str(), key.c_str(), relative_path.c_str()); + SetSettingsChanged(bsi); + + Host::RunOnCPUThread(&Host::Internal::UpdateEmuFolders); + + CloseFileSelector(); + }); + } +} + +void FullscreenUI::StartAutomaticBinding(u32 port) +{ + // messy because the enumeration has to happen on the input thread + Host::RunOnCPUThread([port]() { + std::vector> devices(InputManager::EnumerateDevices()); + GetMTGS().RunOnGSThread([port, devices = std::move(devices)]() { + if (devices.empty()) + { + ShowToast({}, "Automatic binding failed, no devices are available."); + return; + } + + std::vector names; + ImGuiFullscreen::ChoiceDialogOptions options; + options.reserve(devices.size()); + names.reserve(devices.size()); + for (auto& [name, display_name] : devices) + { + names.push_back(std::move(name)); + options.emplace_back(std::move(display_name), false); + } + OpenChoiceDialog("Select Device", false, std::move(options), + [port, names = std::move(names)](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + // since this is working with the device, it has to happen on the input thread too + Host::RunOnCPUThread([port, name = std::move(names[index])]() { + auto lock = Host::GetSettingsLock(); + const bool result = + PAD::MapController(*GetEditingSettingsInterface(), port, InputManager::GetGenericBindingMapping(name)); + + // and the toast needs to happen on the UI thread. + GetMTGS().RunOnGSThread([result, name = std::move(name)]() { + ShowToast({}, result ? fmt::format("Automatic mapping completed for {}.", name) : + fmt::format("Automatic mapping failed for {}.", name)); + }); + }); + CloseChoiceDialog(); + }); + }); + }); +} + +void FullscreenUI::SwitchToSettings() +{ + s_game_settings_entry.reset(); + s_game_settings_interface.reset(); + + // populate the cache with all settings from ini + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); + + PopulateGameListDirectoryCache(bsi); + PopulateGraphicsAdapterList(); + + s_current_main_window = MainWindowType::Settings; + s_settings_page = SettingsPage::Interface; +} + +void FullscreenUI::SwitchToGameSettings(const std::string_view& serial, u32 crc) +{ + s_game_settings_entry.reset(); + s_game_settings_interface = std::make_unique(VMManager::GetGameSettingsPath(serial, crc)); + s_game_settings_interface->Load(); + s_current_main_window = MainWindowType::Settings; + s_settings_page = SettingsPage::Summary; + QueueResetFocus(); +} + +void FullscreenUI::SwitchToGameSettings() +{ + if (s_current_game_serial.empty() || s_current_game_crc == 0) + return; + + auto lock = GameList::GetLock(); + const GameList::Entry* entry = GameList::GetEntryForPath(s_current_game_path.c_str()); + if (!entry) + { + entry = GameList::GetEntryBySerialAndCRC(s_current_game_serial.c_str(), s_current_game_crc); + if (!entry) + { + SwitchToGameSettings(s_current_game_serial.c_str(), s_current_game_crc); + return; + } + } + + SwitchToGameSettings(entry); +} + +void FullscreenUI::SwitchToGameSettings(const std::string& path) +{ + auto lock = GameList::GetLock(); + const GameList::Entry* entry = GameList::GetEntryForPath(path.c_str()); + if (entry) + SwitchToGameSettings(entry); +} + +void FullscreenUI::SwitchToGameSettings(const GameList::Entry* entry) +{ + SwitchToGameSettings(entry->serial.c_str(), entry->crc); + s_game_settings_entry = std::make_unique(*entry); +} + +void FullscreenUI::PopulateGraphicsAdapterList() +{ + HostDisplay* display = Host::GetHostDisplay(); + HostDisplay::AdapterAndModeList ml(display->GetAdapterAndModeList()); + s_graphics_adapter_list_cache = std::move(ml.adapter_names); + s_fullscreen_mode_list_cache = std::move(ml.fullscreen_modes); + s_fullscreen_mode_list_cache.insert(s_fullscreen_mode_list_cache.begin(), "Borderless Fullscreen"); +} + +void FullscreenUI::PopulateGameListDirectoryCache(SettingsInterface* si) +{ + s_game_list_directories_cache.clear(); + for (std::string& dir : si->GetStringList("GameList", "Paths")) + s_game_list_directories_cache.emplace_back(std::move(dir), false); + for (std::string& dir : si->GetStringList("GameList", "RecursivePaths")) + s_game_list_directories_cache.emplace_back(std::move(dir), true); +} + +ImGuiFullscreen::ChoiceDialogOptions FullscreenUI::GetGameListDirectoryOptions(bool recursive_as_checked) +{ + ImGuiFullscreen::ChoiceDialogOptions options; + for (const auto& it : s_game_list_directories_cache) + options.emplace_back(it.first, it.second && recursive_as_checked); + return options; +} + +void FullscreenUI::DoCopyGameSettings() +{ + if (!s_game_settings_interface) + return; + + Pcsx2Config temp; + { + SettingsLoadWrapper wrapper(*GetEditingSettingsInterface(false)); + temp.LoadSave(wrapper); + } + { + SettingsSaveWrapper wrapper(*s_game_settings_interface.get()); + temp.LoadSave(wrapper); + } + + SetSettingsChanged(s_game_settings_interface.get()); + + ShowToast(std::string(), fmt::format("Game settings initialized with global settings for '{}'.", + Path::GetFileTitle(s_game_settings_interface->GetFileName()))); +} + +void FullscreenUI::DoClearGameSettings() +{ + if (!s_game_settings_interface) + return; + + s_game_settings_interface->Clear(); + if (!s_game_settings_interface->GetFileName().empty()) + FileSystem::DeleteFilePath(s_game_settings_interface->GetFileName().c_str()); + + SetSettingsChanged(s_game_settings_interface.get()); + + ShowToast(std::string(), + fmt::format("Game settings have been cleared for '{}'.", Path::GetFileTitle(s_game_settings_interface->GetFileName()))); +} + +void FullscreenUI::DrawSettingsWindow() +{ + ImGuiIO& io = ImGui::GetIO(); + ImVec2 heading_size = + ImVec2(io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f + 2.0f)); + + const float bg_alpha = VMManager::HasValidVM() ? 0.90f : 1.0f; + + if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "settings_category", UIPrimaryColor)) + { + static constexpr float ITEM_WIDTH = 22.0f; + + static constexpr const char* global_icons[] = {ICON_FA_WINDOW_MAXIMIZE, ICON_FA_LIST, ICON_FA_MICROCHIP, ICON_FA_SLIDERS_H, + ICON_FA_HDD, ICON_FA_MAGIC, ICON_FA_HEADPHONES, ICON_FA_SD_CARD, ICON_FA_GAMEPAD, ICON_FA_KEYBOARD, ICON_FA_TROPHY, + ICON_FA_COGS}; + static constexpr const char* per_game_icons[] = {ICON_FA_PARAGRAPH, ICON_FA_SLIDERS_H, ICON_FA_HDD, ICON_FA_MAGIC, + ICON_FA_HEADPHONES, ICON_FA_SD_CARD, ICON_FA_GAMEPAD, ICON_FA_BAN}; + static constexpr SettingsPage global_pages[] = {SettingsPage::Interface, SettingsPage::GameList, SettingsPage::BIOS, + SettingsPage::Emulation, SettingsPage::System, SettingsPage::Graphics, SettingsPage::Audio, SettingsPage::MemoryCard, + SettingsPage::Controller, SettingsPage::Hotkey, SettingsPage::Achievements, SettingsPage::Advanced}; + static constexpr SettingsPage per_game_pages[] = {SettingsPage::Summary, SettingsPage::Emulation, SettingsPage::System, + SettingsPage::Graphics, SettingsPage::Audio, SettingsPage::MemoryCard, SettingsPage::Controller, SettingsPage::GameFixes}; + static constexpr const char* titles[] = {"Summary", "Interface Settings", "Game List Settings", "BIOS Settings", + "Emulation Settings", "System Settings", "Graphics Settings", "Audio Settings", "Memory Card Settings", "Controller Settings", + "Hotkey Settings", "Achievements Settings", "Advanced Settings", "Game Fixes"}; + + const bool game_settings = IsEditingGameSettings(GetEditingSettingsInterface()); + const u32 count = game_settings ? std::size(per_game_pages) : std::size(global_pages); + const char* const* icons = game_settings ? per_game_icons : global_icons; + const SettingsPage* pages = game_settings ? per_game_pages : global_pages; + u32 index = 0; + for (u32 i = 0; i < count; i++) + { + if (pages[i] == s_settings_page) + { + index = i; + break; + } + } + + BeginNavBar(); + + if (ImGui::IsNavInputTest(ImGuiNavInput_FocusPrev, ImGuiNavReadMode_Pressed)) + { + index = (index == 0) ? (count - 1) : (index - 1); + s_settings_page = pages[index]; + } + else if (ImGui::IsNavInputTest(ImGuiNavInput_FocusNext, ImGuiNavReadMode_Pressed)) + { + index = (index + 1) % count; + s_settings_page = pages[index]; + } + + if (NavButton(ICON_FA_BACKWARD, true, true)) + ReturnToMainWindow(); + + if (s_game_settings_entry) + NavTitle(fmt::format("{} ({})", titles[static_cast(pages[index])], s_game_settings_entry->title).c_str()); + else + NavTitle(titles[static_cast(pages[index])]); + + RightAlignNavButtons(count, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + for (u32 i = 0; i < count; i++) + { + if (NavButton(icons[i], i == index, true, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + s_settings_page = pages[i]; + } + } + + EndNavBar(); + } + + EndFullscreenWindow(); + + if (BeginFullscreenWindow(ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y), "settings_parent", + ImVec4(UIBackgroundColor.x, UIBackgroundColor.y, UIBackgroundColor.z, bg_alpha))) + { + ResetFocusHere(); + + if (WantsToCloseMenu()) + { + if (ImGui::IsWindowFocused()) + ReturnToMainWindow(); + } + + auto lock = Host::GetSettingsLock(); + + switch (s_settings_page) + { + case SettingsPage::Summary: + DrawSummarySettingsPage(); + break; + + case SettingsPage::Interface: + DrawInterfaceSettingsPage(); + break; + + case SettingsPage::GameList: + DrawGameListSettingsPage(); + break; + + case SettingsPage::BIOS: + DrawBIOSSettingsPage(); + break; + + case SettingsPage::Emulation: + DrawEmulationSettingsPage(); + break; + + case SettingsPage::System: + DrawSystemSettingsPage(); + break; + + case SettingsPage::Graphics: + DrawGraphicsSettingsPage(); + break; + + case SettingsPage::Audio: + DrawAudioSettingsPage(); + break; + + case SettingsPage::MemoryCard: + DrawMemoryCardSettingsPage(); + break; + + case SettingsPage::Controller: + DrawControllerSettingsPage(); + break; + + case SettingsPage::Hotkey: + DrawHotkeySettingsPage(); + break; + + case SettingsPage::Achievements: + DrawAchievementsSettingsPage(); + break; + + case SettingsPage::Advanced: + DrawAdvancedSettingsPage(); + break; + + case SettingsPage::GameFixes: + DrawGameFixesSettingsPage(); + break; + + default: + break; + } + } + + EndFullscreenWindow(); +} + +void FullscreenUI::DrawSummarySettingsPage() +{ + BeginMenuButtons(); + + MenuHeading("Details"); + + if (s_game_settings_entry) + { + if (MenuButton(ICON_FA_WINDOW_MAXIMIZE " Title", s_game_settings_entry->title.c_str(), true)) + CopyTextToClipboard("Game title copied to clipboard.", s_game_settings_entry->title); + if (MenuButton(ICON_FA_PAGER " Serial", s_game_settings_entry->serial.c_str(), true)) + CopyTextToClipboard("Game serial copied to clipboard.", s_game_settings_entry->serial); + if (MenuButton(ICON_FA_CODE " CRC", fmt::format("{:08X}", s_game_settings_entry->crc).c_str(), true)) + CopyTextToClipboard("Game CRC copied to clipboard.", fmt::format("{:08X}", s_game_settings_entry->crc)); + if (MenuButton(ICON_FA_COMPACT_DISC " Type", GameList::EntryTypeToString(s_game_settings_entry->type), true)) + CopyTextToClipboard("Game type copied to clipboard.", GameList::EntryTypeToString(s_game_settings_entry->type)); + if (MenuButton(ICON_FA_BOX " Region", GameList::RegionToString(s_game_settings_entry->region), true)) + CopyTextToClipboard("Game region copied to clipboard.", GameList::RegionToString(s_game_settings_entry->region)); + if (MenuButton(ICON_FA_STAR " Compatibility Rating", + GameList::EntryCompatibilityRatingToString(s_game_settings_entry->compatibility_rating), true)) + { + CopyTextToClipboard("Game compatibility copied to clipboard.", + GameList::EntryCompatibilityRatingToString(s_game_settings_entry->compatibility_rating)); + } + if (MenuButton(ICON_FA_FOLDER_OPEN " Path", s_game_settings_entry->path.c_str(), true)) + CopyTextToClipboard("Game path copied to clipboard.", s_game_settings_entry->path); + } + else + { + MenuButton(ICON_FA_BAN " Details unavailable for game not scanned in game list.", ""); + } + + MenuHeading("Options"); + + if (MenuButton(ICON_FA_COPY " Copy Settings", "Copies the current global settings to this game.")) + DoCopyGameSettings(); + if (MenuButton(ICON_FA_TRASH " Clear Settings", "Clears all settings set for this game.")) + DoClearGameSettings(); + + EndMenuButtons(); +} + +void FullscreenUI::DrawInterfaceSettingsPage() +{ + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + MenuHeading("Behaviour"); + + DrawToggleSetting(bsi, ICON_FA_MAGIC " Inhibit Screensaver", + "Prevents the screen saver from activating and the host from sleeping while emulation is running.", "UI", "InhibitScreensaver", + true); +#ifdef WITH_DISCORD_PRESENCE + DrawToggleSetting(bsi, "Enable Discord Presence", "Shows the game you are currently playing as part of your profile on Discord.", "UI", + "DiscordPresence", false); +#endif + DrawToggleSetting(bsi, ICON_FA_PAUSE " Pause On Start", "Pauses the emulator when a game is started.", "UI", "StartPaused", false); + DrawToggleSetting(bsi, ICON_FA_VIDEO " Pause On Focus Loss", + "Pauses the emulator when you minimize the window or switch to another application, and unpauses when you switch back.", "UI", + "PauseOnFocusLoss", false); + DrawToggleSetting(bsi, ICON_FA_WINDOW_MAXIMIZE " Pause On Menu", + "Pauses the emulator when you open the quick menu, and unpauses when you close it.", "UI", "PauseOnMenu", true); + DrawToggleSetting(bsi, ICON_FA_POWER_OFF " Confirm Shutdown", + "Determines whether a prompt will be displayed to confirm shutting down the emulator/game when the hotkey is pressed.", "UI", + "ConfirmShutdown", true); + DrawToggleSetting(bsi, ICON_FA_SAVE " Save State On Shutdown", + "Automatically saves the emulator state when powering down or exiting. You can then resume directly from where you left off next " + "time.", + "EmuCore", "SaveStateOnShutdown", false); + + MenuHeading("Game Display"); + DrawToggleSetting(bsi, ICON_FA_TV " Start Fullscreen", "Automatically switches to fullscreen mode when the program is started.", "UI", + "StartFullscreen", false); + DrawToggleSetting(bsi, ICON_FA_MOUSE " Double-Click Toggles Fullscreen", + "Switches between full screen and windowed when the window is double-clicked.", "UI", "DoubleClickTogglesFullscreen", true); + DrawToggleSetting(bsi, ICON_FA_MOUSE_POINTER " Hide Cursor In Fullscreen", + "Hides the mouse pointer/cursor when the emulator is in fullscreen mode.", "UI", "HideMouseCursor", false); + + MenuHeading("On-Screen Display"); + DrawIntRangeSetting(bsi, ICON_FA_SEARCH " OSD Scale", "Determines how large the on-screen messages and monitor are.", "EmuCore/GS", + "OsdScale", 100, 25, 500, "%d%%"); + DrawToggleSetting(bsi, ICON_FA_LIST " Show Messages", + "Shows on-screen-display messages when events occur such as save states being created/loaded, screenshots being taken, etc.", + "EmuCore/GS", "OsdShowMessages", true); + DrawToggleSetting(bsi, ICON_FA_CLOCK " Show Speed", + "Shows the current emulation speed of the system in the top-right corner of the display as a percentage.", "EmuCore/GS", + "OsdShowSpeed", false); + DrawToggleSetting(bsi, ICON_FA_RULER " Show FPS", + "Shows the number of video frames (or v-syncs) displayed per second by the system in the top-right corner of the display.", + "EmuCore/GS", "OsdShowFPS", false); + DrawToggleSetting(bsi, ICON_FA_BATTERY_HALF " Show CPU Usage", + "Shows the CPU usage based on threads in the top-right corner of the display.", "EmuCore/GS", "OsdShowCPU", false); + DrawToggleSetting(bsi, ICON_FA_SPINNER " Show GPU Usage", "Shows the host's GPU usage in the top-right corner of the display.", + "EmuCore/GS", "OsdShowGPU", false); + DrawToggleSetting(bsi, ICON_FA_RULER_VERTICAL " Show Resolution", + "Shows the resolution the game is rendering at in the top-right corner of the display.", "EmuCore/GS", "OsdShowResolution", false); + DrawToggleSetting(bsi, ICON_FA_BARS " Show GS Statistics", + "Shows statistics about GS (primitives, draw calls) in the top-right corner of the display.", "EmuCore/GS", "OsdShowGSStats", + false); + DrawToggleSetting(bsi, ICON_FA_PLAY " Show Status Indicators", + "Shows indicators when fast forwarding, pausing, and other abnormal states are active.", "EmuCore/GS", "OsdShowIndicators", true); + + EndMenuButtons(); +} + +void FullscreenUI::DrawGameListSettingsPage() +{ + BeginMenuButtons(); + + MenuHeading("Game List"); + + if (MenuButton(ICON_FA_FOLDER_PLUS " Add Search Directory", "Adds a new directory to the game search list.")) + { + OpenFileSelector(ICON_FA_FOLDER_PLUS " Add Search Directory", true, [](const std::string& dir) { + if (!dir.empty()) + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); + + bsi->AddToStringList("GameList", "RecursivePaths", dir.c_str()); + bsi->RemoveFromStringList("GameList", "Paths", dir.c_str()); + bsi->Save(); + PopulateGameListDirectoryCache(bsi); + Host::RefreshGameListAsync(false); + } + + CloseFileSelector(); + }); + } + + if (MenuButton( + ICON_FA_FOLDER_OPEN " Change Recursive Directories", "Sets whether subdirectories are searched for each game directory")) + { + OpenChoiceDialog(ICON_FA_FOLDER_OPEN " Change Recursive Directories", true, GetGameListDirectoryOptions(true), + [](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); + if (checked) + { + bsi->RemoveFromStringList("GameList", "Paths", title.c_str()); + bsi->AddToStringList("GameList", "RecursivePaths", title.c_str()); + } + else + { + bsi->RemoveFromStringList("GameList", "RecursivePaths", title.c_str()); + bsi->AddToStringList("GameList", "Paths", title.c_str()); + } + + bsi->Save(); + PopulateGameListDirectoryCache(bsi); + Host::RefreshGameListAsync(false); + }); + } + + if (MenuButton(ICON_FA_FOLDER_MINUS " Remove Search Directory", "Removes a directory from the game search list.")) + { + OpenChoiceDialog(ICON_FA_FOLDER_MINUS " Remove Search Directory", false, GetGameListDirectoryOptions(false), + [](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); + bsi->RemoveFromStringList("GameList", "Paths", title.c_str()); + bsi->RemoveFromStringList("GameList", "RecursivePaths", title.c_str()); + bsi->Save(); + PopulateGameListDirectoryCache(bsi); + Host::RefreshGameListAsync(false); + CloseChoiceDialog(); + }); + } + + if (MenuButton(ICON_FA_SEARCH " Scan For New Games", "Identifies any new files added to the game directories.")) + Host::RefreshGameListAsync(false); + if (MenuButton(ICON_FA_SEARCH_PLUS " Rescan All Games", "Forces a full rescan of all games previously identified.")) + Host::RefreshGameListAsync(true); + + MenuHeading("Search Directories"); + for (const auto& it : s_game_list_directories_cache) + MenuButton(it.first.c_str(), it.second ? "Scanning Subdirectories" : "Not Scanning Subdirectories", false); + + EndMenuButtons(); +} + +void FullscreenUI::DrawBIOSSettingsPage() +{ + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + MenuHeading("BIOS Configuration"); + + DrawFolderSetting(bsi, ICON_FA_FOLDER_OPEN " Change Search Directory", "Folders", "Bios", EmuFolders::Bios); + + const std::string bios_selection(GetEditingSettingsInterface()->GetStringValue("Filenames", "BIOS", "")); + if (MenuButtonWithValue(ICON_FA_MICROCHIP " BIOS Selection", "Changes the BIOS image used to start future sessions.", + bios_selection.empty() ? "Automatic" : bios_selection.c_str())) + { + ImGuiFullscreen::ChoiceDialogOptions choices; + choices.emplace_back("Automatic", bios_selection.empty()); + + std::vector values; + values.push_back(""); + + FileSystem::FindResultsArray results; + FileSystem::FindFiles(EmuFolders::Bios.c_str(), "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &results); + for (const FILESYSTEM_FIND_DATA& fd : results) + { + u32 version, region; + std::string description, zone; + if (!IsBIOS(fd.FileName.c_str(), version, description, region, zone)) + continue; + + const std::string_view filename(Path::GetFileName(fd.FileName)); + choices.emplace_back(fmt::format("{} ({})", description, filename), bios_selection == filename); + values.emplace_back(filename); + } + + OpenChoiceDialog("BIOS Selection", false, std::move(choices), + [game_settings = IsEditingGameSettings(bsi), values = std::move(values)](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue("Filenames", "BIOS", values[index].c_str()); + SetSettingsChanged(bsi); + CloseChoiceDialog(); + }); + } + + MenuHeading("Options and Patches"); + DrawToggleSetting( + bsi, ICON_FA_LIGHTBULB " Fast Boot", "Skips the intro screen, and bypasses region checks.", "EmuCore", "EnableFastBoot", true); + + EndMenuButtons(); +} + +void FullscreenUI::DrawEmulationSettingsPage() +{ + static constexpr int DEFAULT_FRAME_LATENCY = 2; + + static constexpr const char* speed_entries[] = { + "2% [1 FPS (NTSC) / 1 FPS (PAL)]", + "10% [6 FPS (NTSC) / 5 FPS (PAL)]", + "25% [15 FPS (NTSC) / 12 FPS (PAL)]", + "50% [30 FPS (NTSC) / 25 FPS (PAL)]", + "75% [45 FPS (NTSC) / 37 FPS (PAL)]", + "90% [54 FPS (NTSC) / 45 FPS (PAL)]", + "100% [60 FPS (NTSC) / 50 FPS (PAL)]", + "110% [66 FPS (NTSC) / 55 FPS (PAL)]", + "120% [72 FPS (NTSC) / 60 FPS (PAL)]", + "150% [90 FPS (NTSC) / 75 FPS (PAL)]", + "175% [105 FPS (NTSC) / 87 FPS (PAL)]", + "200% [120 FPS (NTSC) / 100 FPS (PAL)]", + "300% [180 FPS (NTSC) / 150 FPS (PAL)]", + "400% [240 FPS (NTSC) / 200 FPS (PAL)]", + "500% [300 FPS (NTSC) / 250 FPS (PAL)]", + "1000% [600 FPS (NTSC) / 500 FPS (PAL)]", + }; + static constexpr const float speed_values[] = { + 0.02f, + 0.10f, + 0.25f, + 0.50f, + 0.75f, + 0.90f, + 1.00f, + 1.10f, + 1.20f, + 1.50f, + 1.75f, + 2.00f, + 3.00f, + 4.00f, + 5.00f, + 10.00f, + }; + static constexpr const char* queue_entries[] = {"0 Frames (Hard Sync)", "1 Frame", "2 Frames", "3 Frames"}; + + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + MenuHeading("Speed Control"); + + DrawFloatListSetting(bsi, "Normal Speed", "Sets the speed when running without fast forwarding.", "Framerate", "NominalScalar", 1.00f, + speed_entries, speed_values, std::size(speed_entries)); + DrawFloatListSetting(bsi, "Fast Forward Speed", "Sets the speed when using the fast forward hotkey.", "Framerate", "TurboScalar", 2.00f, + speed_entries, speed_values, std::size(speed_entries)); + DrawFloatListSetting(bsi, "Slow Motion Speed", "Sets the speed when using the slow motion hotkey.", "Framerate", "SlomoScalar", 0.50f, + speed_entries, speed_values, std::size(speed_entries)); + DrawToggleSetting( + bsi, "Enable Speed Limiter", "When disabled, the game will run as fast as possible.", "EmuCore/GS", "FrameLimitEnable", true); + + MenuHeading("Frame Pacing/Latency Control"); + + bool optimal_frame_pacing = (bsi->GetIntValue("EmuCore/GS", "VsyncQueueSize", DEFAULT_FRAME_LATENCY) == 0); + + DrawIntListSetting(bsi, "Maximum Frame Latency", "Sets the number of frames which can be queued.", "EmuCore/GS", "VsyncQueueSize", + DEFAULT_FRAME_LATENCY, queue_entries, std::size(queue_entries), 0, !optimal_frame_pacing); + + if (ToggleButton("Optimal Frame Pacing", + "Synchronize EE and GS threads after each frame. Lowest input latency, but increases system requirements.", + &optimal_frame_pacing)) + { + bsi->SetIntValue("EmuCore/GS", "VsyncQueueSize", optimal_frame_pacing ? 0 : DEFAULT_FRAME_LATENCY); + SetSettingsChanged(bsi); + } + + DrawToggleSetting(bsi, "Adjust To Host Refresh Rate", "Speeds up emulation so that the guest refresh rate matches the host.", + "EmuCore/GS", "SyncToHostRefreshRate", false); + + MenuHeading("Game Settings"); + + DrawToggleSetting(bsi, "Enable Cheats", "Enables loading cheats from pnach files.", "EmuCore", "EnableCheats", false); + DrawToggleSetting(bsi, "Enable Widescreen Patches", "Enables loading widescreen patches from pnach files.", "EmuCore", + "EnableWideScreenPatches", false); + DrawToggleSetting(bsi, "Enable No-Interlacing Patches", "Enables loading no-interlacing patches from pnach files.", "EmuCore", + "EnableNoInterlacingPatches", false); + DrawToggleSetting(bsi, "Enable Per-Game Settings", "Enables loading ini overlays from gamesettings, or custom settings per-game.", + "EmuCore", "EnablePerGameSettings", true); + DrawToggleSetting(bsi, "Enable Host Filesystem", "Enables access to files from the host: namespace in the virtual machine.", "EmuCore", + "HostFs", false); + + EndMenuButtons(); +} + +void FullscreenUI::DrawClampingModeSetting(SettingsInterface* bsi, const char* title, const char* summary, bool vu) +{ + // This is so messy... maybe we should just make the mode an int in the settings too... + const bool base = IsEditingGameSettings(bsi) ? 1 : 0; + std::optional default_false = IsEditingGameSettings(bsi) ? std::nullopt : std::optional(false); + std::optional default_true = IsEditingGameSettings(bsi) ? std::nullopt : std::optional(true); + + std::optional third = bsi->GetOptionalBoolValue("EmuCore/CPU/Recompiler", vu ? "vuSignOverflow" : "fpuFullMode", default_false); + std::optional second = + bsi->GetOptionalBoolValue("EmuCore/CPU/Recompiler", vu ? "vuExtraOverflow" : "fpuExtraOverflow", default_false); + std::optional first = bsi->GetOptionalBoolValue("EmuCore/CPU/Recompiler", vu ? "vuOverflow" : "fpuOverflow", default_true); + + int index; + if (third.has_value() && third.value()) + index = base + 3; + else if (second.has_value() && second.value()) + index = base + 2; + else if (first.has_value() && first.value()) + index = base + 1; + else if (first.has_value()) + index = base + 0; // none + else + index = 0; // no per game override + + static constexpr const char* ee_clamping_mode_settings[] = { + "Use Global Setting", "None", "Normal (Default)", "Extra + Preserve Sign", "Full"}; + static constexpr const char* vu_clamping_mode_settings[] = { + "Use Global Setting", "None", "Normal (Default)", "Extra", "Extra + Preserve Sign"}; + const char* const* options = vu ? vu_clamping_mode_settings : ee_clamping_mode_settings; + const int setting_offset = IsEditingGameSettings(bsi) ? 0 : 1; + + if (MenuButtonWithValue(title, summary, options[index + setting_offset])) + { + ImGuiFullscreen::ChoiceDialogOptions cd_options; + cd_options.reserve(std::size(ee_clamping_mode_settings)); + for (int i = setting_offset; i < static_cast(std::size(ee_clamping_mode_settings)); i++) + cd_options.emplace_back(options[i], (i == (index + setting_offset))); + OpenChoiceDialog(title, false, std::move(cd_options), + [game_settings = IsEditingGameSettings(bsi), vu](s32 index, const std::string& title, bool checked) { + if (index >= 0) + { + auto lock = Host::GetSettingsLock(); + + std::optional first, second, third; + + if (!game_settings || index > 0) + { + const bool base = game_settings ? 1 : 0; + third = (index >= (base + 3)); + second = (index >= (base + 2)); + first = (index >= (base + 1)); + } + + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetOptionalBoolValue("EmuCore/CPU/Recompiler", vu ? "vuSignOverflow" : "fpuFullMode", third); + bsi->SetOptionalBoolValue("EmuCore/CPU/Recompiler", vu ? "vuExtraOverflow" : "fpuExtraOverflow", second); + bsi->SetOptionalBoolValue("EmuCore/CPU/Recompiler", vu ? "vuOverflow" : "fpuOverflow", first); + SetSettingsChanged(bsi); + } + + CloseChoiceDialog(); + }); + } +} + +void FullscreenUI::DrawSystemSettingsPage() +{ + static constexpr const char* ee_cycle_rate_settings[] = { + "50% Speed", "60% Speed", "75% Speed", "100% Speed (Default)", "130% Speed", "180% Speed", "300% Speed"}; + static constexpr const char* ee_cycle_skip_settings[] = { + "Normal (Default)", "Mild Underclock", "Moderate Overclock", "Maximum Overclock"}; + static constexpr const char* ee_rounding_mode_settings[] = {"Nearest", "Negative", "Positive", "Chop/Zero (Default)"}; + static constexpr const char* affinity_control_settings[] = { + "Disabled", "EE > VU > GS", "EE > GS > VU", "VU > EE > GS", "VU > GS > EE", "GS > EE > VU", "GS > VU > EE"}; + + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + MenuHeading("Emotion Engine (MIPS-III/MIPS-IV)"); + DrawIntListSetting(bsi, "Cycle Rate", "Underclocks or overclocks the emulated Emotion Engine CPU.", "EmuCore/Speedhacks", "EECycleRate", + 0, ee_cycle_rate_settings, std::size(ee_cycle_rate_settings), -3); + DrawIntListSetting(bsi, "Cycle Skip", "Adds a penalty to the Emulated Emotion Engine for executing VU programs.", "EmuCore/Speedhacks", + "EECycleSkip", 0, ee_cycle_skip_settings, std::size(ee_cycle_skip_settings)); + DrawIntListSetting(bsi, "Rounding Mode##ee_rounding_mode", + "Determines how the results of floating-point operations are rounded. Some games need specific settings.", "EmuCore/CPU", + "FPU.Roundmode", 3, ee_rounding_mode_settings, std::size(ee_rounding_mode_settings)); + DrawClampingModeSetting(bsi, "Clamping Mode##ee_clamping_mode", + "Determines how out-of-range floating point numbers are handled. Some games need specific settings.", false); + DrawIntListSetting(bsi, "Affinity Control Mode", + "Pins emulation threads to CPU cores to potentially improve performance/frame time variance.", "EmuCore/CPU", "AffinityControlMode", + 0, affinity_control_settings, std::size(affinity_control_settings), 0); + + MenuHeading("Vector Units"); + DrawIntListSetting(bsi, "Rounding Mode##vu_rounding_mode", + "Determines how the results of floating-point operations are rounded. Some games need specific settings.", "EmuCore/CPU", + "VU.Roundmode", 3, ee_rounding_mode_settings, std::size(ee_rounding_mode_settings)); + DrawClampingModeSetting(bsi, "Clamping Mode##vu_clamping_mode", + "Determines how out-of-range floating point numbers are handled. Some games need specific settings.", true); + DrawToggleSetting(bsi, "Enable MTVU (Multi-Threaded VU1)", "Uses a second thread for VU1 micro programs. Sizable speed boost.", + "EmuCore/Speedhacks", "vuThread", false); + DrawToggleSetting(bsi, "Enable Instant VU1", + "Reduces timeslicing between VU1 and EE recompilers, effectively running VU1 at an infinite clock speed.", "EmuCore/Speedhacks", + "vu1Instant", true); + + MenuHeading("I/O Processor (MIPS-I)"); + DrawToggleSetting( + bsi, "Enable Fast CDVD", "Fast disc access, less loading times. Not recommended.", "EmuCore/Speedhacks", "fastCDVD", false); + + EndMenuButtons(); +} + +void FullscreenUI::DrawGraphicsSettingsPage() +{ + static constexpr const char* s_renderer_names[] = {"Automatic", +#ifdef _WIN32 + "Direct3D 11", "Direct3D 12", +#endif +#ifdef ENABLE_OPENGL + "OpenGL", +#endif +#ifdef ENABLE_VULKAN + "Vulkan", +#endif +#ifdef __APPLE__ + "Metal", +#endif + "Software", "Null"}; + static constexpr const char* s_renderer_values[] = { + "-1", //GSRendererType::Auto, +#ifdef _WIN32 + "3", //GSRendererType::DX11, + "15", //GSRendererType::DX12, +#endif +#ifdef ENABLE_OPENGL + "12", //GSRendererType::OGL, +#endif +#ifdef ENABLE_VULKAN + "14", //GSRendererType::VK, +#endif +#ifdef __APPLE__ + "17", //GSRendererType::Metal, +#endif + "13", //GSRendererType::SW, + "11", //GSRendererType::Null + }; + static constexpr const char* s_vsync_values[] = {"Off", "On", "Adaptive"}; + static constexpr const char* s_deinterlacing_options[] = {"None", "Weave (Top Field First, Sawtooth)", + "Weave (Bottom Field First, Sawtooth)", "Bob (Top Field First)", "Bob (Bottom Field First)", "Blend (Top Field First, Half FPS)", + "Blend (Bottom Field First, Half FPS)", "Automatic (Default)"}; + static constexpr const char* s_resolution_options[] = { + "Native (PS2)", + "2x Native (~720p)", + "3x Native (~1080p)", + "4x Native (~1440p/2K)", + "5x Native (~1620p)", + "6x Native (~2160p/4K)", + "7x Native (~2520p)", + "8x Native (~2880p)", + }; + static constexpr const char* s_mipmapping_options[] = {"Automatic (Default)", "Off", "Basic (Generated Mipmaps)", "Full (PS2 Mipmaps)"}; + static constexpr const char* s_bilinear_options[] = { + "Nearest", "Bilinear (Forced)", "Bilinear (PS2)", "Bilinear (Forced excluding sprite)"}; + static constexpr const char* s_trilinear_options[] = {"Automatic (Default)", "Off (None)", "Trilinear (PS2)", "Trilinear (Forced)"}; + static constexpr const char* s_dithering_options[] = {"Off", "Scaled", "Unscaled (Default)"}; + static constexpr const char* s_crc_fix_options[] = { + "Automatic (Default)", "None (Debug)", "Minimum (Debug)", "Partial (OpenGL)", "Full (Direct3D)", "Aggressive"}; + static constexpr const char* s_blending_options[] = { + "Minimum", "Basic (Recommended)", "Medium", "High", "Full (Slow)", "Maximum (Very Slow)"}; + static constexpr const char* s_anisotropic_filtering_entries[] = {"Off (Default)", "2x", "4x", "8x", "16x"}; + static constexpr const char* s_anisotropic_filtering_values[] = {"0", "2", "4", "8", "16"}; + static constexpr const char* s_preloading_options[] = {"None", "Partial", "Full (Hash Cache)"}; + static constexpr const char* s_generic_options[] = {"Automatic (Default)", "Force Disabled", "Force Enabled"}; + + SettingsInterface* bsi = GetEditingSettingsInterface(); + + const GSRendererType renderer = + static_cast(GetEffectiveIntSetting(bsi, "EmuCore/GS", "Renderer", static_cast(GSRendererType::Auto))); + const bool is_hardware = (renderer == GSRendererType::Auto || renderer == GSRendererType::DX11 || renderer == GSRendererType::DX12 || + renderer == GSRendererType::OGL || renderer == GSRendererType::VK || renderer == GSRendererType::Metal); + //const bool is_software = (renderer == GSRendererType::SW); + + BeginMenuButtons(); + + MenuHeading("Renderer"); + DrawStringListSetting(bsi, "Renderer", "Selects the API used to render the emulated GS.", "EmuCore/GS", "Renderer", "-1", + s_renderer_names, s_renderer_values, std::size(s_renderer_names)); + DrawIntListSetting(bsi, "Sync To Host Refresh (VSync)", "Synchronizes frame presentation with host refresh.", "EmuCore/GS", + "VsyncEnable", static_cast(VsyncMode::Off), s_vsync_values, std::size(s_vsync_values)); + + MenuHeading("Display"); + DrawStringListSetting(bsi, "Aspect Ratio", "Selects the aspect ratio to display the game content at.", "EmuCore/GS", "AspectRatio", + "Auto 4:3/3:2", Pcsx2Config::GSOptions::AspectRatioNames, Pcsx2Config::GSOptions::AspectRatioNames, 0); + DrawStringListSetting(bsi, "FMV Aspect Ratio", "Selects the aspect ratio for display when a FMV is detected as playing.", "EmuCore/GS", + "FMVAspectRatioSwitch", "Auto 4:3/3:2", Pcsx2Config::GSOptions::FMVAspectRatioSwitchNames, + Pcsx2Config::GSOptions::FMVAspectRatioSwitchNames, 0); + DrawIntListSetting(bsi, "Deinterlacing", + "Selects the algorithm used to convert the PS2's interlaced output to progressive for display.", "EmuCore/GS", "deinterlace", + static_cast(GSInterlaceMode::Automatic), s_deinterlacing_options, std::size(s_deinterlacing_options)); + DrawIntRangeSetting(bsi, "Zoom", "Increases or decreases the virtual picture size both horizontally and vertically.", "EmuCore/GS", + "Zoom", 100, 10, 300, "%d%%"); + DrawIntRangeSetting(bsi, "Vertical Stretch", "Increases or decreases the virtual picture size vertically.", "EmuCore/GS", "StretchY", + 100, 10, 300, "%d%%"); + DrawIntRectSetting(bsi, "Crop", "Crops the image, while respecting aspect ratio.", "EmuCore/GS", "CropLeft", 0, "CropTop", 0, + "CropRight", 0, "CropBottom", 0, 0, 720, "%dpx"); + DrawToggleSetting( + bsi, "Bilinear Upscaling", "Smooths out the image when upscaling the console to the screen.", "EmuCore/GS", "linear_present", true); + DrawToggleSetting(bsi, "Integer Upscaling", + "Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an integer " + "number. May result in a sharper image in some 2D games.", + "EmuCore/GS", "IntegerScaling", false); + DrawToggleSetting(bsi, "Internal Resolution Screenshots", + "Save screenshots at the full render resolution, rather than display resolution.", "EmuCore/GS", "InternalResolutionScreenshots", + false); + DrawToggleSetting(bsi, "Screen Offsets", "Enables PCRTC Offsets which position the screen as the game requests.", "EmuCore/GS", + "pcrtc_offsets", false); + DrawToggleSetting(bsi, "Show Overscan", + "Enables the option to show the overscan area on games which draw more than the safe area of the screen.", "EmuCore/GS", + "pcrtc_overscan", false); + DrawToggleSetting(bsi, "Anti-Blur", + "Enables internal Anti-Blur hacks. Less accurate to PS2 rendering but will make a lot of games look less blurry.", "EmuCore/GS", + "pcrtc_antiblur", true); + + MenuHeading("Rendering"); + if (is_hardware) + { + DrawIntListSetting(bsi, "Internal Resolution", "Multiplies the render resolution by the specified factor (upscaling).", + "EmuCore/GS", "upscale_multiplier", 1, s_resolution_options, std::size(s_resolution_options), 1); + DrawIntListSetting(bsi, "Mipmapping", "Determines how mipmaps are used when rendering textures.", "EmuCore/GS", "mipmap_hw", + static_cast(HWMipmapLevel::Automatic), s_mipmapping_options, std::size(s_mipmapping_options), -1); + DrawIntListSetting(bsi, "Bilinear Filtering", "Selects where bilinear filtering is utilized when rendering textures.", "EmuCore/GS", + "filter", static_cast(BiFiltering::PS2), s_bilinear_options, std::size(s_bilinear_options)); + DrawIntListSetting(bsi, "Trilinear Filtering", "Selects where trilinear filtering is utilized when rendering textures.", + "EmuCore/GS", "UserHacks_TriFilter", static_cast(TriFiltering::Automatic), s_trilinear_options, + std::size(s_trilinear_options), -1); + DrawStringListSetting(bsi, "Anisotropic Filtering", "Selects where anistropic filtering is utilized when rendering textures.", + "EmuCore/GS", "MaxAnisotropy", "0", s_anisotropic_filtering_entries, s_anisotropic_filtering_values, + std::size(s_anisotropic_filtering_entries)); + DrawIntListSetting(bsi, "Dithering", "Selects the type of dithering applies when the game requests it.", "EmuCore/GS", + "dithering_ps2", 2, s_dithering_options, std::size(s_dithering_options)); + DrawIntListSetting(bsi, "CRC Fix Level", "Applies manual fixes to difficult-to-emulate effects in the hardware renderers.", + "EmuCore/GS", "crc_hack_level", static_cast(CRCHackLevel::Automatic), s_crc_fix_options, std::size(s_crc_fix_options), -1); + DrawIntListSetting(bsi, "Blending Accuracy", + "Determines the level of accuracy when emulating blend modes not supported by the host graphics API.", "EmuCore/GS", + "accurate_blending_unit", static_cast(AccBlendLevel::Basic), s_blending_options, std::size(s_blending_options)); + DrawIntListSetting(bsi, "Texture Preloading", + "Uploads full textures to the GPU on use, rather than only the utilized regions. Can improve performance in some games.", + "EmuCore/GS", "texture_preloading", static_cast(TexturePreloadingLevel::Off), s_preloading_options, + std::size(s_preloading_options)); + DrawToggleSetting(bsi, "Accurate Destination Alpha Test", + "Implement a more accurate algorithm to compute GS destination alpha testing.", "EmuCore/GS", "accurate_date", true); + DrawToggleSetting(bsi, "GPU Palette Conversion", + "Applies palettes to textures on the GPU instead of the CPU. Can result in speed improvements in some games.", "EmuCore/GS", + "paltex", false); + } + else + { + } + + if (is_hardware) + { + MenuHeading("Hardware Fixes"); + DrawToggleSetting(bsi, "Manual Hardware Fixes", "Disables automatic hardware fixes, allowing you to set fixes manually.", + "EmuCore/GS", "UserHacks", false); + + const bool manual_hw_fixes = GetEffectiveBoolSetting(bsi, "EmuCore/GS", "UserHacks", false); + if (manual_hw_fixes) + { + static constexpr const char* s_cpu_sprite_render_bw_options[] = {"0 (Disabled)", "1 (64 Max Width)", "2 (128 Max Width)", + "3 (192 Max Width)", "4 (256 Max Width)", "5 (320 Max Width)", "6 (384 Max Width)", "7 (448 Max Width)", + "8 (512 Max Width)", "9 (576 Max Width)", "10 (640 Max Width)"}; + static constexpr const char* s_half_pixel_offset_options[] = { + "Off (Default)", "Normal (Vertex)", "Special (Texture)", "Special (Texture - Aggressive)"}; + static constexpr const char* s_round_sprite_options[] = {"Off (Default)", "Half", "Full"}; + + DrawIntListSetting(bsi, "Half-Bottom Override", "Control the half-screen fix detection on texture shuffling.", "EmuCore/GS", + "UserHacks_Half_Bottom_Override", -1, s_generic_options, std::size(s_generic_options), -1); + DrawIntListSetting(bsi, "CPU Sprite Render Size", "Uses sofware renderer to draw texture decompression-like sprites.", + "EmuCore/GS", "UserHacks_CPUSpriteRenderBW", 0, s_cpu_sprite_render_bw_options, std::size(s_cpu_sprite_render_bw_options)); + DrawIntRangeSetting( + bsi, "Skip Draw Start", "Object range to skip drawing.", "EmuCore/GS", "UserHacks_SkipDraw_Start", 0, 0, 5000); + DrawIntRangeSetting(bsi, "Skip Draw End", "Object range to skip drawing.", "EmuCore/GS", "UserHacks_SkipDraw_End", 0, 0, 5000); + DrawToggleSetting(bsi, "Auto Flush (Hardware)", "Force a primitive flush when a framebuffer is also an input texture.", + "EmuCore/GS", "UserHacks_AutoFlush", false, manual_hw_fixes); + DrawToggleSetting(bsi, "CPU Framebuffer Conversion", "Convert 4-bit and 8-bit frame buffer on the CPU instead of the GPU.", + "EmuCore/GS", "UserHacks_CPU_FB_Conversion", false, manual_hw_fixes); + DrawToggleSetting(bsi, "Disable Depth Support", "Disable the support of depth buffer in the texture cache.", "EmuCore/GS", + "UserHacks_DisableDepthSupport", false, manual_hw_fixes); + DrawToggleSetting( + bsi, "Wrap GS Memory", "Emulates GS memory wrapping accurately.", "EmuCore/GS", "wrap_gs_mem", false, manual_hw_fixes); + DrawToggleSetting(bsi, "Disable Safe Features", "This option disables multiple safe features.", "EmuCore/GS", + "UserHacks_Disable_Safe_Features", false, manual_hw_fixes); + DrawToggleSetting(bsi, "Preload Frame", "Uploads GS data when rendering a new frame to reproduce some effects accurately.", + "EmuCore/GS", "preload_frame_with_gs_data", false, manual_hw_fixes); + DrawToggleSetting(bsi, "Disable Partial Invalidation", + "Removes texture cache entries when there is any intersection, rather than only the intersected areas.", "EmuCore/GS", + "UserHacks_DisablePartialInvalidation", false, manual_hw_fixes); + DrawToggleSetting(bsi, "Texture Inside Render Target", + "Allows the texture cache to reuse as an input texture the inner portion of a previous framebuffer.", "EmuCore/GS", + "UserHacks_TextureInsideRt", false, manual_hw_fixes); + + MenuHeading("Upscaling Fixes"); + DrawIntListSetting(bsi, "Half-Pixel Offset", "Adjusts vertices relative to upscaling.", "EmuCore/GS", + "UserHacks_HalfPixelOffset", 0, s_half_pixel_offset_options, std::size(s_half_pixel_offset_options)); + DrawIntListSetting(bsi, "Round Sprite", "Adjusts sprite coordinates.", "EmuCore/GS", "UserHacks_round_sprite_offset", 0, + s_round_sprite_options, std::size(s_round_sprite_options)); + DrawIntRangeSetting(bsi, "TC Offset X", "Adjusts target texture offsets.", "EmuCore/GS", "UserHacks_TCOffsetX", 0, -4096, 4096); + DrawIntRangeSetting(bsi, "TC Offset Y", "Adjusts target texture offsets.", "EmuCore/GS", "UserHacks_TCOffsetY", 0, -4096, 4096); + DrawToggleSetting(bsi, "Align Sprite", "Fixes issues with upscaling (vertical lines) in some games.", "EmuCore/GS", + "UserHacks_align_sprite_X", false, manual_hw_fixes); + DrawToggleSetting(bsi, "Merge Sprite", "Replaces multiple post-processing sprites with a larger single sprite.", "EmuCore/GS", + "UserHacks_merge_pp_sprite", false, manual_hw_fixes); + DrawToggleSetting(bsi, "Wild Arms Hack", + "Lowers the GS precision to avoid gaps between pixels when upscaling. Fixes the text on Wild Arms games.", "EmuCore/GS", + "UserHacks_WildHack", false, manual_hw_fixes); + } + } + else + { + // extrathreads + DrawIntRangeSetting(bsi, "Software Rendering Threads", + "Number of threads to use in addition to the main GS thread for rasterization.", "EmuCore/GS", "extrathreads", 2, 0, 10); + DrawToggleSetting(bsi, "Auto Flush (Software)", "Force a primitive flush when a framebuffer is also an input texture.", + "EmuCore/GS", "autoflush_sw", true); + DrawToggleSetting(bsi, "Edge AA (AA1)", "Enables emulation of the GS's edge anti-aliasing (AA1).", "EmuCore/GS", "aa1", true); + DrawToggleSetting(bsi, "Mipmapping", "Enables emulation of the GS's texture mipmapping.", "EmuCore/GS", "mipmap", true); + } + + if (is_hardware) + { + const bool dumping_active = GetEffectiveBoolSetting(bsi, "EmuCore/GS", "DumpReplaceableTextures", false); + const bool replacement_active = GetEffectiveBoolSetting(bsi, "EmuCore/GS", "LoadTextureReplacements", false); + + MenuHeading("Texture Replacement"); + DrawToggleSetting(bsi, "Load Textures", "Loads replacement textures where available and user-provided.", "EmuCore/GS", + "LoadTextureReplacements", false); + DrawToggleSetting(bsi, "Asynchronous Texture Loading", + "Loads replacement textures on a worker thread, reducing microstutter when replacements are enabled.", "EmuCore/GS", + "LoadTextureReplacementsAsync", true, replacement_active); + DrawToggleSetting(bsi, "Precache Replacements", + "Preloads all replacement textures to memory. Not necessary with asynchronous loading.", "EmuCore/GS", + "PrecacheTextureReplacements", false, replacement_active); + + MenuHeading("Texture Dumping"); + DrawToggleSetting(bsi, "Dump Textures", "Dumps replacable textures to disk. Will reduce performance.", "EmuCore/GS", + "DumpReplaceableTextures", false); + DrawToggleSetting( + bsi, "Dump Mipmaps", "Includes mipmaps when dumping textures.", "EmuCore/GS", "DumpReplaceableMipmaps", false, dumping_active); + DrawToggleSetting(bsi, "Dump FMV Textures", "Allows texture dumping when FMVs are active. You should not enable this.", + "EmuCore/GS", "DumpTexturesWithFMVActive", false, dumping_active); + } + + MenuHeading("Post-Processing"); + { + const bool shadeboost_active = GetEffectiveBoolSetting(bsi, "EmuCore/GS", "ShadeBoost", false); + DrawToggleSetting(bsi, "FXAA", "Enables FXAA post-processing shader.", "EmuCore/GS", "fxaa", false); + DrawToggleSetting(bsi, "Shade Boost", "Enables brightness/contrast/saturation adjustment.", "EmuCore/GS", "ShadeBoost", false); + DrawIntRangeSetting(bsi, "Shade Boost Brightness", "Adjusts brightness. 50 is normal.", "EmuCore/GS", "ShadeBoost_Brightness", 50, + 1, 100, "%d", shadeboost_active); + DrawIntRangeSetting(bsi, "Shade Boost Contrast", "Adjusts contrast. 50 is normal.", "EmuCore/GS", "ShadeBoost_Contrast", 50, 1, 100, + "%d", shadeboost_active); + DrawIntRangeSetting(bsi, "Shade Boost Saturation", "Adjusts saturation. 50 is normal.", "EmuCore/GS", "ShadeBoost_Saturation", 50, + 1, 100, "%d", shadeboost_active); + + static constexpr const char* s_tv_shaders[] = { + "None", "Scanline Filter", "Diagonal Filter", "Triangular Filter", "Wave Filter", "Lottes CRT"}; + DrawIntListSetting( + bsi, "TV Shaders", "Selects post-processing TV shader.", "EmuCore/GS", "TVShader", 0, s_tv_shaders, std::size(s_tv_shaders)); + } + + static constexpr const char* s_gsdump_compression[] = {"Uncompressed", "LZMA (xz)", "Zstandard (zst)"}; + + MenuHeading("Advanced"); + DrawToggleSetting(bsi, "Skip Presenting Duplicate Frames", + "Skips displaying frames that don't change in 25/30fps games. Can improve speed but increase input lag/make frame pacing worse.", + "EmuCore/GS", "SkipDuplicateFrames", false); + DrawToggleSetting(bsi, "Disable Hardware Readbacks", + "Skips thread synchronization for GS downloads. Can improve speed, but break graphical effects.", "EmuCore/GS", + "HWDisableReadbacks", false); + DrawIntListSetting(bsi, "Override Texture Barriers", "Forces texture barrier functionality to the specified value.", "EmuCore/GS", + "OverrideTextureBarriers", -1, s_generic_options, std::size(s_generic_options), -1); + DrawIntListSetting(bsi, "Override Geometry Shaders", "Forces geometry shader functionality to the specified value.", "EmuCore/GS", + "OverrideGeometryShaders", -1, s_generic_options, std::size(s_generic_options), -1); + DrawIntListSetting(bsi, "GS Dump Compression", "Sets the compression algorithm for GS dumps.", "EmuCore/GS", "GSDumpCompression", + static_cast(GSDumpCompressionMethod::LZMA), s_gsdump_compression, std::size(s_gsdump_compression)); + DrawToggleSetting(bsi, "Disable Framebuffer Fetch", "Prevents the usage of framebuffer fetch when supported by host GPU.", "EmuCore/GS", + "DisableFramebufferFetch", false); + DrawToggleSetting(bsi, "Disable Dual-Source Blending", "Prevents the usage of dual-source blending when supported by host GPU.", + "EmuCore/GS", "DisableDualSourceBlend", false); + + EndMenuButtons(); +} + +void FullscreenUI::DrawAudioSettingsPage() +{ + static constexpr const char* interpolation_modes[] = { + "Nearest (Fastest / worst quality)", + "Linear (Simple / okay sound)", + "Cubic (Fake highs / okay sound)", + "Hermite (Better highs / okay sound)", + "Catmull-Rom (PS2-like / good sound)", + "Gaussian (PS2-like / great sound)", + }; + static constexpr const char* synchronization_modes[] = { + "TimeStretch (Recommended)", + "Async Mix (Breaks some games!)", + "None (Audio can skip.)", + }; + static constexpr const char* expansion_modes[] = { + "Stereo (None, Default)", + "Quadrafonic", + "Surround 5.1", + "Surround 7.1", + }; + static constexpr const char* output_entries[] = { + "No Sound (Emulate SPU2 only)", +#ifdef SPU2X_CUBEB + "Cubeb (Cross-platform)", +#endif +#ifdef _WIN32 + "XAudio2", +#endif + }; + static constexpr const char* output_values[] = { + "nullout", +#ifdef SPU2X_CUBEB + "cubeb", +#endif +#ifdef _WIN32 + "xaudio2", +#endif + }; +#if defined(SPU2X_CUBEB) + static constexpr const char* default_output_module = "cubeb"; +#elif defined(_WIN32) + static constexpr const char* default_output_module = "xaudio2"; +#else + static constexpr const char* default_output_module = "nullout"; +#endif + + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + MenuHeading("Runtime Settings"); + DrawIntRangeSetting(bsi, ICON_FA_VOLUME_UP " Output Volume", "Applies a global volume modifier to all sound produced by the game.", + "SPU2/Mixing", "FinalVolume", 100, 0, 100, "%d%%"); + + MenuHeading("Mixing Settings"); + DrawIntListSetting(bsi, ICON_FA_MUSIC " Interpolation Mode", "Determines how ADPCM samples are interpolated to the target pitch.", + "SPU2/Mixing", "Interpolation", static_cast(Pcsx2Config::SPU2Options::InterpolationMode::Gaussian), interpolation_modes, + std::size(interpolation_modes)); + DrawIntListSetting(bsi, ICON_FA_RULER " Synchronization Mode", "Changes when SPU samples are generated relative to system emulation.", + "SPU2/Output", "SynchMode", static_cast(Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch), synchronization_modes, + std::size(synchronization_modes)); + DrawIntListSetting(bsi, ICON_FA_PLUS " Expansion Mode", "Determines how the stereo output is transformed to greater speaker counts.", + "SPU2/Output", "SpeakerConfiguration", 0, expansion_modes, std::size(expansion_modes)); + + MenuHeading("Output Settings"); + DrawStringListSetting(bsi, ICON_FA_PLAY_CIRCLE " Output Module", + "Determines which API is used to play back audio samples on the host.", "SPU2/Output", "OutputModule", default_output_module, + output_entries, output_values, std::size(output_entries)); + DrawIntRangeSetting(bsi, ICON_FA_CLOCK " Latency", "Sets the average output latency when using the cubeb backend.", "SPU2/Output", + "Latency", 100, 15, 200, "%d ms (avg)"); + + MenuHeading("Timestretch Settings"); + DrawIntRangeSetting(bsi, ICON_FA_RULER_HORIZONTAL " Sequence Length", + "Affects how the timestretcher operates when not running at 100% speed.", "Soundtouch", "SequenceLengthMS", 30, 20, 100, "%d ms"); + DrawIntRangeSetting(bsi, ICON_FA_WINDOW_MAXIMIZE " Seekwindow Size", + "Affects how the timestretcher operates when not running at 100% speed.", "Soundtouch", "SeekWindowMS", 20, 10, 30, "%d ms"); + DrawIntRangeSetting(bsi, ICON_FA_RECEIPT " Overlap", "Affects how the timestretcher operates when not running at 100% speed.", + "Soundtouch", "OverlapMS", 20, 5, 15, "%d ms"); + + EndMenuButtons(); +} + +void FullscreenUI::DrawMemoryCardSettingsPage() +{ + BeginMenuButtons(); + + SettingsInterface* bsi = GetEditingSettingsInterface(); + + MenuHeading("Settings and Operations"); + if (MenuButton(ICON_FA_PLUS " Create Memory Card", "Creates a new memory card file or folder.")) + ImGui::OpenPopup("Create Memory Card"); + DrawCreateMemoryCardWindow(); + + DrawFolderSetting(bsi, ICON_FA_FOLDER_OPEN " Memory Card Directory", "Folders", "MemoryCards", EmuFolders::MemoryCards); + DrawToggleSetting(bsi, ICON_FA_SEARCH " Folder Memory Card Filter", + "Simulates a larger memory card by filtering saves only to the current game.", "EmuCore", "McdFolderAutoManage", true); + DrawToggleSetting(bsi, ICON_FA_MAGIC " Auto Eject When Loading", + "Automatically ejects memory cards when they differ after loading a state.", "EmuCore", "McdEnableEjection", true); + + for (u32 port = 0; port < NUM_MEMORY_CARD_PORTS; port++) + { + const std::string title(fmt::format("Console Port {}", port + 1)); + MenuHeading(title.c_str()); + + std::string enable_key(fmt::format("Slot{}_Enable", port + 1)); + std::string file_key(fmt::format("Slot{}_Filename", port + 1)); + + DrawToggleSetting(bsi, fmt::format(ICON_FA_SD_CARD " Card Enabled##card_enabled_{}", port).c_str(), + "If not set, this card will be considered unplugged.", "MemoryCards", enable_key.c_str(), true); + + const bool enabled = GetEffectiveBoolSetting(bsi, "MemoryCards", enable_key.c_str(), true); + + std::optional value(bsi->GetOptionalStringValue("MemoryCards", file_key.c_str(), + IsEditingGameSettings(bsi) ? std::nullopt : std::optional(FileMcd_GetDefaultName(port).c_str()))); + + if (MenuButtonWithValue(fmt::format(ICON_FA_FILE " Card Name##card_name_{}", port).c_str(), + "The selected memory card image will be used for this slot.", value.has_value() ? value->c_str() : "Use Global Setting", + enabled)) + { + ImGuiFullscreen::ChoiceDialogOptions options; + std::vector names; + if (IsEditingGameSettings(bsi)) + options.emplace_back("Use Global Setting", !value.has_value()); + if (value.has_value() && !value->empty()) + { + options.emplace_back(fmt::format("{} (Current)", value.value()), true); + names.push_back(std::move(value.value())); + } + for (AvailableMcdInfo& mci : FileMcd_GetAvailableCards(IsEditingGameSettings(bsi))) + { + if (mci.type == MemoryCardType::Folder) + { + options.emplace_back(fmt::format("{} (Folder)", mci.name), false); + } + else + { + static constexpr const char* file_type_names[] = { + "Unknown", "PS2 (8MB)", "PS2 (16MB)", "PS2 (32MB)", "PS2 (64MB)", "PS1"}; + options.emplace_back(fmt::format("{} ({})", mci.name, file_type_names[static_cast(mci.file_type)]), false); + } + names.push_back(std::move(mci.name)); + } + OpenChoiceDialog(title.c_str(), false, std::move(options), + [game_settings = IsEditingGameSettings(bsi), names = std::move(names), file_key = std::move(file_key)]( + s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + if (game_settings && index == 0) + { + bsi->DeleteValue("MemoryCards", file_key.c_str()); + } + else + { + if (game_settings) + index--; + bsi->SetStringValue("MemoryCards", file_key.c_str(), names[index].c_str()); + } + SetSettingsChanged(bsi); + CloseChoiceDialog(); + }); + } + + if (MenuButton( + fmt::format(ICON_FA_EJECT " Eject Card##eject_card_{}", port).c_str(), "Resets the card name for this slot.", enabled)) + { + bsi->SetStringValue("MemoryCards", file_key.c_str(), ""); + SetSettingsChanged(bsi); + } + } + + + EndMenuButtons(); +} + +void FullscreenUI::DrawCreateMemoryCardWindow() +{ + ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + ImGui::PushFont(g_large_font); + + bool is_open = true; + if (ImGui::BeginPopupModal("Create Memory Card", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + { + ImGui::TextWrapped("Enter the name of the memory card you wish to create, and choose a size. We recommend either using 8MB memory " + "cards, or folder memory cards for best compatibility."); + ImGui::NewLine(); + + static char memcard_name[256] = {}; + ImGui::Text("Card Name: "); + ImGui::InputText("##name", memcard_name, sizeof(memcard_name)); + + ImGui::NewLine(); + + static constexpr std::tuple memcard_types[] = { + {"8 MB [Most Compatible]", MemoryCardType::File, MemoryCardFileType::PS2_8MB}, + {"16 MB", MemoryCardType::File, MemoryCardFileType::PS2_8MB}, + {"32 MB", MemoryCardType::File, MemoryCardFileType::PS2_8MB}, + {"64 MB", MemoryCardType::File, MemoryCardFileType::PS2_8MB}, + {"Folder [Recommended]", MemoryCardType::Folder, MemoryCardFileType::PS2_8MB}, + {"128 KB [PS1]", MemoryCardType::File, MemoryCardFileType::PS1}, + }; + + static int memcard_type = 0; + for (int i = 0; i < static_cast(std::size(memcard_types)); i++) + ImGui::RadioButton(std::get<0>(memcard_types[i]), &memcard_type, i); + + ImGui::NewLine(); + + BeginMenuButtons(); + + const bool create_enabled = (std::strlen(memcard_name) > 0); + + if (ActiveButton(ICON_FA_FOLDER_OPEN " Create", false, create_enabled) && std::strlen(memcard_name) > 0) + { + const std::string real_card_name(fmt::format("{}.ps2", memcard_name)); + if (!FileMcd_GetCardInfo(real_card_name).has_value()) + { + const auto& [type_title, type, file_type] = memcard_types[memcard_type]; + if (FileMcd_CreateNewCard(real_card_name, type, file_type)) + { + ShowToast(std::string(), fmt::format("Memory card '{}' created.", real_card_name)); + + std::memset(memcard_name, 0, sizeof(memcard_name)); + memcard_type = 0; + ImGui::CloseCurrentPopup(); + } + else + { + ShowToast(std::string(), fmt::format("Failed to create memory card '{}'.", real_card_name)); + } + } + else + { + ShowToast(std::string(), fmt::format("A memory card with the name '{}' already exists.", real_card_name)); + } + } + + if (ActiveButton(ICON_FA_TIMES " Cancel", false)) + { + std::memset(memcard_name, 0, sizeof(memcard_name)); + memcard_type = 0; + + ImGui::CloseCurrentPopup(); + } + + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopFont(); + ImGui::PopStyleVar(2); +} + +void FullscreenUI::CopyGlobalControllerSettingsToGame() +{ + SettingsInterface* dsi = GetEditingSettingsInterface(true); + SettingsInterface* ssi = GetEditingSettingsInterface(false); + + PAD::CopyConfiguration(dsi, *ssi, true, true, false); + SetSettingsChanged(dsi); + + ShowToast(std::string(), "Per-game controller configuration initialized with global settings."); +} + +void FullscreenUI::ResetControllerSettings() +{ + SettingsInterface* dsi = GetEditingSettingsInterface(); + + PAD::SetDefaultConfig(*dsi); + ShowToast(std::string(), "Controller settings reset to default."); +} + +void FullscreenUI::DoLoadInputProfile() +{ + std::vector profiles(PAD::GetInputProfileNames()); + if (profiles.empty()) + { + ShowToast(std::string(), "No input profiles available."); + return; + } + + ImGuiFullscreen::ChoiceDialogOptions coptions; + coptions.reserve(profiles.size()); + for (std::string& name : profiles) + coptions.emplace_back(std::move(name), false); + OpenChoiceDialog( + ICON_FA_FOLDER_OPEN " Load Profile", false, std::move(coptions), [](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + INISettingsInterface ssi(VMManager::GetInputProfilePath(title)); + if (!ssi.Load()) + { + ShowToast(std::string(), fmt::format("Failed to load '{}'.", title)); + CloseChoiceDialog(); + return; + } + + auto lock = Host::GetSettingsLock(); + SettingsInterface* dsi = GetEditingSettingsInterface(); + PAD::CopyConfiguration(dsi, ssi, true, true, IsEditingGameSettings(dsi)); + SetSettingsChanged(dsi); + ShowToast(std::string(), fmt::format("Input profile '{}' loaded.", title)); + CloseChoiceDialog(); + }); +} + +void FullscreenUI::DoSaveInputProfile(const std::string& name) +{ + INISettingsInterface dsi(VMManager::GetInputProfilePath(name)); + + auto lock = Host::GetSettingsLock(); + SettingsInterface* ssi = GetEditingSettingsInterface(); + PAD::CopyConfiguration(&dsi, *ssi, true, true, IsEditingGameSettings(ssi)); + if (dsi.Save()) + ShowToast(std::string(), fmt::format("Input profile '{}' saved.", name)); + else + ShowToast(std::string(), fmt::format("Failed to save input profile '{}'.", name)); +} + +void FullscreenUI::DoSaveInputProfile() +{ + std::vector profiles(PAD::GetInputProfileNames()); + if (profiles.empty()) + { + ShowToast(std::string(), "No input profiles available."); + return; + } + + ImGuiFullscreen::ChoiceDialogOptions coptions; + coptions.reserve(profiles.size() + 1); + coptions.emplace_back("Create New...", false); + for (std::string& name : profiles) + coptions.emplace_back(std::move(name), false); + OpenChoiceDialog(ICON_FA_SAVE " Save Profile", false, std::move(coptions), [](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + if (index > 0) + { + DoSaveInputProfile(title); + CloseChoiceDialog(); + return; + } + + CloseChoiceDialog(); + + OpenInputStringDialog(ICON_FA_SAVE " Save Profile", "Enter the name of the input profile you wish to create.", std::string(), + ICON_FA_FOLDER_PLUS " Create", [](std::string title) { + if (!title.empty()) + DoSaveInputProfile(title); + }); + }); +} + +void FullscreenUI::DrawControllerSettingsPage() +{ + BeginMenuButtons(); + + SettingsInterface* bsi = GetEditingSettingsInterface(); + + MenuHeading("Configuration"); + + if (IsEditingGameSettings(bsi)) + { + if (DrawToggleSetting(bsi, ICON_FA_COG " Per-Game Configuration", "Uses game-specific settings for controllers for this game.", + "Pad", "UseGameSettingsForController", false, IsEditingGameSettings(bsi), false)) + { + // did we just enable per-game for the first time? + if (bsi->GetBoolValue("Pad", "UseGameSettingsForController", false) && + !bsi->GetBoolValue("Pad", "GameSettingsInitialized", false)) + { + bsi->SetBoolValue("Pad", "GameSettingsInitialized", true); + CopyGlobalControllerSettingsToGame(); + } + } + } + + if (IsEditingGameSettings(bsi) && !bsi->GetBoolValue("Pad", "UseGameSettingsForController", false)) + { + // nothing to edit.. + EndMenuButtons(); + return; + } + + if (IsEditingGameSettings(bsi)) + { + if (MenuButton(ICON_FA_COPY " Copy Global Settings", "Copies the global controller configuration to this game.")) + CopyGlobalControllerSettingsToGame(); + } + else + { + if (MenuButton(ICON_FA_FOLDER_MINUS " Reset Settings", "Resets all configuration to defaults (including bindings).")) + ResetControllerSettings(); + } + + if (MenuButton(ICON_FA_FOLDER_OPEN " Load Profile", "Replaces these settings with a previously saved input profile.")) + DoLoadInputProfile(); + if (MenuButton(ICON_FA_SAVE " Save Profile", "Stores the current settings to an input profile.")) + DoSaveInputProfile(); + + MenuHeading("Input Sources"); + +#ifdef SDL_BUILD + DrawToggleSetting(bsi, ICON_FA_COG " Enable SDL Input Source", "The SDL input source supports most controllers.", "InputSources", + "SDL", true, true, false); + DrawToggleSetting(bsi, ICON_FA_WIFI " SDL DualShock 4 / DualSense Enhanced Mode", + "Provides vibration and LED control support over Bluetooth.", "InputSources", "SDLControllerEnhancedMode", false, + bsi->GetBoolValue("InputSources", "SDL", true), false); +#endif +#ifdef _WIN32 + DrawToggleSetting(bsi, ICON_FA_COG " Enable XInput Input Source", + "The XInput source provides support for XBox 360/XBox One/XBox Series controllers.", "InputSources", "XInput", false, true, false); +#endif + + MenuHeading("Multitap"); + DrawToggleSetting(bsi, ICON_FA_PLUS_SQUARE " Enable Console Port 1 Multitap", + "Enables an additional three controller slots. Not supported in all games.", "Pad", "MultitapPort1", false, true, false); + DrawToggleSetting(bsi, ICON_FA_PLUS_SQUARE " Enable Console Port 2 Multitap", + "Enables an additional three controller slots. Not supported in all games.", "Pad", "MultitapPort2", false, true, false); + + const std::array mtap_enabled = { + {bsi->GetBoolValue("Pad", "MultitapPort1", false), bsi->GetBoolValue("Pad", "MultitapPort2", false)}}; + + // we reorder things a little to make it look less silly for mtap + static constexpr const std::array mtap_slot_names = {{'A', 'B', 'C', 'D'}}; + static constexpr const std::array mtap_port_order = {{0, 2, 3, 4, 1, 5, 6, 7}}; + + // create the ports + for (u32 global_slot : mtap_port_order) + { + const bool is_mtap_port = sioPadIsMultitapSlot(global_slot); + const auto [mtap_port, mtap_slot] = sioConvertPadToPortAndSlot(global_slot); + if (is_mtap_port && !mtap_enabled[mtap_port]) + continue; + + MenuHeading( + (mtap_enabled[mtap_port] ? fmt::format(ICON_FA_PLUG " Controller Port {}{}", mtap_port + 1, mtap_slot_names[mtap_slot]) : + fmt::format(ICON_FA_PLUG " Controller Port {}", mtap_port + 1)) + .c_str()); + + const std::string section(fmt::format("Pad{}", global_slot + 1)); + const std::string type(bsi->GetStringValue(section.c_str(), "Type", PAD::GetDefaultPadType(global_slot))); + const PAD::ControllerInfo* ci = PAD::GetControllerInfo(type); + if (MenuButton(fmt::format(ICON_FA_GAMEPAD " Controller Type##type{}", global_slot).c_str(), ci ? ci->display_name : "Unknown")) + { + std::vector> raw_options(PAD::GetControllerTypeNames()); + ImGuiFullscreen::ChoiceDialogOptions options; + options.reserve(raw_options.size()); + for (auto& it : raw_options) + { + options.emplace_back(std::move(it.second), type == it.first); + } + OpenChoiceDialog(fmt::format("Port {} Controller Type", global_slot + 1).c_str(), false, std::move(options), + [game_settings = IsEditingGameSettings(bsi), section, raw_options = std::move(raw_options)]( + s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + bsi->SetStringValue(section.c_str(), "Type", raw_options[index].first.c_str()); + SetSettingsChanged(bsi); + CloseChoiceDialog(); + }); + } + + if (!ci || ci->num_bindings == 0) + continue; + + if (MenuButton(ICON_FA_MAGIC " Automatic Mapping", "Attempts to map the selected port to a chosen controller.")) + StartAutomaticBinding(global_slot); + + for (u32 i = 0; i < ci->num_bindings; i++) + { + const PAD::ControllerBindingInfo& bi = ci->bindings[i]; + DrawInputBindingButton(bsi, bi.type, section.c_str(), bi.name, bi.display_name, true); + } + + MenuHeading((mtap_enabled[mtap_port] ? + fmt::format(ICON_FA_MICROCHIP " Controller Port {}{} Macros", mtap_port + 1, mtap_slot_names[mtap_slot]) : + fmt::format(ICON_FA_MICROCHIP " Controller Port {} Macros", mtap_port + 1)) + .c_str()); + + for (u32 macro_index = 0; macro_index < PAD::NUM_MACRO_BUTTONS_PER_CONTROLLER; macro_index++) + { + DrawInputBindingButton(bsi, PAD::ControllerBindingType::Macro, section.c_str(), fmt::format("Macro{}", macro_index + 1).c_str(), + fmt::format("Macro {} Trigger", macro_index + 1).c_str()); + + std::string binds_string(bsi->GetStringValue(section.c_str(), fmt::format("Macro{}Binds", macro_index + 1).c_str())); + if (MenuButton(fmt::format(ICON_FA_KEYBOARD " Macro {} Buttons", macro_index + 1).c_str(), + binds_string.empty() ? "No Buttons Selected" : binds_string.c_str())) + { + std::vector buttons_split(StringUtil::SplitString(binds_string, '&', true)); + ImGuiFullscreen::ChoiceDialogOptions options; + for (u32 i = 0; i < ci->num_bindings; i++) + { + const PAD::ControllerBindingInfo& bi = ci->bindings[i]; + if (bi.type != PAD::ControllerBindingType::Button && bi.type != PAD::ControllerBindingType::Axis && + bi.type != PAD::ControllerBindingType::HalfAxis) + { + continue; + } + options.emplace_back(bi.display_name, std::any_of(buttons_split.begin(), buttons_split.end(), + [bi](const std::string_view& it) { return (it == bi.name); })); + } + + OpenChoiceDialog(fmt::format("Select Macro {} Binds", macro_index + 1).c_str(), true, std::move(options), + [section, macro_index, ci](s32 index, const std::string& title, bool checked) { + // convert display name back to bind name + std::string_view to_modify; + for (u32 j = 0; j < ci->num_bindings; j++) + { + const PAD::ControllerBindingInfo& bi = ci->bindings[j]; + if (bi.display_name == title) + { + to_modify = bi.name; + break; + } + } + if (to_modify.empty()) + { + // wtf? + return; + } + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(); + const std::string key(fmt::format("Macro{}Binds", macro_index + 1)); + + std::string binds_string(bsi->GetStringValue(section.c_str(), key.c_str())); + std::vector buttons_split(StringUtil::SplitString(binds_string, '&', true)); + auto it = std::find(buttons_split.begin(), buttons_split.end(), to_modify); + if (checked) + { + if (it == buttons_split.end()) + buttons_split.push_back(to_modify); + } + else + { + if (it != buttons_split.end()) + buttons_split.erase(it); + } + + binds_string = StringUtil::JoinString(buttons_split.begin(), buttons_split.end(), " & "); + if (binds_string.empty()) + bsi->DeleteValue(section.c_str(), key.c_str()); + else + bsi->SetStringValue(section.c_str(), key.c_str(), binds_string.c_str()); + }); + } + + const std::string freq_key(fmt::format("Macro{}Frequency", macro_index + 1)); + const std::string freq_title(fmt::format(ICON_FA_LIGHTBULB " Macro {} Frequency", macro_index + 1)); + s32 frequency = bsi->GetIntValue(section.c_str(), freq_key.c_str(), 0); + const std::string freq_summary((frequency == 0) ? std::string("Macro will not auto-toggle.") : + fmt::format("Macro will toggle every {} frames.", frequency)); + if (MenuButton(freq_title.c_str(), freq_summary.c_str())) + ImGui::OpenPopup(freq_title.c_str()); + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f)); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + if (ImGui::BeginPopupModal( + freq_title.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::SetNextItemWidth(LayoutScale(450.0f)); + if (ImGui::SliderInt("##value", &frequency, 0, 60, "Toggle every %d frames", ImGuiSliderFlags_NoInput)) + { + if (frequency == 0) + bsi->DeleteValue(section.c_str(), freq_key.c_str()); + else + bsi->SetIntValue(section.c_str(), freq_key.c_str(), frequency); + } + + BeginMenuButtons(); + if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::CloseCurrentPopup(); + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(3); + ImGui::PopFont(); + } + + if (ci->num_settings > 0) + { + MenuHeading((mtap_enabled[mtap_port] ? + fmt::format(ICON_FA_SLIDERS_H " Controller Port {}{} Settings", mtap_port + 1, mtap_slot_names[mtap_slot]) : + fmt::format(ICON_FA_SLIDERS_H " Controller Port {} Settings", mtap_port + 1)) + .c_str()); + + for (u32 i = 0; i < ci->num_settings; i++) + { + const PAD::ControllerSettingInfo& si = ci->settings[i]; + std::string title(fmt::format(ICON_FA_COG " {}", si.display_name)); + switch (si.type) + { + case PAD::ControllerSettingInfo::Type::Boolean: + DrawToggleSetting( + bsi, title.c_str(), si.description, section.c_str(), si.name, si.BooleanDefaultValue(), true, false); + break; + case PAD::ControllerSettingInfo::Type::Integer: + DrawIntRangeSetting(bsi, title.c_str(), si.description, section.c_str(), si.name, si.IntegerDefaultValue(), + si.IntegerMinValue(), si.IntegerMaxValue(), si.format, true); + break; + case PAD::ControllerSettingInfo::Type::Float: + DrawFloatRangeSetting(bsi, title.c_str(), si.description, section.c_str(), si.name, si.FloatDefaultValue(), + si.FloatMinValue(), si.FloatMaxValue(), si.format, si.multiplier, true); + break; + default: + break; + } + } + } + } + + EndMenuButtons(); +} + +void FullscreenUI::DrawHotkeySettingsPage() +{ + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + InputManager::GetHotkeyList(); + + const HotkeyInfo* last_category = nullptr; + for (const HotkeyInfo* hotkey : s_hotkey_list_cache) + { + if (!last_category || hotkey->category != last_category->category) + { + MenuHeading(hotkey->category); + last_category = hotkey; + } + + DrawInputBindingButton(bsi, PAD::ControllerBindingType::Button, "Hotkeys", hotkey->name, hotkey->display_name, false); + } + + EndMenuButtons(); +} + +void FullscreenUI::DrawAchievementsSettingsPage() +{ + // TODO: Implement once achievements are merged. + + BeginMenuButtons(); + ActiveButton(ICON_FA_BAN " This build was not compiled with Achivements support.", false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + EndMenuButtons(); +} + +void FullscreenUI::DrawAdvancedSettingsPage() +{ + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + MenuHeading("Logging"); + + DrawToggleSetting(bsi, "System Console", "Writes log messages to the system console (console window/standard output).", "Logging", + "EnableSystemConsole", false); + DrawToggleSetting(bsi, "File Logging", "Writes log messages to emulog.txt.", "Logging", "EnableFileLogging", false); + DrawToggleSetting(bsi, "Verbose Logging", "Writes dev log messages to log sinks.", "Logging", "EnableVerbose", false, !IsDevBuild); + DrawToggleSetting(bsi, "Log Timestamps", "Writes timestamps alongside log messages.", "Logging", "EnableTimestamps", true); + DrawToggleSetting( + bsi, "EE Console", "Writes debug messages from the game's EE code to the console.", "Logging", "EnableEEConsole", true); + DrawToggleSetting( + bsi, "IOP Console", "Writes debug messages from the game's IOP code to the console.", "Logging", "EnableIOPConsole", true); + DrawToggleSetting(bsi, "CDVD Verbose Reads", "Logs disc reads from games.", "EmuCore", "CdvdVerboseReads", false); + + MenuHeading("Advanced System"); + + DrawToggleSetting(bsi, "Enable EE Recompiler", + "Performs just-in-time binary translation of 64-bit MIPS-IV machine code to native code.", "EmuCore/CPU/Recompiler", "EnableEE", + true); + DrawToggleSetting( + bsi, "Enable EE Cache", "Enables simulation of the EE's cache. Slow.", "EmuCore/CPU/Recompiler", "EnableEECache", false); + DrawToggleSetting(bsi, "Enable INTC Spin Detection", "Huge speedup for some games, with almost no compatibility side effects.", + "EmuCore/Speedhacks", "IntcStat", true); + DrawToggleSetting(bsi, "Enable Wait Loop Detection", "Moderate speedup for some games, with no known side effects.", + "EmuCore/Speedhacks", "WaitLoop", true); + DrawToggleSetting(bsi, "Enable VU0 Recompiler (Micro Mode)", + "New Vector Unit recompiler with much improved compatibility. Recommended.", "EmuCore/CPU/Recompiler", "EnableVU0", true); + DrawToggleSetting(bsi, "Enable VU1 Recompiler", "New Vector Unit recompiler with much improved compatibility. Recommended.", + "EmuCore/CPU/Recompiler", "EnableVU1", true); + DrawToggleSetting(bsi, "Enable VU Flag Optimization", "Good speedup and high compatibility, may cause graphical errors.", + "EmuCore/Speedhacks", "vuFlagHack", true); + DrawToggleSetting(bsi, "Enable IOP Recompiler", + "Performs just-in-time binary translation of 32-bit MIPS-I machine code to native code.", "EmuCore/CPU/Recompiler", "EnableIOP", + true); + + MenuHeading("Graphics"); + + DrawToggleSetting(bsi, "Use Debug Device", "Enables API-level validation of graphics commands", "EmuCore/GS", "UseDebugDevice", false); + + + + EndMenuButtons(); +} + +void FullscreenUI::DrawGameFixesSettingsPage() +{ + SettingsInterface* bsi = GetEditingSettingsInterface(); + + BeginMenuButtons(); + + MenuHeading("Game Fixes"); + ActiveButton("Game fixes should not be modified unless you are aware of what each option does and the implications of doing so.", false, + false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + DrawToggleSetting(bsi, "FPU Multiply Hack", "For Tales of Destiny.", "EmuCore/Gamefixes", "FpuMulHack", false); + DrawToggleSetting(bsi, "FPU Negative Div Hack", "For Gundam games.", "EmuCore/Gamefixes", "FpuNegDivHack", false); + DrawToggleSetting(bsi, "Preload TLB Hack", "To avoid tlb miss on Goemon.", "EmuCore/Gamefixes", "GoemonTlbHack", false); + DrawToggleSetting(bsi, "Switch to Software renderer for FMVs.", "Needed for some games with complex FMV rendering.", + "EmuCore/Gamefixes", "SoftwareRendererFMVHack", false); + DrawToggleSetting( + bsi, "Skip MPEG Hack", "Skips videos/FMVs in games to avoid game hanging/freezes.", "EmuCore/Gamefixes", "SkipMPEGHack", false); + DrawToggleSetting(bsi, "OPH Flag Hack", "Known to affect following games: Bleach Blade Battler, Growlanser II and III, Wizardry.", + "EmuCore/Gamefixes", "OPHFlagHack", false); + DrawToggleSetting(bsi, "EE Timing Hack", + "Known to affect following games: Digital Devil Saga (Fixes FMV and crashes), SSX (Fixes bad graphics and crashes).", + "EmuCore/Gamefixes", "EETimingHack", false); + DrawToggleSetting(bsi, "Handle DMAC writes when it is busy.", + "Known to affect following games: Mana Khemia 1 (Going \"off campus\"), Metal Saga (Intro FMV), Pilot Down Behind Enemy Lines.", + "EmuCore/Gamefixes", "DMABusyHack", false); + DrawToggleSetting(bsi, "Force GIF PATH3 transfers through FIFO", "(Fifa Street 2).", "EmuCore/Gamefixes", "GIFFIFOHack", false); + DrawToggleSetting(bsi, "Simulate VIF1 FIFO read ahead. Fixes slow loading games.", + "Known to affect following games: Test Drive Unlimited, Transformers.", "EmuCore/Gamefixes", "VIFFIFOHack", false); + DrawToggleSetting( + bsi, "Delay VIF1 Stalls (VIF1 FIFO)", "For SOCOM 2 HUD and Spy Hunter loading hang.", "EmuCore/Gamefixes", "VIF1StallHack", false); + DrawToggleSetting(bsi, "VU Add Hack", "Games that need this hack to boot: Star Ocean 3, Radiata Stories, Valkyrie Profile 2.", + "EmuCore/Gamefixes", "VuAddSubHack", false); + DrawToggleSetting(bsi, "VU I bit Hack avoid constant recompilation in some games", + "Scarface The World Is Yours, Crash Tag Team Racing.", "EmuCore/Gamefixes", "IbitHack", false); + DrawToggleSetting(bsi, "VU Sync (Run behind)", "To avoid sync problems when reading or writing VU registers.", "EmuCore/Gamefixes", + "VUSyncHack", false); + DrawToggleSetting( + bsi, "VU Overflow Hack", "To check for possible float overflows (Superman Returns).", "EmuCore/Gamefixes", "VUOverflowHack", false); + DrawToggleSetting(bsi, "VU XGkick Sync", "Use accurate timing for VU XGKicks (slower).", "EmuCore/Gamefixes", "XgKickHack", false); + DrawToggleSetting(bsi, "Use Blit for internal FPS", + "Use alternative method to calclate internal FPS to avoid false readings in some games.", "EmuCore/Gamefixes", + "BlitInternalFPSHack", false); + + EndMenuButtons(); +} + +void FullscreenUI::DrawPauseMenu(MainWindowType type) +{ + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + const ImVec2 display_size(ImGui::GetIO().DisplaySize); + dl->AddRectFilled(ImVec2(0.0f, 0.0f), display_size, IM_COL32(0x21, 0x21, 0x21, 200)); + + // title info + { + const ImVec2 title_size( + g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, s_current_game_title.c_str())); + const ImVec2 subtitle_size(g_medium_font->CalcTextSizeA( + g_medium_font->FontSize, std::numeric_limits::max(), -1.0f, s_current_game_subtitle.c_str())); + + ImVec2 title_pos(display_size.x - LayoutScale(20.0f + 50.0f + 20.0f) - title_size.x, display_size.y - LayoutScale(20.0f + 50.0f)); + ImVec2 subtitle_pos(display_size.x - LayoutScale(20.0f + 50.0f + 20.0f) - subtitle_size.x, + title_pos.y + g_large_font->FontSize + LayoutScale(4.0f)); + float rp_height = 0.0f; + + dl->AddText(g_large_font, g_large_font->FontSize, title_pos, IM_COL32(255, 255, 255, 255), s_current_game_title.c_str()); + dl->AddText(g_medium_font, g_medium_font->FontSize, subtitle_pos, IM_COL32(255, 255, 255, 255), s_current_game_subtitle.c_str()); + + const ImVec2 image_min( + display_size.x - LayoutScale(20.0f + 50.0f) - rp_height, display_size.y - LayoutScale(20.0f + 50.0f) - rp_height); + const ImVec2 image_max(image_min.x + LayoutScale(50.0f) + rp_height, image_min.y + LayoutScale(50.0f) + rp_height); + dl->AddImage(GetCoverForCurrentGame()->GetHandle(), image_min, image_max); + } + + const ImVec2 window_size(LayoutScale(500.0f, LAYOUT_SCREEN_HEIGHT)); + const ImVec2 window_pos(0.0f, display_size.y - window_size.y); + + if (BeginFullscreenWindow( + window_pos, window_size, "pause_menu", ImVec4(0.0f, 0.0f, 0.0f, 0.0f), 0.0f, 10.0f, ImGuiWindowFlags_NoBackground)) + { + static constexpr u32 submenu_item_count[] = { + 10, // None + 4, // Exit + }; + + const bool just_focused = ResetFocusHere(); + BeginMenuButtons(submenu_item_count[static_cast(s_current_pause_submenu)], 1.0f, ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + switch (s_current_pause_submenu) + { + case PauseSubMenu::None: + { + // NOTE: Menu close must come first, because otherwise VM destruction options will race. + const bool can_load_or_save_state = s_current_game_crc != 0; + + if (ActiveButton(ICON_FA_PLAY " Resume Game", false) || WantsToCloseMenu()) + ClosePauseMenu(); + + if (ActiveButton(ICON_FA_FAST_FORWARD " Toggle Frame Limit", false)) + { + ClosePauseMenu(); + DoToggleFrameLimit(); + } + + if (ActiveButton(ICON_FA_UNDO " Load State", false, can_load_or_save_state)) + { + if (OpenSaveStateSelector(true)) + s_current_main_window = MainWindowType::None; + } + + if (ActiveButton(ICON_FA_DOWNLOAD " Save State", false, can_load_or_save_state)) + { + if (OpenSaveStateSelector(false)) + s_current_main_window = MainWindowType::None; + } + + if (ActiveButton(ICON_FA_WRENCH " Game Properties", false, can_load_or_save_state)) + { + SwitchToGameSettings(); + } + + if (ActiveButton(ICON_FA_CAMERA " Save Screenshot", false)) + { + GSQueueSnapshot(std::string()); + ClosePauseMenu(); + } + + if (ActiveButton(GSConfig.UseHardwareRenderer() ? (ICON_FA_PAINT_BRUSH " Switch To Software Renderer") : + (ICON_FA_PAINT_BRUSH " Switch To Hardware Renderer"), + false)) + { + ClosePauseMenu(); + DoToggleSoftwareRenderer(); + } + + if (ActiveButton(ICON_FA_COMPACT_DISC " Change Disc", false)) + { + s_current_main_window = MainWindowType::None; + DoChangeDisc(); + } + + if (ActiveButton(ICON_FA_SLIDERS_H " Settings", false)) + SwitchToSettings(); + + if (ActiveButton(ICON_FA_POWER_OFF " Close Game", false)) + { + // skip submenu when we can't save anyway + if (!can_load_or_save_state) + DoShutdown(false); + else + OpenPauseSubMenu(PauseSubMenu::Exit); + } + } + break; + + case PauseSubMenu::Exit: + { + if (just_focused) + ImGui::SetFocusID(ImGui::GetID(ICON_FA_POWER_OFF " Exit Without Saving"), ImGui::GetCurrentWindow()); + + if (ActiveButton(ICON_FA_BACKWARD " Back To Pause Menu", false)) + { + OpenPauseSubMenu(PauseSubMenu::None); + } + + if (ActiveButton(ICON_FA_SYNC " Reset System", false)) + { + ClosePauseMenu(); + DoReset(); + } + + if (ActiveButton(ICON_FA_SAVE " Exit And Save State", false)) + DoShutdown(true); + + if (ActiveButton(ICON_FA_POWER_OFF " Exit Without Saving", false)) + DoShutdown(false); + } + break; + } + + EndMenuButtons(); + + EndFullscreenWindow(); + } +} + +void FullscreenUI::InitializePlaceholderSaveStateListEntry( + SaveStateListEntry* li, const std::string& title, const std::string& serial, u32 crc, s32 slot) +{ + li->title = fmt::format("{0} Slot {1}##game_slot_{1}", s_current_game_title, slot); + li->summary = "No Save State"; + li->path = {}; + li->slot = slot; + li->preview_texture = {}; +} + +bool FullscreenUI::InitializeSaveStateListEntry( + SaveStateListEntry* li, const std::string& title, const std::string& serial, u32 crc, s32 slot) +{ + std::string filename(VMManager::GetSaveStateFileName(serial.c_str(), crc, slot)); + FILESYSTEM_STAT_DATA sd; + if (filename.empty() || !FileSystem::StatFile(filename.c_str(), &sd)) + { + InitializePlaceholderSaveStateListEntry(li, title, serial, crc, slot); + return false; + } + + li->title = fmt::format("{0} Slot {1}##game_slot_{1}", title, slot); + li->summary = fmt::format("{0} - Saved {1}", serial, TimeToPrintableString(sd.ModificationTime)); + li->slot = slot; + li->path = std::move(filename); + + li->preview_texture.reset(); + + u32 screenshot_width, screenshot_height; + std::vector screenshot_pixels; + if (SaveState_ReadScreenshot(li->path, &screenshot_width, &screenshot_height, &screenshot_pixels)) + { + li->preview_texture = Host::GetHostDisplay()->CreateTexture( + screenshot_width, screenshot_height, screenshot_pixels.data(), sizeof(u32) * screenshot_width, false); + if (!li->preview_texture) + Console.Error("Failed to upload save state image to GPU"); + } + + return true; +} + +void FullscreenUI::ClearSaveStateEntryList() +{ + for (SaveStateListEntry& entry : s_save_state_selector_slots) + { + if (entry.preview_texture) + s_cleanup_textures.push_back(std::move(entry.preview_texture)); + } + s_save_state_selector_slots.clear(); +} + +u32 FullscreenUI::PopulateSaveStateListEntries(const std::string& title, const std::string& serial, u32 crc) +{ + ClearSaveStateEntryList(); + + for (s32 i = 0; i <= MAX_SAVE_STATE_SLOTS; i++) + { + SaveStateListEntry li; + if (InitializeSaveStateListEntry(&li, title, serial, crc, i) || !s_save_state_selector_loading) + s_save_state_selector_slots.push_back(std::move(li)); + } + + return static_cast(s_save_state_selector_slots.size()); +} + +bool FullscreenUI::OpenLoadStateSelectorForGame(const std::string& game_path) +{ + auto lock = GameList::GetLock(); + const GameList::Entry* entry = GameList::GetEntryForPath(game_path.c_str()); + if (entry) + { + s_save_state_selector_loading = true; + if (PopulateSaveStateListEntries(entry->title.c_str(), entry->serial.c_str(), entry->crc) > 0) + { + s_save_state_selector_open = true; + s_save_state_selector_game_path = game_path; + return true; + } + } + + ShowToast({}, "No save states found.", 5.0f); + return false; +} + +bool FullscreenUI::OpenSaveStateSelector(bool is_loading) +{ + s_save_state_selector_game_path = {}; + s_save_state_selector_loading = is_loading; + if (PopulateSaveStateListEntries(s_current_game_title.c_str(), s_current_game_serial.c_str(), s_current_game_crc) > 0) + { + s_save_state_selector_open = true; + return true; + } + + ShowToast({}, "No save states found.", 5.0f); + return false; +} + +void FullscreenUI::CloseSaveStateSelector() +{ + ClearSaveStateEntryList(); + s_save_state_selector_open = false; + s_save_state_selector_loading = false; + s_save_state_selector_game_path = {}; + ReturnToMainWindow(); +} + +void FullscreenUI::DrawSaveStateSelector(bool is_loading, bool fullscreen) +{ + if (fullscreen) + { + if (!BeginFullscreenColumns()) + { + EndFullscreenColumns(); + return; + } + + if (!BeginFullscreenColumnWindow(0.0f, LAYOUT_SCREEN_WIDTH, "save_state_selector_slots")) + { + EndFullscreenColumnWindow(); + EndFullscreenColumns(); + return; + } + } + else + { + const char* window_title = is_loading ? "Load State" : "Save State"; + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + + ImGui::SetNextWindowSize(LayoutScale(1000.0f, 680.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(window_title); + bool is_open = !WantsToCloseMenu(); + if (!ImGui::BeginPopupModal( + window_title, &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove) || + !is_open) + { + ImGui::PopStyleVar(2); + ImGui::PopFont(); + CloseSaveStateSelector(); + return; + } + } + + BeginMenuButtons(); + + constexpr float padding = 10.0f; + constexpr float button_height = 96.0f; + constexpr float max_image_width = 96.0f; + constexpr float max_image_height = 96.0f; + + for (const SaveStateListEntry& entry : s_save_state_selector_slots) + { + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(entry.title.c_str(), true, button_height, &visible, &hovered, &bb.Min, &bb.Max); + if (!visible) + continue; + + ImVec2 pos(bb.Min); + + // use aspect ratio of screenshot to determine height + const HostDisplayTexture* image = entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get(); + const float image_height = max_image_width / (static_cast(image->GetWidth()) / static_cast(image->GetHeight())); + const float image_margin = (max_image_height - image_height) / 2.0f; + const ImRect image_bb( + ImVec2(pos.x, pos.y + LayoutScale(image_margin)), pos + LayoutScale(max_image_width, image_margin + image_height)); + pos.x += LayoutScale(max_image_width + padding); + + ImRect text_bb(pos, ImVec2(bb.Max.x, pos.y + g_large_font->FontSize)); + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.title.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), &text_bb); + ImGui::PopFont(); + + ImGui::PushFont(g_medium_font); + + if (!entry.summary.empty()) + { + text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); + text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; + ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.summary.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), &text_bb); + } + + if (!entry.path.empty()) + { + text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); + text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; + ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.path.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), &text_bb); + } + + ImGui::PopFont(); + + ImGui::GetWindowDrawList()->AddImage( + static_cast(entry.preview_texture ? entry.preview_texture->GetHandle() : GetPlaceholderTexture()->GetHandle()), + image_bb.Min, image_bb.Max); + + if (pressed) + { + if (is_loading) + { + DoLoadState(entry.path); + CloseSaveStateSelector(); + } + else + { + Host::RunOnCPUThread([slot = entry.slot]() { VMManager::SaveStateToSlot(slot); }); + CloseSaveStateSelector(); + } + } + } + + EndMenuButtons(); + + if (fullscreen) + { + EndFullscreenColumnWindow(); + EndFullscreenColumns(); + } + else + { + ImGui::EndPopup(); + ImGui::PopStyleVar(2); + ImGui::PopFont(); + } +} + +void FullscreenUI::DoLoadState(std::string path) +{ + Host::RunOnCPUThread([boot_path = s_save_state_selector_game_path, path = std::move(path)]() { + if (VMManager::HasValidVM()) + { + VMManager::LoadState(path.c_str()); + if (!boot_path.empty() && VMManager::GetDiscPath() != boot_path) + VMManager::ChangeDisc(CDVD_SourceType::Iso, std::move(boot_path)); + } + else + { + VMBootParameters params; + params.filename = std::move(boot_path); + params.save_state = std::move(path); + if (VMManager::Initialize(params)) + VMManager::SetState(VMState::Running); + } + }); +} + +void FullscreenUI::PopulateGameListEntryList() +{ + const u32 count = GameList::GetEntryCount(); + s_game_list_sorted_entries.resize(count); + for (u32 i = 0; i < count; i++) + s_game_list_sorted_entries[i] = GameList::GetEntryByIndex(i); + + // TODO: Custom sort types + std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(), + [](const GameList::Entry* lhs, const GameList::Entry* rhs) { return lhs->title < rhs->title; }); +} + +void FullscreenUI::DrawGameListWindow() +{ + if (!BeginFullscreenColumns()) + { + EndFullscreenColumns(); + return; + } + + auto game_list_lock = GameList::GetLock(); + const GameList::Entry* selected_entry = nullptr; + PopulateGameListEntryList(); + + if (BeginFullscreenColumnWindow(0.0f, 750.0f, "game_list_entries")) + { + const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT * 0.68f, LAYOUT_MENU_BUTTON_HEIGHT)); + + ResetFocusHere(); + + BeginMenuButtons(); + + // TODO: replace with something not heap allocating + std::string summary; + + for (const GameList::Entry* entry : s_game_list_sorted_entries) + { + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(entry->path.c_str(), true, LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max); + if (!visible) + continue; + + HostDisplayTexture* cover_texture = GetGameListCover(entry); + + summary.clear(); + if (entry->serial.empty()) + fmt::format_to(std::back_inserter(summary), "{} - ", GameList::RegionToString(entry->region)); + else + fmt::format_to(std::back_inserter(summary), "{} - {} - ", entry->serial, GameList::RegionToString(entry->region)); + + const std::string_view filename(Path::GetFileName(entry->path)); + summary.append(filename); + + const ImRect image_rect(CenterImage(ImRect(bb.Min, bb.Min + image_size), + ImVec2(static_cast(cover_texture->GetWidth()), static_cast(cover_texture->GetHeight())))); + + ImGui::GetWindowDrawList()->AddImage(cover_texture->GetHandle(), image_rect.Min, image_rect.Max, ImVec2(0.0f, 0.0f), + ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255)); + + const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); + const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f); + const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max); + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, entry->title.c_str(), entry->title.c_str() + entry->title.size(), nullptr, + ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (!summary.empty()) + { + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped( + summary_bb.Min, summary_bb.Max, summary.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (pressed) + { + // launch game + DoStartPath(entry->path); + } + + if (hovered) + selected_entry = entry; + + if (selected_entry && + (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsNavInputTest(ImGuiNavInput_Input, ImGuiNavReadMode_Pressed))) + { + ImGuiFullscreen::ChoiceDialogOptions options = { + {"Open Game Properties", false}, + {"Resume Game", false}, + {"Load State", false}, + {"Default Boot", false}, + {"Fast Boot", false}, + {"Slow Boot", false}, + {"Close Menu", false}, + }; + + OpenChoiceDialog(selected_entry->title.c_str(), false, std::move(options), + [entry_path = selected_entry->path](s32 index, const std::string& title, bool checked) { + switch (index) + { + case 0: // Open Game Properties + SwitchToGameSettings(entry_path); + break; + case 1: // Resume Game + DoStartPath(entry_path, -1); + break; + case 2: // Load State + OpenLoadStateSelectorForGame(entry_path); + break; + case 3: // Default Boot + DoStartPath(entry_path); + break; + case 4: // Fast Boot + DoStartPath(entry_path, std::nullopt, true); + break; + case 5: // Slow Boot + DoStartPath(entry_path, std::nullopt, false); + break; + default: + break; + } + + CloseChoiceDialog(); + }); + } + } + + EndMenuButtons(); + } + EndFullscreenColumnWindow(); + + if (BeginFullscreenColumnWindow(750.0f, LAYOUT_SCREEN_WIDTH, "game_list_info", UIPrimaryDarkColor)) + { + const HostDisplayTexture* cover_texture = + selected_entry ? GetGameListCover(selected_entry) : GetTextureForGameListEntryType(GameList::EntryType::Count); + if (cover_texture) + { + const ImRect image_rect(CenterImage(LayoutScale(ImVec2(275.0f, 400.0f)), + ImVec2(static_cast(cover_texture->GetWidth()), static_cast(cover_texture->GetHeight())))); + + ImGui::SetCursorPos(LayoutScale(ImVec2(128.0f, 20.0f)) + image_rect.Min); + ImGui::Image(selected_entry ? GetGameListCover(selected_entry)->GetHandle() : + GetTextureForGameListEntryType(GameList::EntryType::Count)->GetHandle(), + image_rect.GetSize()); + } + + const float work_width = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); + constexpr float field_margin_y = 10.0f; + constexpr float start_x = 50.0f; + float text_y = 440.0f; + float text_width; + + ImGui::SetCursorPos(LayoutScale(start_x, text_y)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, field_margin_y)); + ImGui::PushTextWrapPos(LayoutScale(480.0f)); + ImGui::BeginGroup(); + + if (selected_entry) + { + // title + ImGui::PushFont(g_large_font); + const std::string_view title( + std::string_view(selected_entry->title).substr(0, (selected_entry->title.length() > 37) ? 37 : std::string_view::npos)); + text_width = ImGui::CalcTextSize(title.data(), title.data() + title.length(), false, work_width).x; + ImGui::SetCursorPosX((work_width - text_width) / 2.0f); + ImGui::TextWrapped( + "%.*s%s", static_cast(title.size()), title.data(), (title.length() == selected_entry->title.length()) ? "" : "..."); + ImGui::PopFont(); + + ImGui::PushFont(g_medium_font); + + // code + text_width = ImGui::CalcTextSize(selected_entry->serial.c_str(), nullptr, false, work_width).x; + ImGui::SetCursorPosX((work_width - text_width) / 2.0f); + ImGui::TextWrapped("%s", selected_entry->serial.c_str()); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 15.0f); + + // file tile + const std::string_view filename(Path::GetFileName(selected_entry->path)); + ImGui::TextWrapped("File: %.*s", static_cast(filename.size()), filename.data()); + + // crc + ImGui::Text("CRC: %08X", selected_entry->crc); + + // region + { + std::string flag_texture(fmt::format("icons/flags/{}.png", GameList::RegionToString(selected_entry->region))); + ImGui::TextUnformatted("Region: "); + ImGui::SameLine(); + ImGui::Image(GetCachedTextureAsync(flag_texture.c_str())->GetHandle(), LayoutScale(23.0f, 16.0f)); + ImGui::SameLine(); + ImGui::Text(" (%s)", GameList::RegionToString(selected_entry->region)); + } + + // compatibility + ImGui::TextUnformatted("Compatibility: "); + ImGui::SameLine(); + if (selected_entry->compatibility_rating != GameDatabaseSchema::Compatibility::Unknown) + { + ImGui::Image(s_game_compatibility_textures[static_cast(selected_entry->compatibility_rating) - 1]->GetHandle(), + LayoutScale(64.0f, 16.0f)); + ImGui::SameLine(); + } + ImGui::Text(" (%s)", GameList::EntryCompatibilityRatingToString(selected_entry->compatibility_rating)); + + // size + ImGui::Text("Size: %.2f MB", static_cast(selected_entry->total_size) / 1048576.0f); + + ImGui::PopFont(); + } + else + { + // title + const char* title = "No Game Selected"; + ImGui::PushFont(g_large_font); + text_width = ImGui::CalcTextSize(title, nullptr, false, work_width).x; + ImGui::SetCursorPosX((work_width - text_width) / 2.0f); + ImGui::TextWrapped("%s", title); + ImGui::PopFont(); + } + + ImGui::EndGroup(); + ImGui::PopTextWrapPos(); + ImGui::PopStyleVar(); + + ImGui::SetCursorPosY(ImGui::GetWindowHeight() - LayoutScale(50.0f)); + BeginMenuButtons(); + if (ActiveButton(ICON_FA_BACKWARD " Back", false)) + ReturnToMainWindow(); + EndMenuButtons(); + } + EndFullscreenColumnWindow(); + + EndFullscreenColumns(); +} + +void FullscreenUI::SwitchToGameList() +{ + s_current_main_window = MainWindowType::GameList; + QueueResetFocus(); +} + +HostDisplayTexture* FullscreenUI::GetGameListCover(const GameList::Entry* entry) +{ + // lookup and grab cover image + auto cover_it = s_cover_image_map.find(entry->path); + if (cover_it == s_cover_image_map.end()) + { + std::string cover_path(GameList::GetCoverImagePathForEntry(entry)); + cover_it = s_cover_image_map.emplace(entry->path, std::move(cover_path)).first; + } + + HostDisplayTexture* tex = (!cover_it->second.empty()) ? GetCachedTextureAsync(cover_it->second.c_str()) : nullptr; + return tex ? tex : GetTextureForGameListEntryType(entry->type); +} + +HostDisplayTexture* FullscreenUI::GetTextureForGameListEntryType(GameList::EntryType type) +{ + switch (type) + { + case GameList::EntryType::ELF: + return s_fallback_exe_texture.get(); + + case GameList::EntryType::PS1Disc: + case GameList::EntryType::PS2Disc: + default: + return s_fallback_disc_texture.get(); + } +} + +HostDisplayTexture* FullscreenUI::GetCoverForCurrentGame() +{ + auto lock = GameList::GetLock(); + + const GameList::Entry* entry = GameList::GetEntryForPath(s_current_game_path.c_str()); + if (!entry) + return s_fallback_disc_texture.get(); + + return GetGameListCover(entry); +} + +std::string FullscreenUI::GetNotificationImageForGame(const GameList::Entry* entry) +{ + std::string ret; + + if (entry) + ret = GameList::GetCoverImagePathForEntry(entry); + + return ret; +} + +std::string FullscreenUI::GetNotificationImageForGame(const std::string& game_path) +{ + auto lock = GameList::GetLock(); + const GameList::Entry* entry = GameList::GetEntryForPath(game_path.c_str()); + return entry ? GetNotificationImageForGame(entry) : std::string(); +} + +////////////////////////////////////////////////////////////////////////// +// Overlays +////////////////////////////////////////////////////////////////////////// + +void FullscreenUI::ExitFullscreenAndOpenURL(const std::string_view& url) +{ + Host::RunOnCPUThread([url = std::string(url)]() { + if (Host::IsFullscreen()) + Host::SetFullscreen(false); + + Host::OpenURL(url); + }); +} + +void FullscreenUI::CopyTextToClipboard(std::string title, const std::string_view& text) +{ + if (Host::CopyTextToClipboard(text)) + ShowToast(std::string(), std::move(title)); + else + ShowToast(std::string(), "Failed to copy text to clipboard."); +} + +void FullscreenUI::OpenAboutWindow() +{ + s_about_window_open = true; +} + +void FullscreenUI::DrawAboutWindow() +{ + ImGui::SetNextWindowSize(LayoutScale(1000.0f, 500.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup("About PCSX2"); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + if (ImGui::BeginPopupModal("About PCSX2", &s_about_window_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + { + ImGui::TextWrapped( + "PCSX2 is a free and open-source PlayStation 2 (PS2) emulator. Its purpose is to emulate the PS2's hardware, using a " + "combination of MIPS CPU Interpreters, Recompilers and a Virtual Machine which manages hardware states and PS2 system memory. " + "This allows you to play PS2 games on your PC, with many additional features and benefits."); + + ImGui::NewLine(); + + ImGui::TextWrapped("PlayStation 2 and PS2 are registered trademarks of Sony Interactive Entertainment. This application is not " + "affiliated in any way with Sony Interactive Entertainment."); + + ImGui::NewLine(); + + BeginMenuButtons(); + + if (ActiveButton(ICON_FA_GLOBE " Website", false)) + ExitFullscreenAndOpenURL(PCSX2_WEBSITE_URL); + + if (ActiveButton(ICON_FA_PERSON_BOOTH " Support Forums", false)) + ExitFullscreenAndOpenURL(PCSX2_FORUMS_URL); + + if (ActiveButton(ICON_FA_BUG " GitHub Repository", false)) + ExitFullscreenAndOpenURL(PCSX2_GITHUB_URL); + + if (ActiveButton(ICON_FA_NEWSPAPER " License", false)) + ExitFullscreenAndOpenURL(PCSX2_LICENSE_URL); + + if (ActiveButton(ICON_FA_WINDOW_CLOSE " Close", false)) + { + ImGui::CloseCurrentPopup(); + s_about_window_open = false; + } + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(2); + ImGui::PopFont(); +} + +bool FullscreenUI::DrawErrorWindow(const char* message) +{ + bool is_open = true; + + ImGuiFullscreen::BeginLayout(); + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup("ReportError"); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + + if (ImGui::BeginPopupModal("ReportError", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + { + ImGui::SetCursorPos(LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::TextWrapped("%s", message); + ImGui::GetCurrentWindow()->DC.CursorPos.y += LayoutScale(5.0f); + + BeginMenuButtons(); + + if (ActiveButton(ICON_FA_WINDOW_CLOSE " Close", false)) + { + ImGui::CloseCurrentPopup(); + is_open = false; + } + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(2); + ImGui::PopFont(); + + ImGuiFullscreen::EndLayout(); + return !is_open; +} + +bool FullscreenUI::DrawConfirmWindow(const char* message, bool* result) +{ + bool is_open = true; + + ImGuiFullscreen::BeginLayout(); + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup("ConfirmMessage"); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + + if (ImGui::BeginPopupModal("ConfirmMessage", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + { + ImGui::SetCursorPos(LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::TextWrapped("%s", message); + ImGui::GetCurrentWindow()->DC.CursorPos.y += LayoutScale(5.0f); + + BeginMenuButtons(); + + bool done = false; + + if (ActiveButton(ICON_FA_CHECK " Yes", false)) + { + *result = true; + done = true; + } + + if (ActiveButton(ICON_FA_TIMES " No", false)) + { + *result = false; + done = true; + } + if (done) + { + ImGui::CloseCurrentPopup(); + is_open = false; + } + + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(2); + ImGui::PopFont(); + + ImGuiFullscreen::EndLayout(); + return !is_open; +} + +FullscreenUI::ProgressCallback::ProgressCallback(std::string name) + : BaseProgressCallback() + , m_name(std::move(name)) +{ + ImGuiFullscreen::OpenBackgroundProgressDialog(m_name.c_str(), "", 0, 100, 0); +} + +FullscreenUI::ProgressCallback::~ProgressCallback() +{ + ImGuiFullscreen::CloseBackgroundProgressDialog(m_name.c_str()); +} + +void FullscreenUI::ProgressCallback::PushState() +{ + BaseProgressCallback::PushState(); +} + +void FullscreenUI::ProgressCallback::PopState() +{ + BaseProgressCallback::PopState(); + Redraw(true); +} + +void FullscreenUI::ProgressCallback::SetCancellable(bool cancellable) +{ + BaseProgressCallback::SetCancellable(cancellable); + Redraw(true); +} + +void FullscreenUI::ProgressCallback::SetTitle(const char* title) +{ + // todo? +} + +void FullscreenUI::ProgressCallback::SetStatusText(const char* text) +{ + BaseProgressCallback::SetStatusText(text); + Redraw(true); +} + +void FullscreenUI::ProgressCallback::SetProgressRange(u32 range) +{ + u32 last_range = m_progress_range; + + BaseProgressCallback::SetProgressRange(range); + + if (m_progress_range != last_range) + Redraw(false); +} + +void FullscreenUI::ProgressCallback::SetProgressValue(u32 value) +{ + u32 lastValue = m_progress_value; + + BaseProgressCallback::SetProgressValue(value); + + if (m_progress_value != lastValue) + Redraw(false); +} + +void FullscreenUI::ProgressCallback::Redraw(bool force) +{ + const int percent = static_cast((static_cast(m_progress_value) / static_cast(m_progress_range)) * 100.0f); + if (percent == m_last_progress_percent && !force) + return; + + m_last_progress_percent = percent; + ImGuiFullscreen::UpdateBackgroundProgressDialog(m_name.c_str(), m_status_text.c_str(), 0, 100, percent); +} + +void FullscreenUI::ProgressCallback::DisplayError(const char* message) +{ + Console.Error(message); + Host::ReportErrorAsync("Error", message); +} + +void FullscreenUI::ProgressCallback::DisplayWarning(const char* message) +{ + Console.Warning(message); +} + +void FullscreenUI::ProgressCallback::DisplayInformation(const char* message) +{ + Console.WriteLn(message); +} + +void FullscreenUI::ProgressCallback::DisplayDebugMessage(const char* message) +{ + DevCon.WriteLn(message); +} + +void FullscreenUI::ProgressCallback::ModalError(const char* message) +{ + Console.Error(message); + Host::ReportErrorAsync("Error", message); +} + +bool FullscreenUI::ProgressCallback::ModalConfirmation(const char* message) +{ + return false; +} + +void FullscreenUI::ProgressCallback::ModalInformation(const char* message) +{ + Console.WriteLn(message); +} + +void FullscreenUI::ProgressCallback::SetCancelled() +{ + if (m_cancellable) + m_cancelled = true; +} diff --git a/pcsx2/Frontend/FullscreenUI.h b/pcsx2/Frontend/FullscreenUI.h new file mode 100644 index 0000000000..e57e523875 --- /dev/null +++ b/pcsx2/Frontend/FullscreenUI.h @@ -0,0 +1,75 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCSX2. + * If not, see . + */ + +#pragma once +#include "common/Pcsx2Defs.h" +#include "common/ProgressCallback.h" +#include +#include + +class HostDisplayTexture; + +namespace FullscreenUI +{ + bool Initialize(); + bool IsInitialized(); + bool HasActiveWindow(); + void OnVMStarted(); + void OnVMPaused(); + void OnVMResumed(); + void OnVMDestroyed(); + void OnRunningGameChanged(std::string path, std::string serial, std::string title, u32 crc); + void OpenPauseMenu(); + + void Shutdown(); + void Render(); + + // Returns true if the message has been dismissed. + bool DrawErrorWindow(const char* message); + bool DrawConfirmWindow(const char* message, bool* result); + + class ProgressCallback final : public BaseProgressCallback + { + public: + ProgressCallback(std::string name); + ~ProgressCallback() override; + + void PushState() override; + void PopState() override; + + void SetCancellable(bool cancellable) override; + void SetTitle(const char* title) override; + void SetStatusText(const char* text) override; + void SetProgressRange(u32 range) override; + void SetProgressValue(u32 value) override; + + void DisplayError(const char* message) override; + void DisplayWarning(const char* message) override; + void DisplayInformation(const char* message) override; + void DisplayDebugMessage(const char* message) override; + + void ModalError(const char* message) override; + bool ModalConfirmation(const char* message) override; + void ModalInformation(const char* message) override; + + void SetCancelled(); + + private: + void Redraw(bool force); + + std::string m_name; + int m_last_progress_percent = -1; + }; +} // namespace FullscreenUI diff --git a/pcsx2/Frontend/ImGuiFullscreen.cpp b/pcsx2/Frontend/ImGuiFullscreen.cpp new file mode 100644 index 0000000000..1fcb544813 --- /dev/null +++ b/pcsx2/Frontend/ImGuiFullscreen.cpp @@ -0,0 +1,2265 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCSX2. + * If not, see . + */ + +#include "PrecompiledHeader.h" + +#define IMGUI_DEFINE_MATH_OPERATORS + +#include "ImGuiFullscreen.h" +#include "IconsFontAwesome5.h" +#include "common/Assertions.h" +#include "common/Easing.h" +#include "common/Image.h" +#include "common/LRUCache.h" +#include "common/FileSystem.h" +#include "common/Path.h" +#include "common/StringUtil.h" +#include "common/Threading.h" +#include "common/Timer.h" +#include "fmt/core.h" +#include "HostDisplay.h" +#include "imgui_internal.h" +#include "misc/cpp/imgui_stdlib.h" +#include +#include +#include + +namespace ImGuiFullscreen +{ + static std::optional LoadTextureImage(const char* path); + static std::shared_ptr UploadTexture(const char* path, const Common::RGBA8Image& image); + static void TextureLoaderThread(); + + static void DrawFileSelector(); + static void DrawChoiceDialog(); + static void DrawInputDialog(); + static void DrawBackgroundProgressDialogs(ImVec2& position, float spacing); + static void DrawNotifications(ImVec2& position, float spacing); + static void DrawToast(); + static void GetMenuButtonFrameBounds(float height, ImVec2* pos, ImVec2* size); + static bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImRect* bb, + ImGuiButtonFlags flags = 0, float hover_alpha = 1.0f); + static void PopulateFileSelectorItems(); + static void SetFileSelectorDirectory(std::string dir); + static ImGuiID GetBackgroundProgressID(const char* str_id); + + ImFont* g_standard_font = nullptr; + ImFont* g_medium_font = nullptr; + ImFont* g_large_font = nullptr; + ImFont* g_icon_font = nullptr; + + float g_layout_scale = 1.0f; + float g_layout_padding_left = 0.0f; + float g_layout_padding_top = 0.0f; + + ImVec4 UIBackgroundColor; + ImVec4 UIBackgroundTextColor; + ImVec4 UIBackgroundLineColor; + ImVec4 UIBackgroundHighlightColor; + ImVec4 UIDisabledColor; + ImVec4 UIPrimaryColor; + ImVec4 UIPrimaryLightColor; + ImVec4 UIPrimaryDarkColor; + ImVec4 UIPrimaryTextColor; + ImVec4 UITextHighlightColor; + ImVec4 UIPrimaryLineColor; + ImVec4 UISecondaryColor; + ImVec4 UISecondaryLightColor; + ImVec4 UISecondaryDarkColor; + ImVec4 UISecondaryTextColor; + + static u32 s_menu_button_index = 0; + static u32 s_close_button_state = 0; + static bool s_focus_reset_queued = false; + + static LRUCache> s_texture_cache(128, true); + static std::shared_ptr s_placeholder_texture; + static std::atomic_bool s_texture_load_thread_quit{false}; + static std::mutex s_texture_load_mutex; + static std::condition_variable s_texture_load_cv; + static std::deque s_texture_load_queue; + static std::deque> s_texture_upload_queue; + static Threading::Thread s_texture_load_thread; + + static bool s_choice_dialog_open = false; + static bool s_choice_dialog_checkable = false; + static std::string s_choice_dialog_title; + static ChoiceDialogOptions s_choice_dialog_options; + static ChoiceDialogCallback s_choice_dialog_callback; + static ImGuiID s_enum_choice_button_id = 0; + static s32 s_enum_choice_button_value = 0; + static bool s_enum_choice_button_set = false; + + static bool s_input_dialog_open = false; + static std::string s_input_dialog_title; + static std::string s_input_dialog_message; + static std::string s_input_dialog_caption; + static std::string s_input_dialog_text; + static std::string s_input_dialog_ok_text; + static InputStringDialogCallback s_input_dialog_callback; + + struct FileSelectorItem + { + FileSelectorItem() = default; + FileSelectorItem(std::string display_name_, std::string full_path_, bool is_file_) + : display_name(std::move(display_name_)) + , full_path(std::move(full_path_)) + , is_file(is_file_) + { + } + FileSelectorItem(const FileSelectorItem&) = default; + FileSelectorItem(FileSelectorItem&&) = default; + ~FileSelectorItem() = default; + + FileSelectorItem& operator=(const FileSelectorItem&) = default; + FileSelectorItem& operator=(FileSelectorItem&&) = default; + + std::string display_name; + std::string full_path; + bool is_file; + }; + + static bool s_file_selector_open = false; + static bool s_file_selector_directory = false; + static std::string s_file_selector_title; + static ImGuiFullscreen::FileSelectorCallback s_file_selector_callback; + static std::string s_file_selector_current_directory; + static std::vector s_file_selector_filters; + static std::vector s_file_selector_items; + + struct Notification + { + std::string title; + std::string text; + std::string badge_path; + Common::Timer::Value start_time; + float duration; + }; + + static std::vector s_notifications; + + static std::string s_toast_title; + static std::string s_toast_message; + static Common::Timer::Value s_toast_start_time; + static float s_toast_duration; + + struct BackgroundProgressDialogData + { + std::string message; + ImGuiID id; + s32 min; + s32 max; + s32 value; + }; + + static std::vector s_background_progress_dialogs; + static std::mutex s_background_progress_lock; +} // namespace ImGuiFullscreen + +void ImGuiFullscreen::SetFonts(ImFont* standard_font, ImFont* medium_font, ImFont* large_font) +{ + g_standard_font = standard_font; + g_medium_font = medium_font; + g_large_font = large_font; +} + +bool ImGuiFullscreen::Initialize(const char* placeholder_image_path) +{ + s_focus_reset_queued = true; + s_close_button_state = 0; + + s_placeholder_texture = LoadTexture(placeholder_image_path); + if (!s_placeholder_texture) + { + Console.Error("Missing placeholder texture '%s', cannot continue", placeholder_image_path); + return false; + } + + s_texture_load_thread_quit.store(false, std::memory_order_release); + s_texture_load_thread.Start(TextureLoaderThread); + return true; +} + +void ImGuiFullscreen::Shutdown() +{ + if (s_texture_load_thread.Joinable()) + { + { + std::unique_lock lock(s_texture_load_mutex); + s_texture_load_thread_quit.store(true, std::memory_order_release); + s_texture_load_cv.notify_one(); + } + s_texture_load_thread.Join(); + } + + s_texture_upload_queue.clear(); + s_placeholder_texture.reset(); + g_standard_font = nullptr; + g_medium_font = nullptr; + g_large_font = nullptr; + + s_texture_cache.Clear(); + + s_notifications.clear(); + s_background_progress_dialogs.clear(); + CloseInputDialog(); + s_choice_dialog_open = false; + s_choice_dialog_checkable = false; + s_choice_dialog_title = {}; + s_choice_dialog_options.clear(); + s_choice_dialog_callback = {}; + s_enum_choice_button_id = 0; + s_enum_choice_button_value = 0; + s_enum_choice_button_set = false; + s_file_selector_open = false; + s_file_selector_directory = false; + s_file_selector_title = {}; + s_file_selector_callback = {}; + s_file_selector_current_directory = {}; + s_file_selector_filters.clear(); + s_file_selector_items.clear(); +} + +const std::shared_ptr& ImGuiFullscreen::GetPlaceholderTexture() +{ + return s_placeholder_texture; +} + +std::optional ImGuiFullscreen::LoadTextureImage(const char* path) +{ + std::optional image; + + std::optional> data; + if (Path::IsAbsolute(path)) + data = FileSystem::ReadBinaryFile(path); + else + data = Host::ReadResourceFile(path); + if (data.has_value()) + { + image = Common::RGBA8Image(); + if (!image->LoadFromBuffer(path, data->data(), data->size())) + { + Console.Error("Failed to read texture resource '%s'", path); + image.reset(); + } + } + else + { + Console.Error("Failed to open texture resource '%s'", path); + } + + return image; +} + +std::shared_ptr ImGuiFullscreen::UploadTexture(const char* path, const Common::RGBA8Image& image) +{ + std::unique_ptr texture = + Host::GetHostDisplay()->CreateTexture(image.GetWidth(), image.GetHeight(), image.GetPixels(), image.GetByteStride()); + if (!texture) + { + Console.Error("failed to create %ux%u texture for resource", image.GetWidth(), image.GetHeight()); + return {}; + } + + DevCon.WriteLn("Uploaded texture resource '%s' (%ux%u)", path, image.GetWidth(), image.GetHeight()); + return std::shared_ptr(std::move(texture)); +} + +std::shared_ptr ImGuiFullscreen::LoadTexture(const char* path) +{ + std::optional image(LoadTextureImage(path)); + if (image.has_value()) + { + std::shared_ptr ret(UploadTexture(path, image.value())); + if (ret) + return ret; + } + + return s_placeholder_texture; +} + +HostDisplayTexture* ImGuiFullscreen::GetCachedTexture(const char* name) +{ + std::shared_ptr* tex_ptr = s_texture_cache.Lookup(name); + if (!tex_ptr) + { + std::shared_ptr tex(LoadTexture(name)); + tex_ptr = s_texture_cache.Insert(name, std::move(tex)); + } + + return tex_ptr->get(); +} + +HostDisplayTexture* ImGuiFullscreen::GetCachedTextureAsync(const char* name) +{ + std::shared_ptr* tex_ptr = s_texture_cache.Lookup(name); + if (!tex_ptr) + { + // insert the placeholder + tex_ptr = s_texture_cache.Insert(name, s_placeholder_texture); + + // queue the actual load + std::unique_lock lock(s_texture_load_mutex); + s_texture_load_queue.emplace_back(name); + s_texture_load_cv.notify_one(); + } + + return tex_ptr->get(); +} + +bool ImGuiFullscreen::InvalidateCachedTexture(const std::string& path) +{ + return s_texture_cache.Remove(path); +} + +void ImGuiFullscreen::UploadAsyncTextures() +{ + std::unique_lock lock(s_texture_load_mutex); + while (!s_texture_upload_queue.empty()) + { + std::pair it(std::move(s_texture_upload_queue.front())); + s_texture_upload_queue.pop_front(); + lock.unlock(); + + std::shared_ptr tex = UploadTexture(it.first.c_str(), it.second); + if (tex) + s_texture_cache.Insert(std::move(it.first), std::move(tex)); + + lock.lock(); + } +} + +void ImGuiFullscreen::TextureLoaderThread() +{ + Threading::SetNameOfCurrentThread("ImGuiFullscreen Texture Loader"); + + std::unique_lock lock(s_texture_load_mutex); + + for (;;) + { + s_texture_load_cv.wait( + lock, []() { return (s_texture_load_thread_quit.load(std::memory_order_acquire) || !s_texture_load_queue.empty()); }); + + if (s_texture_load_thread_quit.load(std::memory_order_acquire)) + break; + + while (!s_texture_load_queue.empty()) + { + std::string path(std::move(s_texture_load_queue.front())); + s_texture_load_queue.pop_front(); + + lock.unlock(); + std::optional image(LoadTextureImage(path.c_str())); + lock.lock(); + + // don't bother queuing back if it doesn't exist + if (image) + s_texture_upload_queue.emplace_back(std::move(path), std::move(image.value())); + } + } + + s_texture_load_queue.clear(); +} + +bool ImGuiFullscreen::UpdateLayoutScale() +{ + static constexpr float LAYOUT_RATIO = LAYOUT_SCREEN_WIDTH / LAYOUT_SCREEN_HEIGHT; + const ImGuiIO& io = ImGui::GetIO(); + + const float screen_width = io.DisplaySize.x; + const float screen_height = io.DisplaySize.y; + const float screen_ratio = screen_width / screen_height; + const float old_scale = g_layout_scale; + + if (screen_ratio > LAYOUT_RATIO) + { + // screen is wider, use height, pad width + g_layout_scale = screen_height / LAYOUT_SCREEN_HEIGHT; + g_layout_padding_top = 0.0f; + g_layout_padding_left = (screen_width - (LAYOUT_SCREEN_WIDTH * g_layout_scale)) / 2.0f; + } + else + { + // screen is taller, use width, pad height + g_layout_scale = screen_width / LAYOUT_SCREEN_WIDTH; + g_layout_padding_top = (screen_height - (LAYOUT_SCREEN_HEIGHT * g_layout_scale)) / 2.0f; + g_layout_padding_left = 0.0f; + } + + return g_layout_scale != old_scale; +} + +ImRect ImGuiFullscreen::CenterImage(const ImVec2& fit_size, const ImVec2& image_size) +{ + const float fit_ar = fit_size.x / fit_size.y; + const float image_ar = image_size.x / image_size.y; + + ImRect ret; + if (fit_ar > image_ar) + { + // center horizontally + const float width = fit_size.y * image_ar; + const float offset = (fit_size.x - width) / 2.0f; + const float height = fit_size.y; + ret = ImRect(ImVec2(offset, 0.0f), ImVec2(offset + width, height)); + } + else + { + // center vertically + const float height = fit_size.x / image_ar; + const float offset = (fit_size.y - height) / 2.0f; + const float width = fit_size.x; + ret = ImRect(ImVec2(0.0f, offset), ImVec2(width, offset + height)); + } + + return ret; +} + +ImRect ImGuiFullscreen::CenterImage(const ImRect& fit_rect, const ImVec2& image_size) +{ + ImRect ret(CenterImage(fit_rect.Max - fit_rect.Min, image_size)); + ret.Translate(fit_rect.Min); + return ret; +} + +void ImGuiFullscreen::BeginLayout() +{ + // we evict from the texture cache at the start of the frame, in case we go over mid-frame, + // we need to keep all those textures alive until the end of the frame + s_texture_cache.ManualEvict(); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Text, UISecondaryTextColor); + ImGui::PushStyleColor(ImGuiCol_TextDisabled, UIDisabledColor); + ImGui::PushStyleColor(ImGuiCol_Button, UISecondaryColor); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, UIBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, UIBackgroundHighlightColor); + ImGui::PushStyleColor(ImGuiCol_Border, UIBackgroundLineColor); + ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, UIBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, UIPrimaryLightColor); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, UIPrimaryDarkColor); +} + +void ImGuiFullscreen::EndLayout() +{ + DrawFileSelector(); + DrawChoiceDialog(); + DrawInputDialog(); + + const float notification_margin = LayoutScale(10.0f); + const float spacing = LayoutScale(10.0f); + const float notification_vertical_pos = GetNotificationVerticalPosition(); + ImVec2 position(notification_margin, notification_vertical_pos * ImGui::GetIO().DisplaySize.y + + ((notification_vertical_pos >= 0.5f) ? -notification_margin : notification_margin)); + DrawBackgroundProgressDialogs(position, spacing); + DrawNotifications(position, spacing); + DrawToast(); + + ImGui::PopStyleColor(10); + ImGui::PopStyleVar(2); +} + +void ImGuiFullscreen::QueueResetFocus() +{ + s_focus_reset_queued = true; + s_close_button_state = 0; +} + +bool ImGuiFullscreen::ResetFocusHere() +{ + if (!s_focus_reset_queued) + return false; + + s_focus_reset_queued = false; + ImGui::SetWindowFocus(); + + // only do the active selection magic when we're using keyboard/gamepad + return (GImGui->NavInputSource == ImGuiInputSource_Keyboard || GImGui->NavInputSource == ImGuiInputSource_Gamepad); +} + +bool ImGuiFullscreen::WantsToCloseMenu() +{ + // Wait for the Close button to be released, THEN pressed + if (s_close_button_state == 0) + { + if (ImGui::IsNavInputTest(ImGuiNavInput_Cancel, ImGuiNavReadMode_Pressed)) + s_close_button_state = 1; + } + else if (s_close_button_state == 1) + { + if (ImGui::IsNavInputTest(ImGuiNavInput_Cancel, ImGuiNavReadMode_Released)) + { + s_close_button_state = 2; + } + } + return s_close_button_state > 1; +} + +void ImGuiFullscreen::ResetCloseMenuIfNeeded() +{ + // If s_close_button_state reached the "Released" state, reset it after the tick + if (s_close_button_state > 1) + { + s_close_button_state = 0; + } +} + +void ImGuiFullscreen::PushPrimaryColor() +{ + ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); + ImGui::PushStyleColor(ImGuiCol_Button, UIPrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, UIPrimaryLightColor); + ImGui::PushStyleColor(ImGuiCol_Border, UIPrimaryLightColor); +} + +void ImGuiFullscreen::PopPrimaryColor() +{ + ImGui::PopStyleColor(5); +} + +void ImGuiFullscreen::PushSecondaryColor() +{ + ImGui::PushStyleColor(ImGuiCol_Text, UISecondaryTextColor); + ImGui::PushStyleColor(ImGuiCol_Button, UISecondaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, UISecondaryColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, UISecondaryLightColor); + ImGui::PushStyleColor(ImGuiCol_Border, UISecondaryLightColor); +} + +void ImGuiFullscreen::PopSecondaryColor() +{ + ImGui::PopStyleColor(5); +} + +bool ImGuiFullscreen::BeginFullscreenColumns(const char* title) +{ + ImGui::SetNextWindowPos(ImVec2(g_layout_padding_left, 0.0f)); + ImGui::SetNextWindowSize(ImVec2(LayoutScale(LAYOUT_SCREEN_WIDTH), ImGui::GetIO().DisplaySize.y)); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + + bool clipped; + if (title) + { + ImGui::PushFont(g_large_font); + clipped = ImGui::Begin(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); + ImGui::PopFont(); + } + else + { + clipped = ImGui::Begin( + "fullscreen_ui_columns_parent", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); + } + + return clipped; +} + +void ImGuiFullscreen::EndFullscreenColumns() +{ + ImGui::End(); + ImGui::PopStyleVar(3); +} + +bool ImGuiFullscreen::BeginFullscreenColumnWindow(float start, float end, const char* name, const ImVec4& background) +{ + const ImVec2 pos(LayoutScale(start), 0.0f); + const ImVec2 size(LayoutScale(end - start), ImGui::GetIO().DisplaySize.y); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, background); + + ImGui::SetCursorPos(pos); + + return ImGui::BeginChild(name, size, false, ImGuiWindowFlags_NavFlattened); +} + +void ImGuiFullscreen::EndFullscreenColumnWindow() +{ + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +bool ImGuiFullscreen::BeginFullscreenWindow(float left, float top, float width, float height, const char* name, + const ImVec4& background /* = HEX_TO_IMVEC4(0x212121, 0xFF) */, float rounding /*= 0.0f*/, float padding /*= 0.0f*/, + ImGuiWindowFlags flags /*= 0*/) +{ + if (left < 0.0f) + left = (LAYOUT_SCREEN_WIDTH - width) * -left; + if (top < 0.0f) + top = (LAYOUT_SCREEN_HEIGHT - height) * -top; + + const ImVec2 pos(ImVec2(LayoutScale(left) + g_layout_padding_left, LayoutScale(top) + g_layout_padding_top)); + const ImVec2 size(LayoutScale(ImVec2(width, height))); + return BeginFullscreenWindow(pos, size, name, background, rounding, padding, flags); +} + +bool ImGuiFullscreen::BeginFullscreenWindow(const ImVec2& position, const ImVec2& size, const char* name, + const ImVec4& background /* = HEX_TO_IMVEC4(0x212121, 0xFF) */, float rounding /*= 0.0f*/, float padding /*= 0.0f*/, + ImGuiWindowFlags flags /*= 0*/) +{ + ImGui::SetNextWindowPos(position); + ImGui::SetNextWindowSize(size); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, background); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(padding, padding)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(rounding)); + + return ImGui::Begin(name, nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBringToFrontOnFocus | + flags); +} + +void ImGuiFullscreen::EndFullscreenWindow() +{ + ImGui::End(); + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(); +} + +void ImGuiFullscreen::BeginMenuButtons(u32 num_items, float y_align, float x_padding, float y_padding, float item_height) +{ + s_menu_button_index = 0; + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(x_padding, y_padding)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + + if (y_align != 0.0f) + { + const float total_size = + static_cast(num_items) * LayoutScale(item_height + (y_padding * 2.0f)) + LayoutScale(y_padding * 2.0f); + const float window_height = ImGui::GetWindowHeight(); + if (window_height > total_size) + ImGui::SetCursorPosY((window_height - total_size) * y_align); + } +} + +void ImGuiFullscreen::EndMenuButtons() +{ + ImGui::PopStyleVar(4); +} + +void ImGuiFullscreen::DrawWindowTitle(const char* title) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + const ImVec2 pos(window->DC.CursorPos + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + const ImVec2 size(window->WorkRect.GetWidth() - (LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING) * 2.0f), + g_large_font->FontSize + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f); + const ImRect rect(pos, pos + size); + + ImGui::ItemSize(size); + if (!ImGui::ItemAdd(rect, window->GetID("window_title"))) + return; + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(rect.Min, rect.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &rect); + ImGui::PopFont(); + + const ImVec2 line_start(pos.x, pos.y + g_large_font->FontSize + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING)); + const ImVec2 line_end(pos.x + size.x, line_start.y); + const float line_thickness = LayoutScale(1.0f); + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddLine(line_start, line_end, IM_COL32(255, 255, 255, 255), line_thickness); +} + +void ImGuiFullscreen::GetMenuButtonFrameBounds(float height, ImVec2* pos, ImVec2* size) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + *pos = window->DC.CursorPos; + *size = ImVec2(window->WorkRect.GetWidth(), LayoutScale(height) + ImGui::GetStyle().FramePadding.y * 2.0f); +} + +bool ImGuiFullscreen::MenuButtonFrame( + const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImRect* bb, ImGuiButtonFlags flags, float hover_alpha) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + { + *visible = false; + *hovered = false; + return false; + } + + ImVec2 pos, size; + GetMenuButtonFrameBounds(height, &pos, &size); + *bb = ImRect(pos, pos + size); + + const ImGuiID id = window->GetID(str_id); + ImGui::ItemSize(size); + if (enabled) + { + if (!ImGui::ItemAdd(*bb, id)) + { + *visible = false; + *hovered = false; + return false; + } + } + else + { + if (ImGui::IsClippedEx(*bb, id)) + { + *visible = false; + *hovered = false; + return false; + } + } + + *visible = true; + + bool held; + bool pressed; + if (enabled) + { + pressed = ImGui::ButtonBehavior(*bb, id, hovered, &held, flags); + if (*hovered) + { + const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, hover_alpha); + + const float t = std::min(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1), 1.0f); + ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t)); + + ImGui::RenderFrame(bb->Min, bb->Max, col, true, 0.0f); + + ImGui::PopStyleColor(); + } + } + else + { + pressed = false; + held = false; + } + + const ImGuiStyle& style = ImGui::GetStyle(); + bb->Min += style.FramePadding; + bb->Max -= style.FramePadding; + + return pressed; +} + +bool ImGuiFullscreen::MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImVec2* min, + ImVec2* max, ImGuiButtonFlags flags /*= 0*/, float hover_alpha /*= 0*/) +{ + ImRect bb; + const bool result = MenuButtonFrame(str_id, enabled, height, visible, hovered, &bb, flags, hover_alpha); + *min = bb.Min; + *max = bb.Max; + return result; +} + +void ImGuiFullscreen::MenuHeading(const char* title, bool draw_line /*= true*/) +{ + const float line_thickness = draw_line ? LayoutScale(1.0f) : 0.0f; + const float line_padding = draw_line ? LayoutScale(5.0f) : 0.0f; + + bool visible, hovered; + ImRect bb; + MenuButtonFrame(title, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, &bb); + if (!visible) + return; + + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb); + ImGui::PopFont(); + ImGui::PopStyleColor(); + + if (draw_line) + { + const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding); + const ImVec2 line_end(bb.Max.x, line_start.y); + ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled), line_thickness); + } +} + +bool ImGuiFullscreen::MenuHeadingButton( + const char* title, const char* value /*= nullptr*/, bool enabled /*= true*/, bool draw_line /*= true*/) +{ + const float line_thickness = draw_line ? LayoutScale(1.0f) : 0.0f; + const float line_padding = draw_line ? LayoutScale(5.0f) : 0.0f; + + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, &bb); + if (!visible) + return false; + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb); + + if (value) + { + const ImVec2 value_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), 0.0f, value)); + const ImRect value_bb(ImVec2(bb.Max.x - value_size.x, bb.Min.y), ImVec2(bb.Max.x, bb.Max.y)); + ImGui::RenderTextClipped(value_bb.Min, value_bb.Max, value, nullptr, nullptr, ImVec2(0.0f, 0.0f), &value_bb); + } + + ImGui::PopFont(); + if (!enabled) + ImGui::PopStyleColor(); + + if (draw_line) + { + const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding); + const ImVec2 line_end(bb.Max.x, line_start.y); + ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled), line_thickness); + } + + return pressed; +} + +bool ImGuiFullscreen::ActiveButton(const char* title, bool is_active, bool enabled, float height, ImFont* font) +{ + if (is_active) + { + ImVec2 pos, size; + GetMenuButtonFrameBounds(height, &pos, &size); + ImGui::RenderFrame(pos, pos + size, ImGui::GetColorU32(UIPrimaryColor), false); + } + + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const ImRect title_bb(bb.GetTL(), bb.GetBR()); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (!enabled) + ImGui::PopStyleColor(); + + s_menu_button_index++; + return pressed; +} + +bool ImGuiFullscreen::MenuButton(const char* title, const char* summary, bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + s_menu_button_index++; + return pressed; +} + +bool ImGuiFullscreen::MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id, const ImVec2& image_size, + bool enabled, float height, const ImVec2& uv0, const ImVec2& uv1, ImFont* title_font, ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + ImGui::GetWindowDrawList()->AddImage(user_texture_id, bb.Min, bb.Min + image_size, uv0, uv1, + enabled ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + const float midpoint = bb.Min.y + title_font->FontSize + LayoutScale(4.0f); + const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f); + const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(title_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + s_menu_button_index++; + return pressed; +} + +bool ImGuiFullscreen::FloatingButton(const char* text, float x, float y, float width, float height, float anchor_x, float anchor_y, + bool enabled, ImFont* font, ImVec2* out_position) +{ + const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), 0.0f, text)); + const ImVec2& padding(ImGui::GetStyle().FramePadding); + if (width < 0.0f) + width = (padding.x * 2.0f) + text_size.x; + if (height < 0.0f) + height = (padding.y * 2.0f) + text_size.y; + + const ImVec2 window_size(ImGui::GetWindowSize()); + if (anchor_x == -1.0f) + x -= width; + else if (anchor_x == -0.5f) + x -= (width * 0.5f); + else if (anchor_x == 0.5f) + x = (window_size.x * 0.5f) - (width * 0.5f) - x; + else if (anchor_x == 1.0f) + x = window_size.x - width - x; + if (anchor_y == -1.0f) + y -= height; + else if (anchor_y == -0.5f) + y -= (height * 0.5f); + else if (anchor_y == 0.5f) + y = (window_size.y * 0.5f) - (height * 0.5f) - y; + else if (anchor_y == 1.0f) + y = window_size.y - height - y; + + if (out_position) + *out_position = ImVec2(x, y); + + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImVec2 base(ImGui::GetWindowPos() + ImVec2(x, y)); + ImRect bb(base, base + ImVec2(width, height)); + + const ImGuiID id = window->GetID(text); + if (enabled) + { + if (!ImGui::ItemAdd(bb, id)) + return false; + } + else + { + if (ImGui::IsClippedEx(bb, id)) + return false; + } + + bool hovered; + bool held; + bool pressed; + if (enabled) + { + pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0); + if (hovered) + { + const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f); + ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f); + } + } + else + { + hovered = false; + pressed = false; + held = false; + } + + bb.Min += padding; + bb.Max -= padding; + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(bb.Min, bb.Max, text, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb); + ImGui::PopFont(); + + if (!enabled) + ImGui::PopStyleColor(); + + return pressed; +} + +bool ImGuiFullscreen::ToggleButton( + const char* title, const char* summary, bool* v, bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb, ImGuiButtonFlags_PressedOnClick); + if (!visible) + return false; + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + const float toggle_width = LayoutScale(50.0f); + const float toggle_height = LayoutScale(25.0f); + const float toggle_x = LayoutScale(8.0f); + const float toggle_y = (LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT) - toggle_height) * 0.5f; + const float toggle_radius = toggle_height * 0.5f; + const ImVec2 toggle_pos(bb.Max.x - toggle_width - toggle_x, bb.Min.y + toggle_y); + + if (pressed) + *v = !*v; + + float t = *v ? 1.0f : 0.0f; + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImGuiContext& g = *GImGui; + if (g.LastActiveId == g.CurrentWindow->GetID(title)) + { + static constexpr const float ANIM_SPEED = 0.08f; + float t_anim = ImSaturate(g.LastActiveIdTimer / ANIM_SPEED); + t = *v ? (t_anim) : (1.0f - t_anim); + } + + ImU32 col_bg; + ImU32 col_knob; + if (!enabled) + { + col_bg = ImGui::GetColorU32(UIDisabledColor); + col_knob = IM_COL32(200, 200, 200, 200); + } + else + { + col_bg = ImGui::GetColorU32(ImLerp(HEX_TO_IMVEC4(0x8C8C8C, 0xff), UISecondaryLightColor, t)); + col_knob = IM_COL32(255, 255, 255, 255); + } + + dl->AddRectFilled(toggle_pos, ImVec2(toggle_pos.x + toggle_width, toggle_pos.y + toggle_height), col_bg, toggle_height * 0.5f); + dl->AddCircleFilled(ImVec2(toggle_pos.x + toggle_radius + t * (toggle_width - toggle_radius * 2.0f), toggle_pos.y + toggle_radius), + toggle_radius - 1.5f, col_knob, 32); + + s_menu_button_index++; + return pressed; +} + +bool ImGuiFullscreen::ThreeWayToggleButton( + const char* title, const char* summary, std::optional* v, bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb, ImGuiButtonFlags_PressedOnClick); + if (!visible) + return false; + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + const float toggle_width = LayoutScale(50.0f); + const float toggle_height = LayoutScale(25.0f); + const float toggle_x = LayoutScale(8.0f); + const float toggle_y = (LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT) - toggle_height) * 0.5f; + const float toggle_radius = toggle_height * 0.5f; + const ImVec2 toggle_pos(bb.Max.x - toggle_width - toggle_x, bb.Min.y + toggle_y); + + if (pressed) + { + if (v->has_value() && v->value()) + *v = false; + else if (v->has_value() && !v->value()) + v->reset(); + else + *v = true; + } + + float t = v->has_value() ? (v->value() ? 1.0f : 0.0f) : 0.5f; + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImGuiContext& g = *GImGui; + if (g.LastActiveId == g.CurrentWindow->GetID(title)) + { + static constexpr const float ANIM_SPEED = 0.08f; + float t_anim = ImSaturate(g.LastActiveIdTimer / ANIM_SPEED); + t = (v->has_value() ? (v->value() ? std::min(t_anim + 0.5f, 1.0f) : (1.0f - t_anim)) : (t_anim * 0.5f)); + } + + const float color_t = v->has_value() ? t : 0.0f; + + ImU32 col_bg; + if (!enabled) + col_bg = IM_COL32(0x75, 0x75, 0x75, 0xff); + else if (hovered) + col_bg = ImGui::GetColorU32( + ImLerp(v->has_value() ? HEX_TO_IMVEC4(0xf05100, 0xff) : HEX_TO_IMVEC4(0x9e9e9e, 0xff), UISecondaryLightColor, color_t)); + else + col_bg = ImGui::GetColorU32( + ImLerp(v->has_value() ? HEX_TO_IMVEC4(0xc45100, 0xff) : HEX_TO_IMVEC4(0x757575, 0xff), UISecondaryLightColor, color_t)); + + dl->AddRectFilled(toggle_pos, ImVec2(toggle_pos.x + toggle_width, toggle_pos.y + toggle_height), col_bg, toggle_height * 0.5f); + dl->AddCircleFilled(ImVec2(toggle_pos.x + toggle_radius + t * (toggle_width - toggle_radius * 2.0f), toggle_pos.y + toggle_radius), + toggle_radius - 1.5f, IM_COL32(255, 255, 255, 255), 32); + + s_menu_button_index++; + return pressed; +} + +bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, s32* value, s32 min, s32 max, s32 increment, const char* format, + bool enabled /*= true*/, float height /*= LAYOUT_MENU_BUTTON_HEIGHT*/, ImFont* font /*= g_large_font*/, + ImFont* summary_font /*= g_medium_font*/) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const std::string value_text(StringUtil::StdStringFromFormat(format, *value)); + const ImVec2 value_size(ImGui::CalcTextSize(value_text.c_str())); + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const float text_end = bb.Max.x - value_size.x; + const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y)); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::RenderTextClipped(bb.Min, bb.Max, value_text.c_str(), nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + if (pressed) + ImGui::OpenPopup(title); + + bool changed = false; + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f)); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + if (ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::SetNextItemWidth(LayoutScale(450.0f)); + changed = ImGui::SliderInt("##value", value, min, max, format, ImGuiSliderFlags_NoInput); + + BeginMenuButtons(); + if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::CloseCurrentPopup(); + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(3); + ImGui::PopFont(); + + return changed; +} + +bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, float* value, float min, float max, float increment, + const char* format, bool enabled /*= true*/, float height /*= LAYOUT_MENU_BUTTON_HEIGHT*/, ImFont* font /*= g_large_font*/, + ImFont* summary_font /*= g_medium_font*/) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const std::string value_text(StringUtil::StdStringFromFormat(format, *value)); + const ImVec2 value_size(ImGui::CalcTextSize(value_text.c_str())); + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const float text_end = bb.Max.x - value_size.x; + const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y)); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::RenderTextClipped(bb.Min, bb.Max, value_text.c_str(), nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + if (pressed) + ImGui::OpenPopup(title); + + bool changed = false; + + ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f)); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + if (ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::SetNextItemWidth(LayoutScale(450.0f)); + changed = ImGui::SliderFloat("##value", value, min, max, format, ImGuiSliderFlags_NoInput); + + BeginMenuButtons(); + if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::CloseCurrentPopup(); + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(3); + ImGui::PopFont(); + + return changed; +} + +bool ImGuiFullscreen::MenuButtonWithValue( + const char* title, const char* summary, const char* value, bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const ImVec2 value_size(ImGui::CalcTextSize(value)); + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const float text_end = bb.Max.x - value_size.x; + const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y)); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::RenderTextClipped(bb.Min, bb.Max, value, nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + return pressed; +} + +bool ImGuiFullscreen::EnumChoiceButtonImpl(const char* title, const char* summary, s32* value_pointer, + const char* (*to_display_name_function)(s32 value, void* opaque), void* opaque, u32 count, bool enabled, float height, ImFont* font, + ImFont* summary_font) +{ + const bool pressed = + MenuButtonWithValue(title, summary, to_display_name_function(*value_pointer, opaque), enabled, height, font, summary_font); + + if (pressed) + { + s_enum_choice_button_id = ImGui::GetID(title); + s_enum_choice_button_value = *value_pointer; + s_enum_choice_button_set = false; + + ChoiceDialogOptions options; + options.reserve(count); + for (u32 i = 0; i < count; i++) + options.emplace_back(to_display_name_function(static_cast(i), opaque), static_cast(*value_pointer) == i); + OpenChoiceDialog(title, false, std::move(options), [](s32 index, const std::string& title, bool checked) { + if (index >= 0) + s_enum_choice_button_value = index; + + s_enum_choice_button_set = true; + CloseChoiceDialog(); + }); + } + + bool changed = false; + if (s_enum_choice_button_set && s_enum_choice_button_id == ImGui::GetID(title)) + { + changed = s_enum_choice_button_value != *value_pointer; + if (changed) + *value_pointer = s_enum_choice_button_value; + + s_enum_choice_button_id = 0; + s_enum_choice_button_value = 0; + s_enum_choice_button_set = false; + } + + return changed; +} + +void ImGuiFullscreen::BeginNavBar(float x_padding /*= LAYOUT_MENU_BUTTON_X_PADDING*/, float y_padding /*= LAYOUT_MENU_BUTTON_Y_PADDING*/) +{ + s_menu_button_index = 0; + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(x_padding, y_padding)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(1.0f, 1.0f)); + PushPrimaryColor(); +} + +void ImGuiFullscreen::EndNavBar() +{ + PopPrimaryColor(); + ImGui::PopStyleVar(4); +} + +void ImGuiFullscreen::NavTitle(const char* title, float height /*= LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY*/, ImFont* font /*= g_large_font*/) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return; + + s_menu_button_index++; + + const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), 0.0f, title)); + const ImVec2 pos(window->DC.CursorPos); + const ImGuiStyle& style = ImGui::GetStyle(); + const ImVec2 size = ImVec2(text_size.x, LayoutScale(height) + style.FramePadding.y * 2.0f); + + ImGui::ItemSize(ImVec2(size.x + style.FrameBorderSize + style.ItemSpacing.x, size.y + style.FrameBorderSize + style.ItemSpacing.y)); + ImGui::SameLine(); + + ImRect bb(pos, pos + size); + if (ImGui::IsClippedEx(bb, 0)) + return; + + bb.Min.y += style.FramePadding.y; + bb.Max.y -= style.FramePadding.y; + + ImGui::PushFont(font); + ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb); + ImGui::PopFont(); +} + +void ImGuiFullscreen::RightAlignNavButtons(u32 num_items /*= 0*/, float item_width /*= LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY*/, + float item_height /*= LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY*/) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + const ImGuiStyle& style = ImGui::GetStyle(); + + const float total_item_width = style.FramePadding.x * 2.0f + style.FrameBorderSize + style.ItemSpacing.x + LayoutScale(item_width); + const float margin = total_item_width * static_cast(num_items); + ImGui::SetCursorPosX(window->InnerClipRect.Max.x - margin - style.FramePadding.x); +} + +bool ImGuiFullscreen::NavButton(const char* title, bool is_active, bool enabled /* = true */, float width /* = -1.0f */, + float height /* = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY */, ImFont* font /* = g_large_font */) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + s_menu_button_index++; + + const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), 0.0f, title)); + const ImVec2 pos(window->DC.CursorPos); + const ImGuiStyle& style = ImGui::GetStyle(); + const ImVec2 size = ImVec2(((width < 0.0f) ? text_size.x : LayoutScale(width)) + style.FramePadding.x * 2.0f, + LayoutScale(height) + style.FramePadding.y * 2.0f); + + ImGui::ItemSize(ImVec2(size.x + style.FrameBorderSize + style.ItemSpacing.x, size.y + style.FrameBorderSize + style.ItemSpacing.y)); + ImGui::SameLine(); + + ImRect bb(pos, pos + size); + const ImGuiID id = window->GetID(title); + if (enabled) + { + if (!ImGui::ItemAdd(bb, id)) + return false; + } + else + { + if (ImGui::IsClippedEx(bb, id)) + return false; + } + + bool held; + bool pressed; + bool hovered; + if (enabled) + { + pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0); + if (hovered) + { + const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f); + ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f); + } + } + else + { + pressed = false; + held = false; + hovered = false; + } + + bb.Min += style.FramePadding; + bb.Max -= style.FramePadding; + + ImGui::PushStyleColor( + ImGuiCol_Text, ImGui::GetColorU32(enabled ? (is_active ? ImGuiCol_Text : ImGuiCol_TextDisabled) : ImGuiCol_ButtonHovered)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb); + ImGui::PopFont(); + + ImGui::PopStyleColor(); + + return pressed; +} + +void ImGuiFullscreen::PopulateFileSelectorItems() +{ + s_file_selector_items.clear(); + + if (s_file_selector_current_directory.empty()) + { + for (std::string& root_path : FileSystem::GetRootDirectoryList()) + { + s_file_selector_items.emplace_back( + StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", root_path.c_str()), std::move(root_path), false); + } + } + else + { + FileSystem::FindResultsArray results; + FileSystem::FindFiles(s_file_selector_current_directory.c_str(), "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &results); + + std::string parent_path; + std::string::size_type sep_pos = s_file_selector_current_directory.rfind(FS_OSPATH_SEPARATOR_CHARACTER); + if (sep_pos != std::string::npos) + { + parent_path = s_file_selector_current_directory.substr(0, sep_pos); + //FIXME FileSystem::CanonicalizePath(parent_path, true); + } + + s_file_selector_items.emplace_back(ICON_FA_FOLDER_OPEN " ", std::move(parent_path), false); + std::sort(results.begin(), results.end(), [](const FILESYSTEM_FIND_DATA& lhs, const FILESYSTEM_FIND_DATA& rhs) { + if ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != (rhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)) + return (lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 0; + + // return std::lexicographical_compare(lhs.FileName.begin(), lhs.FileName.end(), rhs.FileName.begin(), + // rhs.FileName.end()); + return (StringUtil::Strcasecmp(lhs.FileName.c_str(), rhs.FileName.c_str()) < 0); + }); + + for (const FILESYSTEM_FIND_DATA& fd : results) + { + std::string full_path(StringUtil::StdStringFromFormat( + "%s" FS_OSPATH_SEPARATOR_STR "%s", s_file_selector_current_directory.c_str(), fd.FileName.c_str())); + + if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) + { + std::string title(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", fd.FileName.c_str())); + s_file_selector_items.emplace_back(std::move(title), std::move(full_path), false); + } + else + { + if (s_file_selector_filters.empty() || + std::none_of(s_file_selector_filters.begin(), s_file_selector_filters.end(), + [&fd](const std::string& filter) { return StringUtil::WildcardMatch(fd.FileName.c_str(), filter.c_str()); })) + { + continue; + } + + std::string title(StringUtil::StdStringFromFormat(ICON_FA_FILE " %s", fd.FileName.c_str())); + s_file_selector_items.emplace_back(std::move(title), std::move(full_path), true); + } + } + } +} + +void ImGuiFullscreen::SetFileSelectorDirectory(std::string dir) +{ + while (!dir.empty() && dir.back() == FS_OSPATH_SEPARATOR_CHARACTER) + dir.erase(dir.size() - 1); + + s_file_selector_current_directory = std::move(dir); + PopulateFileSelectorItems(); +} + +bool ImGuiFullscreen::IsFileSelectorOpen() +{ + return s_file_selector_open; +} + +void ImGuiFullscreen::OpenFileSelector( + const char* title, bool select_directory, FileSelectorCallback callback, FileSelectorFilters filters, std::string initial_directory) +{ + if (s_file_selector_open) + CloseFileSelector(); + + s_file_selector_open = true; + s_file_selector_directory = select_directory; + s_file_selector_title = StringUtil::StdStringFromFormat("%s##file_selector", title); + s_file_selector_callback = std::move(callback); + s_file_selector_filters = std::move(filters); + + if (initial_directory.empty() || !FileSystem::DirectoryExists(initial_directory.c_str())) + initial_directory = FileSystem::GetWorkingDirectory(); + SetFileSelectorDirectory(std::move(initial_directory)); +} + +void ImGuiFullscreen::CloseFileSelector() +{ + if (!s_file_selector_open) + return; + + s_file_selector_open = false; + s_file_selector_directory = false; + std::string().swap(s_file_selector_title); + FileSelectorCallback().swap(s_file_selector_callback); + FileSelectorFilters().swap(s_file_selector_filters); + std::string().swap(s_file_selector_current_directory); + s_file_selector_items.clear(); + ImGui::CloseCurrentPopup(); + QueueResetFocus(); +} + +void ImGuiFullscreen::DrawFileSelector() +{ + if (!s_file_selector_open) + return; + + ImGui::SetNextWindowSize(LayoutScale(1000.0f, 680.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(s_file_selector_title.c_str()); + + FileSelectorItem* selected = nullptr; + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); + ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor); + + bool is_open = !WantsToCloseMenu(); + bool directory_selected = false; + if (ImGui::BeginPopupModal( + s_file_selector_title.c_str(), &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor); + + BeginMenuButtons(); + + if (!s_file_selector_current_directory.empty()) + { + MenuButton(fmt::format(ICON_FA_FOLDER_OPEN " {}", s_file_selector_current_directory).c_str(), nullptr, false, + LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + + if (s_file_selector_directory && !s_file_selector_current_directory.empty()) + { + if (MenuButton(ICON_FA_FOLDER_PLUS " ", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + directory_selected = true; + } + + for (FileSelectorItem& item : s_file_selector_items) + { + if (MenuButton(item.display_name.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + selected = &item; + } + + EndMenuButtons(); + + ImGui::PopStyleColor(1); + + ImGui::EndPopup(); + } + else + { + is_open = false; + } + + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(2); + ImGui::PopFont(); + + if (selected) + { + if (selected->is_file) + { + s_file_selector_callback(selected->full_path); + } + else + { + SetFileSelectorDirectory(std::move(selected->full_path)); + } + } + else if (directory_selected) + { + s_file_selector_callback(s_file_selector_current_directory); + } + else if (!is_open) + { + std::string no_path; + s_file_selector_callback(no_path); + CloseFileSelector(); + } +} + +bool ImGuiFullscreen::IsChoiceDialogOpen() +{ + return s_choice_dialog_open; +} + +void ImGuiFullscreen::OpenChoiceDialog(const char* title, bool checkable, ChoiceDialogOptions options, ChoiceDialogCallback callback) +{ + if (s_choice_dialog_open) + CloseChoiceDialog(); + + s_choice_dialog_open = true; + s_choice_dialog_checkable = checkable; + s_choice_dialog_title = StringUtil::StdStringFromFormat("%s##choice_dialog", title); + s_choice_dialog_options = std::move(options); + s_choice_dialog_callback = std::move(callback); +} + +void ImGuiFullscreen::CloseChoiceDialog() +{ + if (!s_choice_dialog_open) + return; + + s_choice_dialog_open = false; + s_choice_dialog_checkable = false; + std::string().swap(s_choice_dialog_title); + ChoiceDialogOptions().swap(s_choice_dialog_options); + ChoiceDialogCallback().swap(s_choice_dialog_callback); + QueueResetFocus(); +} + +void ImGuiFullscreen::DrawChoiceDialog() +{ + if (!s_choice_dialog_open) + return; + + const float width = 600.0f; + const float title_height = g_large_font->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f; + const float height = std::min(400.0f, title_height + (LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) * + static_cast(s_choice_dialog_options.size())); + ImGui::SetNextWindowSize(LayoutScale(width, height)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(s_choice_dialog_title.c_str()); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); + ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor); + + bool is_open = !WantsToCloseMenu(); + s32 choice = -1; + + if (ImGui::BeginPopupModal( + s_choice_dialog_title.c_str(), &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor); + + BeginMenuButtons(); + + if (s_choice_dialog_checkable) + { + for (s32 i = 0; i < static_cast(s_choice_dialog_options.size()); i++) + { + auto& option = s_choice_dialog_options[i]; + + const std::string title(fmt::format("{0} {1}", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first)); + if (MenuButton(title.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + choice = i; + option.second = !option.second; + } + } + } + else + { + for (s32 i = 0; i < static_cast(s_choice_dialog_options.size()); i++) + { + auto& option = s_choice_dialog_options[i]; + std::string title; + if (option.second) + title += ICON_FA_CHECK " "; + title += option.first; + + if (ActiveButton(title.c_str(), option.second, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + choice = i; + for (s32 j = 0; j < static_cast(s_choice_dialog_options.size()); j++) + s_choice_dialog_options[j].second = (j == i); + } + } + } + + EndMenuButtons(); + + ImGui::PopStyleColor(1); + + ImGui::EndPopup(); + } + else + { + is_open = false; + } + + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(2); + ImGui::PopFont(); + + if (choice >= 0) + { + const auto& option = s_choice_dialog_options[choice]; + s_choice_dialog_callback(choice, option.first, option.second); + } + else if (!is_open) + { + std::string no_string; + s_choice_dialog_callback(-1, no_string, false); + CloseChoiceDialog(); + } +} + + +bool ImGuiFullscreen::IsInputDialogOpen() +{ + return s_input_dialog_open; +} + +void ImGuiFullscreen::OpenInputStringDialog( + std::string title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback) +{ + s_input_dialog_open = true; + s_input_dialog_title = std::move(title); + s_input_dialog_message = std::move(message); + s_input_dialog_caption = std::move(caption); + s_input_dialog_ok_text = std::move(ok_button_text); + s_input_dialog_callback = std::move(callback); +} + +void ImGuiFullscreen::DrawInputDialog() +{ + if (!s_input_dialog_open) + return; + + ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(s_input_dialog_title.c_str()); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + ImGui::PushFont(g_large_font); + + bool is_open = true; + if (ImGui::BeginPopupModal(s_input_dialog_title.c_str(), &is_open, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + ImGui::TextWrapped("%s", s_input_dialog_message.c_str()); + ImGui::NewLine(); + + if (!s_input_dialog_caption.empty()) + ImGui::TextUnformatted(s_input_dialog_caption.c_str()); + ImGui::InputText("##input", &s_input_dialog_text); + + ImGui::NewLine(); + + BeginMenuButtons(); + + const bool ok_enabled = !s_input_dialog_text.empty(); + + if (ActiveButton(s_input_dialog_ok_text.c_str(), false, ok_enabled) && ok_enabled) + { + // have to move out in case they open another dialog in the callback + InputStringDialogCallback cb(std::move(s_input_dialog_callback)); + std::string text(std::move(s_input_dialog_text)); + CloseInputDialog(); + ImGui::CloseCurrentPopup(); + cb(std::move(text)); + } + + if (ActiveButton(ICON_FA_TIMES " Cancel", false)) + { + CloseInputDialog(); + + ImGui::CloseCurrentPopup(); + } + + EndMenuButtons(); + + ImGui::EndPopup(); + } + if (!is_open) + CloseInputDialog(); + + ImGui::PopFont(); + ImGui::PopStyleVar(2); +} + +void ImGuiFullscreen::CloseInputDialog() +{ + if (!s_input_dialog_open) + return; + + s_input_dialog_open = false; + s_input_dialog_title = {}; + s_input_dialog_message = {}; + s_input_dialog_caption = {}; + s_input_dialog_ok_text = {}; + s_input_dialog_text = {}; + s_input_dialog_callback = {}; +} + +static float s_notification_vertical_position = 0.3f; +static float s_notification_vertical_direction = -1.0f; + +float ImGuiFullscreen::GetNotificationVerticalPosition() +{ + return s_notification_vertical_position; +} + +float ImGuiFullscreen::GetNotificationVerticalDirection() +{ + return s_notification_vertical_direction; +} + +void ImGuiFullscreen::SetNotificationVerticalPosition(float position, float direction) +{ + s_notification_vertical_position = position; + s_notification_vertical_direction = direction; +} + +ImGuiID ImGuiFullscreen::GetBackgroundProgressID(const char* str_id) +{ + return ImHashStr(str_id); +} + +void ImGuiFullscreen::OpenBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value) +{ + const ImGuiID id = GetBackgroundProgressID(str_id); + + std::unique_lock lock(s_background_progress_lock); + + for (const BackgroundProgressDialogData& data : s_background_progress_dialogs) + { + pxAssert(data.id != id); + } + + BackgroundProgressDialogData data; + data.id = id; + data.message = std::move(message); + data.min = min; + data.max = max; + data.value = value; + s_background_progress_dialogs.push_back(std::move(data)); +} + +void ImGuiFullscreen::UpdateBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value) +{ + const ImGuiID id = GetBackgroundProgressID(str_id); + + std::unique_lock lock(s_background_progress_lock); + + for (BackgroundProgressDialogData& data : s_background_progress_dialogs) + { + if (data.id == id) + { + data.message = std::move(message); + data.min = min; + data.max = max; + data.value = value; + return; + } + } + + pxFailRel("Updating unknown progress entry."); +} + +void ImGuiFullscreen::CloseBackgroundProgressDialog(const char* str_id) +{ + const ImGuiID id = GetBackgroundProgressID(str_id); + + std::unique_lock lock(s_background_progress_lock); + + for (auto it = s_background_progress_dialogs.begin(); it != s_background_progress_dialogs.end(); ++it) + { + if (it->id == id) + { + s_background_progress_dialogs.erase(it); + return; + } + } + + pxFailRel("Closing unknown progress entry."); +} + +void ImGuiFullscreen::DrawBackgroundProgressDialogs(ImVec2& position, float spacing) +{ + std::unique_lock lock(s_background_progress_lock); + if (s_background_progress_dialogs.empty()) + return; + + const float window_width = LayoutScale(500.0f); + const float window_height = LayoutScale(75.0f); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, UIPrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, UISecondaryLightColor); + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, LayoutScale(4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, LayoutScale(1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(10.0f, 10.0f)); + ImGui::PushFont(g_medium_font); + + ImDrawList* dl = ImGui::GetForegroundDrawList(); + + for (const BackgroundProgressDialogData& data : s_background_progress_dialogs) + { + const float window_pos_x = position.x; + const float window_pos_y = position.y - ((s_notification_vertical_direction < 0.0f) ? window_height : 0.0f); + + dl->AddRectFilled(ImVec2(window_pos_x, window_pos_y), ImVec2(window_pos_x + window_width, window_pos_y + window_height), + IM_COL32(0x11, 0x11, 0x11, 200), LayoutScale(10.0f)); + + ImVec2 pos(window_pos_x + LayoutScale(10.0f), window_pos_y + LayoutScale(10.0f)); + dl->AddText(g_medium_font, g_medium_font->FontSize, pos, IM_COL32(255, 255, 255, 255), data.message.c_str(), nullptr, 0.0f); + pos.y += g_medium_font->FontSize + LayoutScale(10.0f); + + ImVec2 bar_end(pos.x + window_width - LayoutScale(10.0f * 2.0f), pos.y + LayoutScale(25.0f)); + float fraction = static_cast(data.value - data.min) / static_cast(data.max - data.min); + dl->AddRectFilled(pos, bar_end, ImGui::GetColorU32(UIPrimaryDarkColor)); + dl->AddRectFilled(pos, ImVec2(pos.x + fraction * (bar_end.x - pos.x), bar_end.y), ImGui::GetColorU32(UISecondaryColor)); + + const std::string text(fmt::format("{}%", static_cast(std::round(fraction * 100.0f)))); + const ImVec2 text_size(ImGui::CalcTextSize(text.c_str())); + const ImVec2 text_pos( + pos.x + ((bar_end.x - pos.x) / 2.0f) - (text_size.x / 2.0f), pos.y + ((bar_end.y - pos.y) / 2.0f) - (text_size.y / 2.0f)); + dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, ImGui::GetColorU32(UIPrimaryTextColor), text.c_str()); + + position.y += s_notification_vertical_direction * (window_height + spacing); + } + + ImGui::PopFont(); + ImGui::PopStyleVar(4); + ImGui::PopStyleColor(2); +} + +////////////////////////////////////////////////////////////////////////// +// Notifications +////////////////////////////////////////////////////////////////////////// + +void ImGuiFullscreen::AddNotification(float duration, std::string title, std::string text, std::string image_path) +{ + Notification notif; + notif.duration = duration; + notif.title = std::move(title); + notif.text = std::move(text); + notif.badge_path = std::move(image_path); + notif.start_time = Common::Timer::GetCurrentValue(); + s_notifications.push_back(std::move(notif)); +} + +void ImGuiFullscreen::ClearNotifications() +{ + s_notifications.clear(); +} + +void ImGuiFullscreen::DrawNotifications(ImVec2& position, float spacing) +{ + if (s_notifications.empty()) + return; + + static constexpr float EASE_IN_TIME = 0.6f; + static constexpr float EASE_OUT_TIME = 0.6f; + const Common::Timer::Value current_time = Common::Timer::GetCurrentValue(); + + const float horizontal_padding = ImGuiFullscreen::LayoutScale(20.0f); + const float vertical_padding = ImGuiFullscreen::LayoutScale(10.0f); + const float horizontal_spacing = ImGuiFullscreen::LayoutScale(10.0f); + const float vertical_spacing = ImGuiFullscreen::LayoutScale(4.0f); + const float badge_size = ImGuiFullscreen::LayoutScale(48.0f); + const float min_width = ImGuiFullscreen::LayoutScale(200.0f); + const float max_width = ImGuiFullscreen::LayoutScale(800.0f); + const float max_text_width = max_width - badge_size - (horizontal_padding * 2.0f) - horizontal_spacing; + const float min_height = (vertical_padding * 2.0f) + badge_size; + const float shadow_size = ImGuiFullscreen::LayoutScale(4.0f); + const float rounding = ImGuiFullscreen::LayoutScale(4.0f); + + ImFont* const title_font = ImGuiFullscreen::g_large_font; + ImFont* const text_font = ImGuiFullscreen::g_medium_font; + +#if 0 + static constexpr u32 toast_background_color = IM_COL32(241, 241, 241, 255); + static constexpr u32 toast_border_color = IM_COL32(0x88, 0x88, 0x88, 255); + static constexpr u32 toast_title_color = IM_COL32(1, 1, 1, 255); + static constexpr u32 toast_text_color = IM_COL32(0, 0, 0, 255); +#else + static constexpr u32 toast_background_color = IM_COL32(0x21, 0x21, 0x21, 255); + static constexpr u32 toast_border_color = IM_COL32(0x48, 0x48, 0x48, 255); + static constexpr u32 toast_title_color = IM_COL32(0xff, 0xff, 0xff, 255); + static constexpr u32 toast_text_color = IM_COL32(0xff, 0xff, 0xff, 255); +#endif + + for (u32 index = 0; index < static_cast(s_notifications.size());) + { + const Notification& notif = s_notifications[index]; + const float time_passed = static_cast(Common::Timer::ConvertValueToSeconds(current_time - notif.start_time)); + if (time_passed >= notif.duration) + { + s_notifications.erase(s_notifications.begin() + index); + continue; + } + + const ImVec2 title_size(text_font->CalcTextSizeA( + title_font->FontSize, max_text_width, max_text_width, notif.title.c_str(), notif.title.c_str() + notif.title.size())); + + const ImVec2 text_size(text_font->CalcTextSizeA( + text_font->FontSize, max_text_width, max_text_width, notif.text.c_str(), notif.text.c_str() + notif.text.size())); + + const float box_width = + std::max((horizontal_padding * 2.0f) + badge_size + horizontal_spacing + std::max(title_size.x, text_size.x), min_width); + const float box_height = std::max((vertical_padding * 2.0f) + title_size.y + vertical_spacing + text_size.y, min_height); + + float x_offset = 0.0f; + if (time_passed < EASE_IN_TIME) + { + const float disp = (box_width + position.x); + x_offset = -(disp - (disp * Easing::InBack(time_passed / EASE_IN_TIME))); + } + else if (time_passed > (notif.duration - EASE_OUT_TIME)) + { + const float disp = (box_width + position.x); + x_offset = -(disp - (disp * Easing::OutBack((notif.duration - time_passed) / EASE_OUT_TIME))); + } + + const ImVec2 box_min(position.x + x_offset, position.y - ((s_notification_vertical_direction < 0.0f) ? box_height : 0.0f)); + const ImVec2 box_max(box_min.x + box_width, box_min.y + box_height); + + ImDrawList* dl = ImGui::GetForegroundDrawList(); + dl->AddRectFilled(ImVec2(box_min.x + shadow_size, box_min.y + shadow_size), + ImVec2(box_max.x + shadow_size, box_max.y + shadow_size), IM_COL32(20, 20, 20, 180), rounding, ImDrawCornerFlags_All); + dl->AddRectFilled(box_min, box_max, toast_background_color, rounding, ImDrawCornerFlags_All); + dl->AddRect(box_min, box_max, toast_border_color, rounding, ImDrawCornerFlags_All, ImGuiFullscreen::LayoutScale(1.0f)); + + const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding); + const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size); + if (!notif.badge_path.empty()) + { + HostDisplayTexture* tex = GetCachedTexture(notif.badge_path.c_str()); + if (tex) + dl->AddImage(static_cast(tex->GetHandle()), badge_min, badge_max); + } + + const ImVec2 title_min(badge_max.x + horizontal_spacing, box_min.y + vertical_padding); + const ImVec2 title_max(title_min.x + title_size.x, title_min.y + title_size.y); + dl->AddText(title_font, title_font->FontSize, title_min, toast_title_color, notif.title.c_str(), + notif.title.c_str() + notif.title.size(), max_text_width); + + const ImVec2 text_min(badge_max.x + horizontal_spacing, title_max.y + vertical_spacing); + const ImVec2 text_max(text_min.x + text_size.x, text_min.y + text_size.y); + dl->AddText(text_font, text_font->FontSize, text_min, toast_text_color, notif.text.c_str(), notif.text.c_str() + notif.text.size(), + max_text_width); + + position.y += s_notification_vertical_direction * (box_height + shadow_size + spacing); + index++; + } +} + +void ImGuiFullscreen::ShowToast(std::string title, std::string message, float duration) +{ + s_toast_title = std::move(title); + s_toast_message = std::move(message); + s_toast_start_time = Common::Timer::GetCurrentValue(); + s_toast_duration = duration; +} + +void ImGuiFullscreen::ClearToast() +{ + s_toast_message = {}; + s_toast_title = {}; + s_toast_start_time = 0; + s_toast_duration = 0.0f; +} + +void ImGuiFullscreen::DrawToast() +{ + if (s_toast_title.empty() && s_toast_message.empty()) + return; + + const float elapsed = static_cast(Common::Timer::ConvertValueToSeconds(Common::Timer::GetCurrentValue() - s_toast_start_time)); + if (elapsed >= s_toast_duration) + { + ClearToast(); + return; + } + + // fade out the last second + const float alpha = std::min(std::min(elapsed * 4.0f, s_toast_duration - elapsed), 1.0f); + + const float max_width = LayoutScale(600.0f); + + ImFont* title_font = g_large_font; + ImFont* message_font = g_medium_font; + const float padding = LayoutScale(20.0f); + const float total_padding = padding * 2.0f; + const float margin = LayoutScale(20.0f); + const float spacing = s_toast_title.empty() ? 0.0f : LayoutScale(10.0f); + const ImVec2 display_size(ImGui::GetIO().DisplaySize); + const ImVec2 title_size(s_toast_title.empty() ? ImVec2(0.0f, 0.0f) : + title_font->CalcTextSizeA(title_font->FontSize, FLT_MAX, max_width, + s_toast_title.c_str(), s_toast_title.c_str() + s_toast_title.length())); + const ImVec2 message_size(s_toast_message.empty() ? ImVec2(0.0f, 0.0f) : + message_font->CalcTextSizeA(message_font->FontSize, FLT_MAX, max_width, + s_toast_message.c_str(), s_toast_message.c_str() + s_toast_message.length())); + const ImVec2 comb_size(std::max(title_size.x, message_size.x), title_size.y + spacing + message_size.y); + + const ImVec2 box_size(comb_size.x + total_padding, comb_size.y + total_padding); + const ImVec2 box_pos((display_size.x - box_size.x) * 0.5f, (display_size.y - margin - box_size.y)); + + ImDrawList* dl = ImGui::GetForegroundDrawList(); + dl->AddRectFilled(box_pos, box_pos + box_size, ImGui::GetColorU32(ModAlpha(UIPrimaryColor, alpha)), padding); + if (!s_toast_title.empty()) + { + const float offset = (comb_size.x - title_size.x) * 0.5f; + dl->AddText(title_font, title_font->FontSize, box_pos + ImVec2(offset + padding, padding), + ImGui::GetColorU32(ModAlpha(UIPrimaryTextColor, alpha)), s_toast_title.c_str(), s_toast_title.c_str() + s_toast_title.length(), + max_width); + } + if (!s_toast_message.empty()) + { + const float offset = (comb_size.x - message_size.x) * 0.5f; + dl->AddText(message_font, message_font->FontSize, box_pos + ImVec2(offset + padding, padding + spacing + title_size.y), + ImGui::GetColorU32(ModAlpha(UIPrimaryTextColor, alpha)), s_toast_message.c_str(), + s_toast_message.c_str() + s_toast_message.length(), max_width); + } +} + +void ImGuiFullscreen::SetTheme() +{ +#if 1 + // dark + UIBackgroundColor = HEX_TO_IMVEC4(0x212121, 0xff); + UIBackgroundTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); + UIBackgroundLineColor = HEX_TO_IMVEC4(0xf0f0f0, 0xff); + UIBackgroundHighlightColor = HEX_TO_IMVEC4(0x4b4b4b, 0xff); + UIPrimaryColor = HEX_TO_IMVEC4(0x2e2e2e, 0xff); + UIPrimaryLightColor = HEX_TO_IMVEC4(0x484848, 0xff); + UIPrimaryDarkColor = HEX_TO_IMVEC4(0x000000, 0xff); + UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); + UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff); + UITextHighlightColor = HEX_TO_IMVEC4(0x90caf9, 0xff); + UIPrimaryLineColor = HEX_TO_IMVEC4(0xffffff, 0xff); + UISecondaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff); + UISecondaryLightColor = HEX_TO_IMVEC4(0x63a4ff, 0xff); + UISecondaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff); + UISecondaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); +#elif 1 + // light + UIBackgroundColor = HEX_TO_IMVEC4(0xf5f5f6, 0xff); + UIBackgroundTextColor = HEX_TO_IMVEC4(0x000000, 0xff); + UIBackgroundLineColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff); + UIBackgroundHighlightColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff); + UIPrimaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff); + UIPrimaryLightColor = HEX_TO_IMVEC4(0x5472d3, 0xff); + UIPrimaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff); + UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); + UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff); + UITextHighlightColor = HEX_TO_IMVEC4(0x8e8e8e, 0xff); + UIPrimaryLineColor = HEX_TO_IMVEC4(0x000000, 0xff); + UISecondaryColor = HEX_TO_IMVEC4(0x3d5afe, 0xff); + UISecondaryLightColor = HEX_TO_IMVEC4(0xc0cfff, 0xff); + UISecondaryDarkColor = HEX_TO_IMVEC4(0x0031ca, 0xff); + UISecondaryTextColor = HEX_TO_IMVEC4(0x000000, 0xff); +#endif +} diff --git a/pcsx2/Frontend/ImGuiFullscreen.h b/pcsx2/Frontend/ImGuiFullscreen.h new file mode 100644 index 0000000000..e8db08153e --- /dev/null +++ b/pcsx2/Frontend/ImGuiFullscreen.h @@ -0,0 +1,239 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2022 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCSX2. + * If not, see . + */ + +#pragma once +#include "common/Pcsx2Defs.h" +#include "imgui.h" +#include "imgui_internal.h" +#include +#include +#include +#include +#include + +class HostDisplayTexture; + +namespace ImGuiFullscreen +{ +#define HEX_TO_IMVEC4(hex, alpha) \ + ImVec4(static_cast((hex >> 16) & 0xFFu) / 255.0f, static_cast((hex >> 8) & 0xFFu) / 255.0f, \ + static_cast(hex & 0xFFu) / 255.0f, static_cast(alpha) / 255.0f) + + static constexpr float LAYOUT_SCREEN_WIDTH = 1280.0f; + static constexpr float LAYOUT_SCREEN_HEIGHT = 720.0f; + static constexpr float LAYOUT_LARGE_FONT_SIZE = 26.0f; + static constexpr float LAYOUT_MEDIUM_FONT_SIZE = 16.0f; + static constexpr float LAYOUT_SMALL_FONT_SIZE = 10.0f; + static constexpr float LAYOUT_MENU_BUTTON_HEIGHT = 50.0f; + static constexpr float LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY = 26.0f; + static constexpr float LAYOUT_MENU_BUTTON_X_PADDING = 15.0f; + static constexpr float LAYOUT_MENU_BUTTON_Y_PADDING = 10.0f; + + extern ImFont* g_standard_font; + extern ImFont* g_medium_font; + extern ImFont* g_large_font; + + extern float g_layout_scale; + extern float g_layout_padding_left; + extern float g_layout_padding_top; + + extern ImVec4 UIBackgroundColor; + extern ImVec4 UIBackgroundTextColor; + extern ImVec4 UIBackgroundLineColor; + extern ImVec4 UIBackgroundHighlightColor; + extern ImVec4 UIDisabledColor; + extern ImVec4 UIPrimaryColor; + extern ImVec4 UIPrimaryLightColor; + extern ImVec4 UIPrimaryDarkColor; + extern ImVec4 UIPrimaryTextColor; + extern ImVec4 UITextHighlightColor; + extern ImVec4 UIPrimaryLineColor; + extern ImVec4 UISecondaryColor; + extern ImVec4 UISecondaryLightColor; + extern ImVec4 UISecondaryDarkColor; + extern ImVec4 UISecondaryTextColor; + + static __fi float DPIScale(float v) { return ImGui::GetIO().DisplayFramebufferScale.x * v; } + + static __fi float DPIScale(int v) { return ImGui::GetIO().DisplayFramebufferScale.x * static_cast(v); } + + static __fi ImVec2 DPIScale(const ImVec2& v) + { + const ImVec2& fbs = ImGui::GetIO().DisplayFramebufferScale; + return ImVec2(v.x * fbs.x, v.y * fbs.y); + } + + static __fi float WindowWidthScale(float v) { return ImGui::GetWindowWidth() * v; } + + static __fi float WindowHeightScale(float v) { return ImGui::GetWindowHeight() * v; } + + static __fi float LayoutScale(float v) { return g_layout_scale * v; } + + static __fi ImVec2 LayoutScale(const ImVec2& v) { return ImVec2(v.x * g_layout_scale, v.y * g_layout_scale); } + + static __fi ImVec2 LayoutScale(float x, float y) { return ImVec2(x * g_layout_scale, y * g_layout_scale); } + + static __fi ImVec2 LayoutScaleAndOffset(float x, float y) + { + return ImVec2(g_layout_padding_left + x * g_layout_scale, g_layout_padding_top + y * g_layout_scale); + } + + static __fi ImVec4 ModAlpha(const ImVec4& v, float a) + { + return ImVec4(v.x, v.y, v.z, a); + } + + /// Centers an image within the specified bounds, scaling up or down as needed. + ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size); + ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size); + + /// Initializes, setting up any state. + bool Initialize(const char* placeholder_image_path); + + void SetTheme(); + void SetFonts(ImFont* standard_font, ImFont* medium_font, ImFont* large_font); + bool UpdateLayoutScale(); + + /// Shuts down, clearing all state. + void Shutdown(); + + /// Texture cache. + const std::shared_ptr& GetPlaceholderTexture(); + std::shared_ptr LoadTexture(const char* path); + HostDisplayTexture* GetCachedTexture(const char* name); + HostDisplayTexture* GetCachedTextureAsync(const char* name); + bool InvalidateCachedTexture(const std::string& path); + void UploadAsyncTextures(); + + void BeginLayout(); + void EndLayout(); + + void QueueResetFocus(); + bool ResetFocusHere(); + bool WantsToCloseMenu(); + void ResetCloseMenuIfNeeded(); + + void PushPrimaryColor(); + void PopPrimaryColor(); + void PushSecondaryColor(); + void PopSecondaryColor(); + + void DrawWindowTitle(const char* title); + + bool BeginFullscreenColumns(const char* title = nullptr); + void EndFullscreenColumns(); + + bool BeginFullscreenColumnWindow(float start, float end, const char* name, const ImVec4& background = UIBackgroundColor); + void EndFullscreenColumnWindow(); + + bool BeginFullscreenWindow(float left, float top, float width, float height, const char* name, + const ImVec4& background = HEX_TO_IMVEC4(0x212121, 0xFF), float rounding = 0.0f, float padding = 0.0f, ImGuiWindowFlags flags = 0); + bool BeginFullscreenWindow(const ImVec2& position, const ImVec2& size, const char* name, + const ImVec4& background = HEX_TO_IMVEC4(0x212121, 0xFF), float rounding = 0.0f, float padding = 0.0f, ImGuiWindowFlags flags = 0); + void EndFullscreenWindow(); + + void BeginMenuButtons(u32 num_items = 0, float y_align = 0.0f, float x_padding = LAYOUT_MENU_BUTTON_X_PADDING, + float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING, float item_height = LAYOUT_MENU_BUTTON_HEIGHT); + void EndMenuButtons(); + bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImVec2* min, ImVec2* max, + ImGuiButtonFlags flags = 0, float hover_alpha = 1.0f); + void MenuHeading(const char* title, bool draw_line = true); + bool MenuHeadingButton(const char* title, const char* value = nullptr, bool enabled = true, bool draw_line = true); + bool ActiveButton(const char* title, bool is_active, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, + ImFont* font = g_large_font); + bool MenuButton(const char* title, const char* summary, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + bool MenuButtonWithValue(const char* title, const char* summary, const char* value, bool enabled = true, + float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + bool MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id, const ImVec2& image_size, bool enabled = true, + float height = LAYOUT_MENU_BUTTON_HEIGHT, const ImVec2& uv0 = ImVec2(0.0f, 0.0f), const ImVec2& uv1 = ImVec2(1.0f, 1.0f), + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + bool FloatingButton(const char* text, float x, float y, float width = -1.0f, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, + float anchor_x = 0.0f, float anchor_y = 0.0f, bool enabled = true, ImFont* font = g_large_font, ImVec2* out_position = nullptr); + bool ToggleButton(const char* title, const char* summary, bool* v, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + bool ThreeWayToggleButton(const char* title, const char* summary, std::optional* v, bool enabled = true, + float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + bool RangeButton(const char* title, const char* summary, s32* value, s32 min, s32 max, s32 increment, const char* format = "%d", + bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + bool RangeButton(const char* title, const char* summary, float* value, float min, float max, float increment, const char* format = "%f", + bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + bool EnumChoiceButtonImpl(const char* title, const char* summary, s32* value_pointer, + const char* (*to_display_name_function)(s32 value, void* opaque), void* opaque, u32 count, bool enabled, float height, ImFont* font, + ImFont* summary_font); + + template + static __fi bool EnumChoiceButton(const char* title, const char* summary, DataType* value_pointer, + const char* (*to_display_name_function)(DataType value), CountType count, bool enabled = true, + float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font) + { + s32 value = static_cast(*value_pointer); + auto to_display_name_wrapper = [](s32 value, void* opaque) -> const char* { + return (*static_cast(opaque))(static_cast(value)); + }; + + if (EnumChoiceButtonImpl(title, summary, &value, to_display_name_wrapper, &to_display_name_function, static_cast(count), + enabled, height, font, summary_font)) + { + *value_pointer = static_cast(value); + return true; + } + else + { + return false; + } + } + + void BeginNavBar(float x_padding = LAYOUT_MENU_BUTTON_X_PADDING, float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING); + void EndNavBar(); + void NavTitle(const char* title, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font); + void RightAlignNavButtons(u32 num_items = 0, float item_width = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, + float item_height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + bool NavButton(const char* title, bool is_active, bool enabled = true, float width = -1.0f, + float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font); + + using FileSelectorCallback = std::function; + using FileSelectorFilters = std::vector; + bool IsFileSelectorOpen(); + void OpenFileSelector(const char* title, bool select_directory, FileSelectorCallback callback, + FileSelectorFilters filters = FileSelectorFilters(), std::string initial_directory = std::string()); + void CloseFileSelector(); + + using ChoiceDialogCallback = std::function; + using ChoiceDialogOptions = std::vector>; + bool IsChoiceDialogOpen(); + void OpenChoiceDialog(const char* title, bool checkable, ChoiceDialogOptions options, ChoiceDialogCallback callback); + void CloseChoiceDialog(); + + using InputStringDialogCallback = std::function; + bool IsInputDialogOpen(); + void OpenInputStringDialog( + std::string title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback); + void CloseInputDialog(); + + float GetNotificationVerticalPosition(); + float GetNotificationVerticalDirection(); + void SetNotificationVerticalPosition(float position, float direction); + + void OpenBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value); + void UpdateBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value); + void CloseBackgroundProgressDialog(const char* str_id); + + void AddNotification(float duration, std::string title, std::string text, std::string image_path); + void ClearNotifications(); + + void ShowToast(std::string title, std::string message, float duration = 10.0f); + void ClearToast(); +} // namespace ImGuiFullscreen diff --git a/pcsx2/Frontend/ImGuiManager.cpp b/pcsx2/Frontend/ImGuiManager.cpp index 45b7455881..095d71319b 100644 --- a/pcsx2/Frontend/ImGuiManager.cpp +++ b/pcsx2/Frontend/ImGuiManager.cpp @@ -34,7 +34,6 @@ #include "Config.h" #include "Counters.h" #include "Frontend/ImGuiManager.h" -#include "Frontend/InputManager.h" #include "GS.h" #include "GS/GS.h" #include "Host.h" @@ -43,6 +42,10 @@ #include "PerformanceMetrics.h" #ifdef PCSX2_CORE +#include "Frontend/FullscreenUI.h" +#include "Frontend/ImGuiManager.h" +#include "Frontend/ImGuiFullscreen.h" +#include "Frontend/InputManager.h" #include "VMManager.h" #endif @@ -52,7 +55,7 @@ namespace ImGuiManager static void SetKeyMap(); static bool LoadFontData(); static void UnloadFontData(); - static bool AddImGuiFonts(); + static bool AddImGuiFonts(bool fullscreen_fonts); static ImFont* AddTextFont(float size); static ImFont* AddFixedFont(float size); static bool AddIconFonts(float size); @@ -66,6 +69,8 @@ static float s_global_scale = 1.0f; static ImFont* s_standard_font; static ImFont* s_fixed_font; +static ImFont* s_medium_font; +static ImFont* s_large_font; static std::vector s_standard_font_data; static std::vector s_fixed_font_data; @@ -111,6 +116,10 @@ bool ImGuiManager::Initialize() SetKeyMap(); SetStyle(); +#ifdef PCSX2_CORE + pxAssertRel(!FullscreenUI::IsInitialized(), "Fullscreen UI is not initialized on ImGui init"); +#endif + if (!display->CreateImGuiContext()) { pxFailRel("Failed to create ImGui device context"); @@ -120,7 +129,7 @@ bool ImGuiManager::Initialize() return false; } - if (!AddImGuiFonts() || !display->UpdateImGuiFontTexture()) + if (!AddImGuiFonts(false) || !display->UpdateImGuiFontTexture()) { pxFailRel("Failed to create ImGui font text"); display->DestroyImGuiContext(); @@ -138,6 +147,10 @@ bool ImGuiManager::Initialize() void ImGuiManager::Shutdown() { +#ifdef PCSX2_CORE + FullscreenUI::Shutdown(); +#endif + HostDisplay* display = Host::GetHostDisplay(); if (display) display->DestroyImGuiContext(); @@ -146,6 +159,11 @@ void ImGuiManager::Shutdown() s_standard_font = nullptr; s_fixed_font = nullptr; + s_medium_font = nullptr; + s_large_font = nullptr; +#ifdef PCSX2_CORE + ImGuiFullscreen::SetFonts(nullptr, nullptr, nullptr); +#endif UnloadFontData(); } @@ -172,8 +190,13 @@ void ImGuiManager::UpdateScale() const float window_scale = display ? display->GetWindowScale() : 1.0f; const float scale = std::max(window_scale * static_cast(EmuConfig.GS.OsdScale / 100.0), 1.0f); +#ifdef PCSX2_CORE + if (scale == s_global_scale && (!HasFullscreenFonts() || !ImGuiFullscreen::UpdateLayoutScale())) + return; +#else if (scale == s_global_scale) return; +#endif // This is assumed to be called mid-frame. ImGui::EndFrame(); @@ -185,7 +208,7 @@ void ImGuiManager::UpdateScale() SetStyle(); ImGui::GetStyle().ScaleAllSizes(scale); - if (!AddImGuiFonts()) + if (!AddImGuiFonts(HasFullscreenFonts())) pxFailRel("Failed to create ImGui font text"); if (!display->UpdateImGuiFontTexture()) @@ -393,7 +416,7 @@ ImFont* ImGuiManager::AddFixedFont(float size) bool ImGuiManager::AddIconFonts(float size) { - static const ImWchar range_fa[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; + static constexpr ImWchar range_fa[] = { 0xf001,0xf002,0xf005,0xf005,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf021,0xf021,0xf025,0xf025,0xf028,0xf028,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf04a,0xf04c,0xf04e,0xf04e,0xf050,0xf050,0xf052,0xf052,0xf059,0xf059,0xf05e,0xf05e,0xf065,0xf065,0xf067,0xf067,0xf071,0xf071,0xf07b,0xf07c,0xf085,0xf085,0xf091,0xf091,0xf0a0,0xf0a0,0xf0ac,0xf0ad,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0d0,0xf0d0,0xf0e2,0xf0e2,0xf0eb,0xf0eb,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf11b,0xf11c,0xf121,0xf121,0xf140,0xf140,0xf144,0xf144,0xf14a,0xf14a,0xf15b,0xf15b,0xf188,0xf188,0xf192,0xf192,0xf1c9,0xf1c9,0xf1dd,0xf1de,0xf1e6,0xf1e6,0xf1ea,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f5,0xf2f5,0xf410,0xf410,0xf466,0xf466,0xf51f,0xf51f,0xf543,0xf543,0xf545,0xf545,0xf547,0xf548,0xf552,0xf552,0xf65d,0xf65e,0xf756,0xf756,0xf7c2,0xf7c2,0xf815,0xf815,0xf818,0xf818,0xf8cc,0xf8cc,0x0,0x0 }; ImFontConfig cfg; cfg.MergeMode = true; @@ -406,7 +429,7 @@ bool ImGuiManager::AddIconFonts(float size) s_icon_font_data.data(), static_cast(s_icon_font_data.size()), size * 0.75f, &cfg, range_fa) != nullptr); } -bool ImGuiManager::AddImGuiFonts() +bool ImGuiManager::AddImGuiFonts(bool fullscreen_fonts) { const float standard_font_size = std::ceil(15.0f * s_global_scale); @@ -421,9 +444,56 @@ bool ImGuiManager::AddImGuiFonts() if (!s_fixed_font) return false; +#ifdef PCSX2_CORE + if (fullscreen_fonts) + { + const float medium_font_size = std::ceil(ImGuiFullscreen::LayoutScale(ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE)); + s_medium_font = AddTextFont(medium_font_size); + if (!s_medium_font || !AddIconFonts(medium_font_size)) + return false; + + const float large_font_size = std::ceil(ImGuiFullscreen::LayoutScale(ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE)); + s_large_font = AddTextFont(large_font_size); + if (!s_large_font || !AddIconFonts(large_font_size)) + return false; + } + else + { + s_medium_font = nullptr; + s_large_font = nullptr; + } + + ImGuiFullscreen::SetFonts(s_standard_font, s_medium_font, s_large_font); +#endif + return io.Fonts->Build(); } +bool ImGuiManager::AddFullscreenFontsIfMissing() +{ + if (HasFullscreenFonts()) + return true; + + // can't do this in the middle of a frame + ImGui::EndFrame(); + + if (!AddImGuiFonts(true)) + { + Console.Error("Failed to lazily allocate fullscreen fonts."); + AddImGuiFonts(false); + } + + Host::GetHostDisplay()->UpdateImGuiFontTexture(); + NewFrame(); + + return HasFullscreenFonts(); +} + +bool ImGuiManager::HasFullscreenFonts() +{ + return (s_medium_font && s_large_font); +} + struct OSDMessage { std::string key; @@ -767,7 +837,13 @@ void ImGuiManager::RenderOSD() // acquire for IO.MousePos. std::atomic_thread_fence(std::memory_order_acquire); +#ifdef PCSX2_CORE + // Don't draw OSD when we're just running big picture. + if (VMManager::HasValidVM()) + DrawPerformanceOverlay(); +#else DrawPerformanceOverlay(); +#endif AcquirePendingOSDMessages(); DrawOSDMessages(); @@ -788,6 +864,18 @@ ImFont* ImGuiManager::GetFixedFont() return s_fixed_font; } +ImFont* ImGuiManager::GetMediumFont() +{ + AddFullscreenFontsIfMissing(); + return s_medium_font; +} + +ImFont* ImGuiManager::GetLargeFont() +{ + AddFullscreenFontsIfMissing(); + return s_large_font; +} + #ifdef PCSX2_CORE bool ImGuiManager::WantsTextInput() @@ -900,4 +988,5 @@ bool ImGuiManager::ProcessGenericInputEvent(GenericInputBinding key, float value return true; } -#endif // PCSX2_CORE \ No newline at end of file +#endif // PCSX2_CORE + diff --git a/pcsx2/Frontend/ImGuiManager.h b/pcsx2/Frontend/ImGuiManager.h index 54473083e8..c50e7255bf 100644 --- a/pcsx2/Frontend/ImGuiManager.h +++ b/pcsx2/Frontend/ImGuiManager.h @@ -43,12 +43,26 @@ namespace ImGuiManager /// Returns the scale of all on-screen elements. float GetGlobalScale(); + /// Returns true if fullscreen fonts are present. + bool HasFullscreenFonts(); + + /// Allocates/adds fullscreen fonts if they're not loaded. + bool AddFullscreenFontsIfMissing(); + /// Returns the standard font for external drawing. ImFont* GetStandardFont(); /// Returns the fixed-width font for external drawing. ImFont* GetFixedFont(); + /// Returns the medium font for external drawing, scaled by ImGuiFullscreen. + /// This font is allocated on demand. + ImFont* GetMediumFont(); + + /// Returns the large font for external drawing, scaled by ImGuiFullscreen. + /// This font is allocated on demand. + ImFont* GetLargeFont(); + #ifdef PCSX2_CORE /// Returns true if imgui wants to intercept text input. bool WantsTextInput(); diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp index 0445cd40b1..e3d4110383 100644 --- a/pcsx2/GS/GS.cpp +++ b/pcsx2/GS/GS.cpp @@ -40,6 +40,7 @@ #include "pcsx2/GS.h" #ifdef PCSX2_CORE #include "pcsx2/HostSettings.h" +#include "pcsx2/Frontend/FullscreenUI.h" #include "pcsx2/Frontend/InputManager.h" #endif @@ -281,7 +282,13 @@ static bool DoGSOpen(GSRendererType renderer, u8* basemem) return false; } +#ifdef PCSX2_CORE + // Don't override the fullscreen UI's vsync choice. + if (!FullscreenUI::IsInitialized()) + display->SetVSync(EmuConfig.GetEffectiveVsyncMode()); +#else display->SetVSync(EmuConfig.GetEffectiveVsyncMode()); +#endif GSConfig.OsdShowGPU = EmuConfig.GS.OsdShowGPU && display->SetGPUTimingEnabled(true); g_gs_renderer->SetRegsMem(basemem); diff --git a/pcsx2/PAD/Host/PAD.cpp b/pcsx2/PAD/Host/PAD.cpp index e8df1dcb08..ee9ee1aa22 100644 --- a/pcsx2/PAD/Host/PAD.cpp +++ b/pcsx2/PAD/Host/PAD.cpp @@ -361,7 +361,8 @@ void PAD::SetDefaultConfig(SettingsInterface& si) // si.SetStringValue("Hotkeys", "FrameAdvance", "Keyboard"); TBD // si.SetStringValue("Hotkeys", "IncreaseSpeed", "Keyboard"); TBD // si.SetStringValue("Hotkeys", "ResetVM", "Keyboard"); TBD - si.SetStringValue("Hotkeys", "ShutdownVM", "Keyboard/Escape"); + // si.SetStringValue("Hotkeys", "ShutdownVM", "Keyboard"); TBD + si.SetStringValue("Hotkeys", "OpenPauseMenu", "Keyboard/Escape"); si.SetStringValue("Hotkeys", "ToggleFrameLimit", "Keyboard/F4"); si.SetStringValue("Hotkeys", "TogglePause", "Keyboard/Space"); si.SetStringValue("Hotkeys", "ToggleSlowMotion", "Keyboard/Shift & Keyboard/Backtab"); diff --git a/pcsx2/SysForwardDefs.h b/pcsx2/SysForwardDefs.h index 6d595e3af3..d13caed1ae 100644 --- a/pcsx2/SysForwardDefs.h +++ b/pcsx2/SysForwardDefs.h @@ -36,6 +36,12 @@ #define VER_INTERNAL_NAME_STR VER_ORIGINAL_FILENAME_STR #define VER_COPYRIGHT_STR "Copyright (C) 2022" +#define PCSX2_WEBSITE_URL "https://pcsx2.net/" +#define PCSX2_FORUMS_URL "https://forums.pcsx2.net/" +#define PCSX2_GITHUB_URL "https://github.com/PCSX2/pcsx2" +#define PCSX2_LICENSE_URL "https://github.com/PCSX2/pcsx2/blob/master/pcsx2/Docs/License.txt" +#define PCSX2_DISCORD_URL "https://discord.com/invite/TCz3t9k" + static const bool PCSX2_isReleaseVersion = 0; class SysCoreThread; diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 91691d90df..f3934b31fd 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -56,6 +56,7 @@ #include "DebugTools/MIPSAnalyst.h" #include "DebugTools/SymbolMap.h" +#include "Frontend/FullscreenUI.h" #include "Frontend/INISettingsInterface.h" #include "Frontend/InputManager.h" #include "Frontend/GameList.h" @@ -72,7 +73,6 @@ namespace VMManager { - static void LoadSettings(); static void ApplyGameFixes(); static bool UpdateGameSettingsLayer(); static void CheckForConfigChanges(const Pcsx2Config& old_config); @@ -137,6 +137,7 @@ static s32 s_current_save_slot = 1; static u32 s_frame_advance_count = 0; static u32 s_mxcsr_saved; static std::optional s_limiter_mode_prior_to_hold_interaction; +static bool s_gs_open_on_initialize = false; bool VMManager::PerformEarlyHardwareChecks(const char** error) { @@ -200,9 +201,15 @@ void VMManager::SetState(VMState state) SPU2SetOutputPaused(state == VMState::Paused); if (state == VMState::Paused) + { Host::OnVMPaused(); + FullscreenUI::OnVMPaused(); + } else + { Host::OnVMResumed(); + FullscreenUI::OnVMResumed(); + } } } @@ -256,6 +263,12 @@ bool VMManager::Internal::InitializeGlobals() x86caps.CalculateMHz(); SysLogMachineCaps(); + if (GSinit() != 0) + { + Host::ReportErrorAsync("Error", "Failed to initialize GS (GSinit())."); + return false; + } + return true; } @@ -683,6 +696,12 @@ void VMManager::UpdateRunningGame(bool resetting, bool game_starting) GetMTGS().SendGameCRC(new_crc); Host::OnGameChanged(s_disc_path, s_game_serial, s_game_name, s_game_crc); + if (FullscreenUI::IsInitialized()) + { + GetMTGS().RunOnGSThread([disc_path = s_disc_path, game_serial = s_game_serial, game_name = s_game_name, game_crc = s_game_crc]() { + FullscreenUI::OnRunningGameChanged(std::move(disc_path), std::move(game_serial), std::move(game_name), game_crc); + }); + } #if 0 // TODO: Enable this when the debugger is added to Qt, and it's active. Otherwise, this is just a waste of time. @@ -846,8 +865,6 @@ bool VMManager::Initialize(VMBootParameters boot_params) Host::OnVMDestroyed(); }; - LoadSettings(); - std::string state_to_load; if (!ApplyBootParameters(std::move(boot_params), &state_to_load)) return false; @@ -870,14 +887,18 @@ bool VMManager::Initialize(VMBootParameters boot_params) ScopedGuard close_cdvd = [] { DoCDVDclose(); }; Console.WriteLn("Opening GS..."); - if (!GetMTGS().WaitForOpen()) + s_gs_open_on_initialize = GetMTGS().IsOpen(); + if (!s_gs_open_on_initialize && !GetMTGS().WaitForOpen()) { // we assume GS is going to report its own error Console.WriteLn("Failed to open GS."); return false; } - ScopedGuard close_gs = []() { GetMTGS().WaitForClose(); }; + ScopedGuard close_gs = []() { + if (!s_gs_open_on_initialize) + GetMTGS().WaitForClose(); + }; Console.WriteLn("Opening SPU2..."); if (SPU2init() != 0 || SPU2open() != 0) @@ -964,6 +985,7 @@ bool VMManager::Initialize(VMBootParameters boot_params) Console.WriteLn("VM subsystems initialized in %.2f ms", init_timer.GetTimeMilliseconds()); s_state.store(VMState::Paused, std::memory_order_release); Host::OnVMStarted(); + FullscreenUI::OnVMStarted(); UpdateRunningGame(true, false); @@ -1047,7 +1069,14 @@ void VMManager::Shutdown(bool save_resume_state) DoCDVDclose(); FWclose(); FileMcd_EmuClose(); - GetMTGS().WaitForClose(); + + // If the fullscreen UI is running, do a hardware reset on the GS + // so that the texture cache and targets are all cleared. + if (s_gs_open_on_initialize) + GetMTGS().ResetGS(true); + else + GetMTGS().WaitForClose(); + USBshutdown(); SPU2shutdown(); PADshutdown(); @@ -1058,6 +1087,7 @@ void VMManager::Shutdown(bool save_resume_state) s_state.store(VMState::Shutdown, std::memory_order_release); Host::OnVMDestroyed(); + FullscreenUI::OnVMDestroyed(); } void VMManager::Reset() @@ -1623,20 +1653,27 @@ void VMManager::CheckForMemoryCardConfigChanges(const Pcsx2Config& old_config) void VMManager::CheckForConfigChanges(const Pcsx2Config& old_config) { - CheckForCPUConfigChanges(old_config); - CheckForGSConfigChanges(old_config); - CheckForFramerateConfigChanges(old_config); - CheckForPatchConfigChanges(old_config); - CheckForSPU2ConfigChanges(old_config); - CheckForDEV9ConfigChanges(old_config); - CheckForMemoryCardConfigChanges(old_config); - - if (EmuConfig.EnableCheats != old_config.EnableCheats || - EmuConfig.EnableWideScreenPatches != old_config.EnableWideScreenPatches || - EmuConfig.EnableNoInterlacingPatches != old_config.EnableNoInterlacingPatches) + if (HasValidVM()) { - VMManager::ReloadPatches(true, true); + CheckForCPUConfigChanges(old_config); + CheckForFramerateConfigChanges(old_config); + CheckForPatchConfigChanges(old_config); + CheckForSPU2ConfigChanges(old_config); + CheckForDEV9ConfigChanges(old_config); + CheckForMemoryCardConfigChanges(old_config); + + if (EmuConfig.EnableCheats != old_config.EnableCheats || + EmuConfig.EnableWideScreenPatches != old_config.EnableWideScreenPatches || + EmuConfig.EnableNoInterlacingPatches != old_config.EnableNoInterlacingPatches) + { + VMManager::ReloadPatches(true, true); + } } + + // For the big picture UI, we still need to update GS settings, since it's running, + // and we don't update its config when we start the VM. + if (HasValidVM() || GetMTGS().IsOpen()) + CheckForGSConfigChanges(old_config); } void VMManager::ApplySettings() @@ -1654,9 +1691,7 @@ void VMManager::ApplySettings() const Pcsx2Config old_config(EmuConfig); LoadSettings(); - - if (HasValidVM()) - CheckForConfigChanges(old_config); + CheckForConfigChanges(old_config); } bool VMManager::ReloadGameSettings() @@ -1741,6 +1776,10 @@ static void HotkeySaveStateSlot(s32 slot) } BEGIN_HOTKEY_LIST(g_vm_manager_hotkeys) +DEFINE_HOTKEY("OpenPauseMenu", "System", "Open Pause Menu", [](s32 pressed) { + if (!pressed) + FullscreenUI::OpenPauseMenu(); +}) DEFINE_HOTKEY("TogglePause", "System", "Toggle Pause", [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) VMManager::SetPaused(VMManager::GetState() != VMState::Paused); @@ -1801,7 +1840,7 @@ DEFINE_HOTKEY("FrameAdvance", "System", "Frame Advance", [](s32 pressed) { }) DEFINE_HOTKEY("ShutdownVM", "System", "Shut Down Virtual Machine", [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) - Host::RequestVMShutdown(true, true); + Host::RequestVMShutdown(true, true, EmuConfig.SaveStateOnShutdown); }) DEFINE_HOTKEY("ResetVM", "System", "Reset Virtual Machine", [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) @@ -1858,7 +1897,6 @@ DEFINE_HOTKEY_SAVESTATE_X(10) DEFINE_HOTKEY_LOADSTATE_X(10) #undef DEFINE_HOTKEY_SAVESTATE_X #undef DEFINE_HOTKEY_LOADSTATE_X - END_HOTKEY_LIST() #ifdef _WIN32 diff --git a/pcsx2/VMManager.h b/pcsx2/VMManager.h index 0399f3666f..f002800967 100644 --- a/pcsx2/VMManager.h +++ b/pcsx2/VMManager.h @@ -76,6 +76,9 @@ namespace VMManager /// Returns the name of the disc/executable currently running. std::string GetGameName(); + /// Loads global settings (i.e. EmuConfig). + void LoadSettings(); + /// Initializes all system components. bool Initialize(VMBootParameters boot_params); @@ -249,7 +252,7 @@ namespace Host void RequestExit(bool save_state_if_running); /// Requests shut down of the current virtual machine. - void RequestVMShutdown(bool allow_confirm, bool allow_save_state); + void RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state); /// Returns true if the hosting application is currently fullscreen. bool IsFullscreen(); diff --git a/pcsx2/pcsx2core.vcxproj b/pcsx2/pcsx2core.vcxproj index 2adbb524e4..be8a425101 100644 --- a/pcsx2/pcsx2core.vcxproj +++ b/pcsx2/pcsx2core.vcxproj @@ -188,7 +188,9 @@ + + @@ -508,7 +510,9 @@ + + diff --git a/pcsx2/pcsx2core.vcxproj.filters b/pcsx2/pcsx2core.vcxproj.filters index 4e90e6ba82..bdbbb39d64 100644 --- a/pcsx2/pcsx2core.vcxproj.filters +++ b/pcsx2/pcsx2core.vcxproj.filters @@ -1275,6 +1275,12 @@ Host + + Host + + + Host + @@ -2119,6 +2125,12 @@ Host + + Host + + + Host + diff --git a/tools/generate_update_fa_glyph_ranges.py b/tools/generate_update_fa_glyph_ranges.py new file mode 100644 index 0000000000..98f887b0d2 --- /dev/null +++ b/tools/generate_update_fa_glyph_ranges.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +import sys +import os +import glob +import re +import functools + +# PCSX2 - PS2 Emulator for PCs +# Copyright (C) 2002-2022 PCSX2 Dev Team +# +# PCSX2 is free software: you can redistribute it and/or modify it under the terms +# of the GNU Lesser General Public License as published by the Free Software Found- +# ation, either version 3 of the License, or (at your option) any later version. +# +# PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with PCSX2. +# If not, see . + +# pylint: disable=bare-except, disable=missing-function-docstring + +src_dirs = [os.path.join(os.path.dirname(__file__), "..", "pcsx2"), os.path.join(os.path.dirname(__file__), "..", "pcsx2-qt")] +fa_file = os.path.join(os.path.dirname(__file__), "..", "3rdparty", "include", "IconsFontAwesome5.h") +dst_file = os.path.join(os.path.dirname(__file__), "..", "pcsx2", "Frontend", "ImguiManager.cpp") + +all_source_files = list(functools.reduce(lambda prev, src_dir: prev + glob.glob(os.path.join(src_dir, "**", "*.cpp"), recursive=True) + \ + glob.glob(os.path.join(src_dir, "**", "*.h"), recursive=True) + \ + glob.glob(os.path.join(src_dir, "**", "*.inl"), recursive=True), src_dirs, [])) + +tokens = set() +for filename in all_source_files: + data = None + with open(filename, "r") as f: + try: + data = f.read() + except: + continue + + tokens = tokens.union(set(re.findall("(ICON_FA_[a-zA-Z0-9_]+)", data))) + +print("{} tokens found.".format(len(tokens))) +if len(tokens) == 0: + sys.exit(0) + +u8_encodings = {} +with open(fa_file, "r") as f: + for line in f.readlines(): + match = re.match("#define (ICON_FA_[^ ]+) \"([^\"]+)\"", line) + if match is None: + continue + u8_encodings[match[1]] = bytes.fromhex(match[2].replace("\\x", "")) + +out_pattern = "(static constexpr ImWchar range_fa\[\] = \{)[0-9A-Z_a-z, \n]+(\};)" + +codepoints = list() +for token in tokens: + u8_bytes = u8_encodings[token] + u8 = str(u8_bytes, "utf-8") + u16 = u8.encode("utf-16le") + if len(u16) > 2: + raise ValueError("{} too long".format(u8_bytes)) + + codepoint = int.from_bytes(u16, byteorder="little", signed=False) + codepoints.append(codepoint) +codepoints.sort() +codepoints.append(0) # null terminator + +startc = codepoints[0] +endc = None +pairs = [startc] +for codepoint in codepoints: + if endc is not None and (endc + 1) != codepoint: + pairs.append(endc) + pairs.append(codepoint) + startc = codepoint + endc = codepoint + else: + endc = codepoint +pairs.append(endc) + +pairs_str = ",".join(map("0x{:x}".format, pairs)) + +with open(dst_file, "r") as f: + original = f.read() + updated = re.sub(out_pattern, "\\1 " + pairs_str + " \\2", original) + if original != updated: + with open(dst_file, "w") as f: + f.write(updated) + print("Updated {}".format(dst_file)) + else: + print("Skipping updating {}".format(dst_file)) +