From f2bc79352f7380def20ba149223d614d1088b2a7 Mon Sep 17 00:00:00 2001 From: moonpower Date: Fri, 3 Apr 2026 06:16:04 +0200 Subject: [PATCH] Integrate standalone FSUI into Panda SDL frontend --- .gitmodules | 3 + CMakeLists.txt | 66 +- include/config.hpp | 10 + include/emulator.hpp | 1 + include/frontend_settings.hpp | 2 + include/panda_sdl/imgui_layer.hpp | 23 +- include/panda_sdl/panda_fsui.hpp | 92 ++ src/config.cpp | 66 +- src/frontend_settings.cpp | 16 +- src/panda_sdl/frontend_sdl.cpp | 19 +- src/panda_sdl/imgui_layer.cpp | 1100 +++++++++++++--------- src/panda_sdl/panda_fsui.cpp | 1418 +++++++++++++++++++++++++++++ third_party/discord-rpc | 2 +- third_party/fsui | 1 + 14 files changed, 2348 insertions(+), 471 deletions(-) create mode 100644 include/panda_sdl/panda_fsui.hpp create mode 100644 src/panda_sdl/panda_fsui.cpp create mode 160000 third_party/fsui diff --git a/.gitmodules b/.gitmodules index e419210f..344d5523 100644 --- a/.gitmodules +++ b/.gitmodules @@ -80,3 +80,6 @@ [submodule "third_party/xbyak"] path = third_party/xbyak url = https://github.com/Panda3DS-emu/xbyak +[submodule "third_party/fsui"] + path = third_party/fsui + url = https://git.nanodata.cloud/moonpower/fsui-lib.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4fa8a1a1..47b1fc32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,7 +176,6 @@ include_directories(${FMT_INCLUDE_DIR}) include_directories(third_party/boost/) include_directories(third_party/elfio/) include_directories(third_party/hips/include/) -include_directories(third_party/imgui/) include_directories(third_party/dynarmic/src) include_directories(third_party/cityhash/include) include_directories(third_party/result/include) @@ -186,6 +185,13 @@ include_directories(third_party/stb) include_directories(third_party/opengl) include_directories(third_party/miniaudio) include_directories(third_party/mio/single_include) +if(IMGUI_FRONTEND) + include_directories(third_party/fsui/include) + include_directories(third_party/fsui/third_party/imgui) + include_directories(third_party/fsui/third_party/imgui/backends) +else() + include_directories(third_party/imgui/) +endif() add_compile_definitions(NOMINMAX) # Make windows.h not define min/max macros because third-party deps don't like it add_compile_definitions(WIN32_LEAN_AND_MEAN) # Make windows.h not include literally everything @@ -202,20 +208,35 @@ endif() if (NOT ANDROID) if (UWP_BUILD) + if (NOT TARGET SDL2::SDL2) + add_library(SDL2::SDL2 SHARED IMPORTED GLOBAL) + set_target_properties(SDL2::SDL2 PROPERTIES + IMPORTED_IMPLIB "${MINGW_UWP_DIR}/deps/lib/SDL2.lib" + INTERFACE_INCLUDE_DIRECTORIES "${MINGW_UWP_DIR}/deps/include;${MINGW_UWP_DIR}/deps/include/SDL2" + ) + endif() target_include_directories(AlberCore PUBLIC "${MINGW_UWP_DIR}/deps/include" "${MINGW_UWP_DIR}/deps/include/SDL2" ) - target_link_libraries(AlberCore PUBLIC "${MINGW_UWP_DIR}/deps/lib/SDL2.lib") + target_link_libraries(AlberCore PUBLIC SDL2::SDL2) elseif (USE_SYSTEM_SDL2) find_package(SDL2 CONFIG REQUIRED) target_link_libraries(AlberCore PUBLIC SDL2::SDL2) else() set(SDL_STATIC ON CACHE BOOL "" FORCE) - set(SDL_SHARED OFF CACHE BOOL "" FORCE) + if(IMGUI_FRONTEND) + set(SDL_SHARED ON CACHE BOOL "" FORCE) + else() + set(SDL_SHARED OFF CACHE BOOL "" FORCE) + endif() set(SDL_TEST OFF CACHE BOOL "" FORCE) add_subdirectory(third_party/SDL2) - target_link_libraries(AlberCore PUBLIC SDL2-static) + if(IMGUI_FRONTEND) + target_link_libraries(AlberCore PUBLIC SDL2::SDL2) + else() + target_link_libraries(AlberCore PUBLIC SDL2-static) + endif() endif() endif() @@ -228,6 +249,16 @@ include_directories(third_party/duckstation) include_directories(third_party/host_memory/include) add_subdirectory(third_party/cmrc) +add_subdirectory(third_party/glad) +if(IMGUI_FRONTEND) + set(FSUI_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) + set(FSUI_ENABLE_INSTALL OFF CACHE BOOL "" FORCE) + set(FSUI_PLATFORM_BACKEND SDL2 CACHE STRING "" FORCE) + add_subdirectory(third_party/fsui) + if(TARGET fsui-renderer-opengl) + target_compile_definitions(fsui-renderer-opengl PRIVATE FSUI_USE_LEGACY_GLAD=1) + endif() +endif() set(BOOST_ROOT "${CMAKE_SOURCE_DIR}/third_party/boost") set(Boost_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/boost") @@ -243,8 +274,6 @@ if(ANDROID) target_link_libraries(AlberCore PRIVATE EGL log) endif() -add_subdirectory(third_party/glad) - # Cryptopp doesn't support compiling under clang-cl, so we have to include it as a prebuilt MSVC static library if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND MSVC) add_subdirectory(third_party/cryptoppwin) @@ -500,18 +529,21 @@ cmrc_add_resource_library( "src/core/services/fonts/SharedFontReplacement.bin" ) -set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp - third_party/imgui/imgui_draw.cpp - third_party/imgui/imgui_tables.cpp - third_party/imgui/imgui_widgets.cpp - third_party/imgui/imgui_demo.cpp - - third_party/cityhash/cityhash.cpp +set(THIRD_PARTY_SOURCE_FILES third_party/cityhash/cityhash.cpp third_party/xxhash/xxhash.c third_party/host_memory/host_memory.cpp third_party/host_memory/virtual_buffer.cpp ) +if(NOT IMGUI_FRONTEND) + list(APPEND THIRD_PARTY_SOURCE_FILES + third_party/imgui/imgui.cpp + third_party/imgui/imgui_draw.cpp + third_party/imgui/imgui_tables.cpp + third_party/imgui/imgui_widgets.cpp + third_party/imgui/imgui_demo.cpp + ) +endif() if(ENABLE_LUAJIT AND NOT ANDROID) # Build luv and libuv for Lua TCP server usage if we're not on Android @@ -900,11 +932,11 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) if(IMGUI_FRONTEND) list(APPEND FRONTEND_SOURCE_FILES src/panda_sdl/imgui_layer.cpp - third_party/imgui/backends/imgui_impl_sdl.cpp - third_party/imgui/backends/imgui_impl_opengl3.cpp + src/panda_sdl/panda_fsui.cpp ) list(APPEND FRONTEND_HEADER_FILES "include/panda_sdl/imgui_layer.hpp" + "include/panda_sdl/panda_fsui.hpp" ) if(WIN32) list(APPEND FRONTEND_LIBRARIES imm32) @@ -916,6 +948,10 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) if(FRONTEND_LIBRARIES) target_link_libraries(Alber PRIVATE ${FRONTEND_LIBRARIES}) endif() + if(IMGUI_FRONTEND) + target_link_libraries(Alber PRIVATE FSUI::backend-sdl FSUI::donor) + target_compile_definitions(Alber PRIVATE PANDA3DS_FSUI_ICON_DIR="${PROJECT_SOURCE_DIR}/docs/img") + endif() target_sources(Alber PRIVATE ${FRONTEND_SOURCE_FILES} ${FRONTEND_HEADER_FILES} ${GL_CONTEXT_SOURCE_FILES} ${APP_RESOURCES}) elseif(BUILD_HYDRA_CORE) target_compile_definitions(AlberCore PRIVATE PANDA3DS_HYDRA_CORE=1) diff --git a/include/config.hpp b/include/config.hpp index ba53dcdc..4b1adafd 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include "audio/dsp_core.hpp" #include "frontend_settings.hpp" @@ -110,6 +111,15 @@ struct EmulatorConfig { static constexpr size_t maxRecentGames = 8; std::vector recentlyPlayed; + std::vector fsuiGameListPaths; + std::vector fsuiGameListRecursivePaths; + std::filesystem::path fsuiCoversPath = ""; + int fsuiDefaultGameView = 0; + int fsuiGameSort = 0; + bool fsuiGameSortReverse = false; + std::string fsuiTheme = "Dark"; + std::string fsuiPromptIconPack = "Auto"; + std::filesystem::path fsuiBackgroundImagePath = ""; // Frontend window settings struct WindowSettings { diff --git a/include/emulator.hpp b/include/emulator.hpp index b1191f6e..b72dc933 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -126,6 +126,7 @@ class Emulator { ServiceManager& getServiceManager() { return kernel.getServiceManager(); } LuaManager& getLua() { return lua; } AudioDeviceInterface& getAudioDevice() { return audioDevice; } + const std::optional& getROMPath() const { return romPath; } RendererType getRendererType() const { return config.rendererType; } Renderer* getRenderer() { return gpu.getRenderer(); } diff --git a/include/frontend_settings.hpp b/include/frontend_settings.hpp index 2cbe0475..b5214878 100644 --- a/include/frontend_settings.hpp +++ b/include/frontend_settings.hpp @@ -28,6 +28,7 @@ struct FrontendSettings { WindowIcon icon = WindowIcon::Rpog; std::string language = "en"; bool showImGuiDebugPanel = true; + bool enableFullscreenUI = false; #ifdef IMGUI_FRONTEND bool stretchImGuiOutputToWindow = true; #else @@ -39,4 +40,5 @@ struct FrontendSettings { static WindowIcon iconFromString(std::string inString); static const char* iconToString(WindowIcon icon); + static bool defaultFullscreenUIEnabled(); }; diff --git a/include/panda_sdl/imgui_layer.hpp b/include/panda_sdl/imgui_layer.hpp index f17df63c..75f746d8 100644 --- a/include/panda_sdl/imgui_layer.hpp +++ b/include/panda_sdl/imgui_layer.hpp @@ -4,10 +4,17 @@ #include +#include #include +#include #include #include "emulator.hpp" +#include "fsui/backend_sdl.hpp" +#include "fsui/fsui.hpp" +#include "panda_sdl/panda_fsui.hpp" + +struct ImFont; class ImGuiLayer { public: @@ -30,13 +37,24 @@ class ImGuiLayer { void showPauseMenuFromController(); private: + bool useFullscreenUI(); void drawDebugPanel(); - void drawPausePanel(); - void drawSettingsPanel(); + void drawClassicPausePanel(); + void drawClassicSettingsPanel(); + void drawSettingsGeneralSection(bool& reloadSettings); + void drawSettingsWindowSection(bool& reloadSettings); + void drawSettingsUISection(bool& reloadSettings); + void drawSettingsGraphicsSection(bool& reloadSettings); + void drawSettingsAudioSection(bool& reloadSettings); + void drawSettingsBatterySection(bool& reloadSettings); + void drawSettingsSDSection(bool& reloadSettings); SDL_Window* window = nullptr; SDL_GLContext glContext = nullptr; Emulator& emu; + fsui::FontStack fontStack; + fsui::SdlImGuiBackend imguiBackend; + PandaFsuiAdapter fsuiAdapter; bool showDebug = true; bool showPauseMenu = false; @@ -44,6 +62,7 @@ class ImGuiLayer { bool isPaused = false; bool captureKeyboard = false; bool captureMouse = false; + bool fullscreenSelectorMode = false; std::function onPauseChange; std::function onVsyncChange; diff --git a/include/panda_sdl/panda_fsui.hpp b/include/panda_sdl/panda_fsui.hpp new file mode 100644 index 00000000..4139b8c8 --- /dev/null +++ b/include/panda_sdl/panda_fsui.hpp @@ -0,0 +1,92 @@ +#pragma once + +#ifdef IMGUI_FRONTEND + +#include + +#include +#include +#include +#include + +#include "fsui/fsui.hpp" + +class Emulator; + +class PandaFsuiAdapter { + public: + enum class ClassicUiRequest { + None, + Return, + OpenSettings, + }; + + PandaFsuiAdapter(SDL_Window* window, Emulator& emu); + + bool initialize(const fsui::FontStack& fonts); + void shutdown(bool clear_state); + void render(); + void consumeCommands(); + + void setSelectorMode(bool selector_mode); + bool isSelectorMode() const; + bool hasActiveWindow() const; + void openPauseMenu(); + void returnToPreviousWindow(); + void returnToMainWindow(); + void switchToSettings(); + + std::optional consumeLaunchPath(); + bool consumeCloseSelector(); + ClassicUiRequest consumeClassicUiRequest(); + + void setPauseCallback(std::function callback); + void setVsyncCallback(std::function callback); + void setExitToSelectorCallback(std::function callback); + + private: + struct ParsedMetadata; + struct LanguageOption; + + void syncUiStateFromConfig(); + void persistUiState(bool reload); + void seedGameListPathsIfNeeded(); + std::vector currentScanRoots() const; + bool hasSupportedExtension(const std::filesystem::path& path) const; + std::vector buildGameList(); + std::vector buildSettingsPages(fsui::SettingsScope scope); + fsui::CurrentGameInfo buildCurrentGameInfo(); + std::vector buildLandingItems(); + std::vector buildStartItems(); + std::vector buildExitItems(); + std::vector buildPauseItems(); + std::vector buildGameLaunchOptions(const fsui::GameEntry& entry); + + std::optional readMetadataForPath(const std::filesystem::path& path) const; + fsui::GameEntry buildGameListEntry(const std::filesystem::directory_entry& entry) const; + std::filesystem::path defaultCoverDirectory() const; + std::filesystem::path findCoverPath(const std::filesystem::path& rom_path, const std::string& title_id) const; + std::string currentGameTitle() const; + std::string currentGameSubtitle() const; + std::string formatPercent(float value) const; + std::string formatInteger(int value) const; + std::string formatTitleId(std::uint64_t program_id) const; + void openFileAndLaunch(); + void requestLaunchPath(const std::filesystem::path& path); + void requestClassicUi(bool open_settings); + void openUnsupportedPrompt(std::string title, std::string message) const; + + SDL_Window* window = nullptr; + Emulator& emu; + fsui::UiContext fsuiContext; + fsui::UiState uiState; + std::vector cachedGameList; + std::optional pendingLaunchPath; + bool closeSelectorRequested = false; + ClassicUiRequest pendingClassicUiRequest = ClassicUiRequest::None; + std::function onPauseChange; + std::function onVsyncChange; + std::function onExitToSelector; +}; + +#endif diff --git a/src/config.cpp b/src/config.cpp index 797f19d3..77e8be5c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -15,7 +15,10 @@ // We are legally allowed, as per the author's wish, to use the above code without any licensing restrictions // However we still want to follow the license as closely as possible and offer the proper attributions. -EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) : filePath(path) { load(); } +EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) : filePath(path) { + frontendSettings.enableFullscreenUI = FrontendSettings::defaultFullscreenUIEnabled(); + load(); +} void EmulatorConfig::load() { const std::filesystem::path& path = filePath; @@ -70,6 +73,31 @@ void EmulatorConfig::load() { } } + if (data.contains("GameList")) { + auto gameListResult = toml::expect(data.at("GameList")); + if (gameListResult.is_ok()) { + auto gameList = gameListResult.unwrap(); + + fsuiGameListPaths.clear(); + if (gameList.contains("Paths") && gameList.at("Paths").is_array()) { + for (const auto& item : gameList.at("Paths").as_array()) { + if (item.is_string()) { + fsuiGameListPaths.emplace_back(toml::get(item)); + } + } + } + + fsuiGameListRecursivePaths.clear(); + if (gameList.contains("RecursivePaths") && gameList.at("RecursivePaths").is_array()) { + for (const auto& item : gameList.at("RecursivePaths").as_array()) { + if (item.is_string()) { + fsuiGameListRecursivePaths.emplace_back(toml::get(item)); + } + } + } + } + } + if (data.contains("Window")) { auto windowResult = toml::expect(data.at("Window")); if (windowResult.is_ok()) { @@ -170,6 +198,14 @@ void EmulatorConfig::load() { frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or(ui, "WindowIcon", "rpog")); frontendSettings.language = toml::find_or(ui, "Language", "en"); frontendSettings.showImGuiDebugPanel = toml::find_or(ui, "ShowImGuiDebugPanel", true); + frontendSettings.enableFullscreenUI = + toml::find_or(ui, "EnableFullscreenUI", FrontendSettings::defaultFullscreenUIEnabled()); + fsuiTheme = toml::find_or(ui, "FullscreenUITheme", "Dark"); + fsuiPromptIconPack = toml::find_or(ui, "FullscreenUIPromptIcons", "Auto"); + fsuiBackgroundImagePath = toml::find_or(ui, "FullscreenUIBackgroundImage", ""); + fsuiDefaultGameView = static_cast(toml::find_or(ui, "DefaultFullscreenUIGameView", 0)); + fsuiGameSort = static_cast(toml::find_or(ui, "FullscreenUIGameSort", 0)); + fsuiGameSortReverse = toml::find_or(ui, "FullscreenUIGameSortReverse", false); #ifdef IMGUI_FRONTEND frontendSettings.stretchImGuiOutputToWindow = toml::find_or(ui, "StretchImGuiOutputToWindow", true); #else @@ -177,6 +213,14 @@ void EmulatorConfig::load() { #endif } } + + if (data.contains("Folders")) { + auto foldersResult = toml::expect(data.at("Folders")); + if (foldersResult.is_ok()) { + auto folders = foldersResult.unwrap(); + fsuiCoversPath = toml::find_or(folders, "Covers", ""); + } + } } void EmulatorConfig::save() { @@ -212,6 +256,18 @@ void EmulatorConfig::save() { } data["General"]["RecentGames"] = recentsArray; + toml::array pathsArray; + for (const auto& path : fsuiGameListPaths) { + pathsArray.push_back(path.string()); + } + data["GameList"]["Paths"] = pathsArray; + + toml::array recursivePathsArray; + for (const auto& path : fsuiGameListRecursivePaths) { + recursivePathsArray.push_back(path.string()); + } + data["GameList"]["RecursivePaths"] = recursivePathsArray; + data["Window"]["AppVersionOnWindow"] = windowSettings.showAppVersion; data["Window"]["RememberWindowPosition"] = windowSettings.rememberPosition; data["Window"]["WindowPosX"] = windowSettings.x; @@ -250,7 +306,15 @@ void EmulatorConfig::save() { data["UI"]["WindowIcon"] = std::string(FrontendSettings::iconToString(frontendSettings.icon)); data["UI"]["Language"] = frontendSettings.language; data["UI"]["ShowImGuiDebugPanel"] = frontendSettings.showImGuiDebugPanel; + data["UI"]["EnableFullscreenUI"] = frontendSettings.enableFullscreenUI; + data["UI"]["FullscreenUITheme"] = fsuiTheme; + data["UI"]["FullscreenUIPromptIcons"] = fsuiPromptIconPack; + data["UI"]["FullscreenUIBackgroundImage"] = fsuiBackgroundImagePath.string(); + data["UI"]["DefaultFullscreenUIGameView"] = fsuiDefaultGameView; + data["UI"]["FullscreenUIGameSort"] = fsuiGameSort; + data["UI"]["FullscreenUIGameSortReverse"] = fsuiGameSortReverse; data["UI"]["StretchImGuiOutputToWindow"] = frontendSettings.stretchImGuiOutputToWindow; + data["Folders"]["Covers"] = fsuiCoversPath.string(); std::ofstream file(path, std::ios::out); file << data; diff --git a/src/frontend_settings.cpp b/src/frontend_settings.cpp index 2d1d37c6..f9f8192b 100644 --- a/src/frontend_settings.cpp +++ b/src/frontend_settings.cpp @@ -4,6 +4,10 @@ #include #include +#if defined(__WINRT__) && !defined(__ANDROID__) +#include +#endif + // Frontend setting serialization/deserialization functions FrontendSettings::Theme FrontendSettings::themeFromString(std::string inString) { @@ -62,4 +66,14 @@ const char* FrontendSettings::iconToString(WindowIcon icon) { case WindowIcon::Rpog: default: return "rpog"; } -} \ No newline at end of file +} + +bool FrontendSettings::defaultFullscreenUIEnabled() { + #if defined(__WINRT__) && !defined(__ANDROID__) + return SDL_WinRTGetDeviceFamily() == SDL_WINRT_DEVICEFAMILY_XBOX; + #elif defined(__XBOXONE__) || defined(__XBOXSERIES__) + return true; + #else + return false; + #endif +} diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 7e970e9d..f9832475 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -298,7 +298,20 @@ void FrontendSDL::initialize(SDL_Window* existingWindow, SDL_GLContext existingC #endif } -bool FrontendSDL::loadROM(const std::filesystem::path& path) { return emu.loadROM(path); } +bool FrontendSDL::loadROM(const std::filesystem::path& path) { + const bool loaded = emu.loadROM(path); + if (loaded) { + emuPaused = false; + #ifdef IMGUI_FRONTEND + if (imgui) { + imgui->setPaused(false); + } + #endif + emu.getConfig().addToRecentGames(path); + emu.getConfig().save(); + } + return loaded; +} std::optional FrontendSDL::selectGame() { #ifdef IMGUI_FRONTEND @@ -717,7 +730,9 @@ void FrontendSDL::run() { // TODO: Should this be uncommented? // kernel.evalReschedule(); + #ifndef IMGUI_FRONTEND SDL_GL_SwapWindow(window); + #endif } #ifdef IMGUI_FRONTEND @@ -800,4 +815,4 @@ void FrontendSDL::togglePaused() { setPaused(!emuPaused); } -FrontendSDL::~FrontendSDL() = default; \ No newline at end of file +FrontendSDL::~FrontendSDL() = default; diff --git a/src/panda_sdl/imgui_layer.cpp b/src/panda_sdl/imgui_layer.cpp index 4357e601..e5e44365 100644 --- a/src/panda_sdl/imgui_layer.cpp +++ b/src/panda_sdl/imgui_layer.cpp @@ -5,141 +5,174 @@ #include #include +#include #include -#include -#include +#include +#include +#include +#include +#include "helpers.hpp" #include "imgui.h" -#include "backends/imgui_impl_opengl3.h" -#include "backends/imgui_impl_sdl.h" #include "version.hpp" -namespace { -constexpr int kDebugPadding = 10; +namespace +{ + static const char* const s_fsui_theme_labels[] = { + "Dark", + "Light", + }; -struct InstalledGame { - std::string title; - std::string id; - std::filesystem::path path; -}; + static const char* const s_fsui_theme_values[] = { + "Dark", + "Light", + }; -std::vector scanGamesInDirectory(const std::filesystem::path& dir) { - std::vector games; - if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) return games; - for (const auto& entry : std::filesystem::directory_iterator(dir)) { - if (entry.is_regular_file()) { - auto ext = entry.path().extension().string(); + struct InstalledGame + { + std::string title; + std::string id; + std::filesystem::path path; + }; + + std::vector scanGamesInDirectory(const std::filesystem::path& dir) + { + std::vector games; + std::error_code ec; + if (!std::filesystem::exists(dir, ec) || !std::filesystem::is_directory(dir, ec)) { + return games; + } + + for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) { + if (ec || !entry.is_regular_file()) { + continue; + } + + std::string ext = entry.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (ext == ".cci" || ext == ".3ds" || ext == ".cxi" || ext == ".app" || ext == ".ncch" || ext == ".elf" || ext == ".axf" || ext == ".3dsx") { - InstalledGame game; - game.title = entry.path().stem().string(); - game.id = entry.path().filename().string(); - game.path = entry.path(); - games.push_back(game); + games.push_back({ + .title = entry.path().stem().string(), + .id = entry.path().filename().string(), + .path = entry.path(), + }); } } + return games; } - return games; -} -std::vector scanAllGames() { - std::vector allGames; - std::filesystem::path eRoot("E:/"); + std::vector scanAllGames() { - auto games = scanGamesInDirectory(eRoot); - allGames.insert(allGames.end(), games.begin(), games.end()); - } - { - std::filesystem::path ePanda = eRoot / "PANDA3DS"; - auto games = scanGamesInDirectory(ePanda); - allGames.insert(allGames.end(), games.begin(), games.end()); - } - std::filesystem::path rootPath; -#ifdef __WINRT__ - { - char* prefPath = SDL_GetPrefPath(nullptr, nullptr); - if (prefPath) { - rootPath = std::filesystem::path(prefPath); - SDL_free(prefPath); + std::vector all_games; + auto append_games = [&](const std::filesystem::path& path) { + auto games = scanGamesInDirectory(path); + all_games.insert(all_games.end(), games.begin(), games.end()); + }; + + append_games("E:/"); + append_games(std::filesystem::path("E:/") / "PANDA3DS"); + + std::filesystem::path root_path; + #ifdef __WINRT__ + char* base_path = SDL_GetPrefPath(nullptr, nullptr); + #else + char* base_path = SDL_GetBasePath(); + #endif + if (base_path) { + root_path = std::filesystem::path(base_path); + SDL_free(base_path); } - } -#else - { - char* basePath = SDL_GetBasePath(); - if (basePath) { - rootPath = std::filesystem::path(basePath); - SDL_free(basePath); + + if (!root_path.empty()) { + append_games(root_path); + append_games(root_path / "PANDA3DS"); } - } -#endif - if (!rootPath.empty()) { - auto games = scanGamesInDirectory(rootPath); - allGames.insert(allGames.end(), games.begin(), games.end()); - std::filesystem::path pandaPath = rootPath / "PANDA3DS"; - auto games2 = scanGamesInDirectory(pandaPath); - allGames.insert(allGames.end(), games2.begin(), games2.end()); - } - return allGames; -} -std::string primaryScanRootLabel() { -#ifdef __WINRT__ - char* prefPath = SDL_GetPrefPath(nullptr, nullptr); - if (!prefPath) return "[SDL Pref Path]"; - std::string out(prefPath); - SDL_free(prefPath); - return out; -#else - char* basePath = SDL_GetBasePath(); - if (!basePath) return "[SDL Base Path]"; - std::string out(basePath); - SDL_free(basePath); - return out; -#endif -} + std::sort(all_games.begin(), all_games.end(), [](const InstalledGame& lhs, const InstalledGame& rhs) { + if (lhs.title != rhs.title) { + return lhs.title < rhs.title; + } + return lhs.path < rhs.path; + }); -struct VersionInfo { - std::string appVersion; - std::string gitRevision; -}; - -bool isHexRevision(const std::string& value) { - if (value.size() != 7) { - return false; + all_games.erase( + std::unique(all_games.begin(), all_games.end(), [](const InstalledGame& lhs, const InstalledGame& rhs) { + return lhs.path == rhs.path; + }), + all_games.end() + ); + return all_games; } - for (char c : value) { - if (!std::isxdigit(static_cast(c))) { + + std::string primaryScanRootLabel() + { + #ifdef __WINRT__ + char* base_path = SDL_GetPrefPath(nullptr, nullptr); + #else + char* base_path = SDL_GetBasePath(); + #endif + if (!base_path) { + return "[SDL Path]"; + } + std::string out(base_path); + SDL_free(base_path); + return out; + } + + struct VersionInfo + { + std::string app_version; + std::string git_revision; + }; + + bool isHexRevision(const std::string& value) + { + if (value.size() != 7) { return false; } - } - return true; -} - -VersionInfo splitVersionString(const char* versionString) { - VersionInfo info{versionString ? versionString : "", ""}; - if (info.appVersion.empty()) { - info.gitRevision = "unknown"; - return info; + for (char c : value) { + if (!std::isxdigit(static_cast(c))) { + return false; + } + } + return true; } - const auto dot = info.appVersion.find_last_of('.'); - if (dot != std::string::npos) { - const std::string maybeRev = info.appVersion.substr(dot + 1); - if (isHexRevision(maybeRev)) { - info.gitRevision = maybeRev; - info.appVersion = info.appVersion.substr(0, dot); + VersionInfo splitVersionString(const char* version_string) + { + VersionInfo info{version_string ? version_string : "", ""}; + if (info.app_version.empty()) { + info.git_revision = "unknown"; return info; } + + const auto dot = info.app_version.find_last_of('.'); + if (dot != std::string::npos) { + const std::string maybe_rev = info.app_version.substr(dot + 1); + if (isHexRevision(maybe_rev)) { + info.git_revision = maybe_rev; + info.app_version = info.app_version.substr(0, dot); + return info; + } + } + + info.git_revision = "embedded"; + return info; } +} // namespace - info.gitRevision = "embedded"; - return info; -} +ImGuiLayer::ImGuiLayer(SDL_Window* window, SDL_GLContext context, Emulator& emu) + : window(window), glContext(context), emu(emu), fsuiAdapter(window, emu) +{} + +bool ImGuiLayer::useFullscreenUI() +{ + return emu.getConfig().frontendSettings.enableFullscreenUI; } -ImGuiLayer::ImGuiLayer(SDL_Window* window, SDL_GLContext context, Emulator& emu) : window(window), glContext(context), emu(emu) {} - -void ImGuiLayer::init() { +void ImGuiLayer::init() +{ IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); @@ -148,30 +181,55 @@ void ImGuiLayer::init() { io.IniFilename = nullptr; showDebug = emu.getConfig().frontendSettings.showImGuiDebugPanel; - const bool sdlInitOk = ImGui_ImplSDL2_InitForOpenGL(window, glContext); - const bool glInitOk = ImGui_ImplOpenGL3_Init("#version 410"); - #ifdef IMGUI_FRONTEND_DEBUG - printf("[IMGUI] ImGui init: SDL2=%s OpenGL3=%s window=%p context=%p\n", sdlInitOk ? "ok" : "fail", - glInitOk ? "ok" : "fail", window, glContext); - if (!sdlInitOk || !glInitOk) { - printf("[IMGUI] ImGui backend init failed\n"); + fsui::SdlImGuiBackendConfig backend_config; + backend_config.window = window; + backend_config.gl_context = glContext; + backend_config.renderer_backend = fsui::RendererBackend::OpenGL; + backend_config.glsl_version = "#version 410 core"; + if (!imguiBackend.initialize(backend_config)) { + Helpers::panic("Failed to initialize the shared SDL ImGui backend"); } - #endif + + if (!fsui::BuildDefaultFontStack(fsui::DescribeSdl2FontBootstrap(window), fontStack)) { + Helpers::panic("Failed to load bundled fullscreen UI fonts"); + } + fsuiAdapter.setPauseCallback([this](bool paused) { + isPaused = paused; + showPauseMenu = paused; + if (onPauseChange) { + onPauseChange(paused); + } + }); + fsuiAdapter.setVsyncCallback([this](bool enabled) { + if (onVsyncChange) { + onVsyncChange(enabled); + } + }); + fsuiAdapter.setExitToSelectorCallback([this]() { + showPauseMenu = false; + showSettings = false; + if (onExitToSelector) { + onExitToSelector(); + } + }); + fsuiAdapter.initialize(fontStack); } -void ImGuiLayer::shutdown() { - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplSDL2_Shutdown(); +void ImGuiLayer::shutdown() +{ + fsuiAdapter.shutdown(true); + imguiBackend.shutdown(); ImGui::DestroyContext(); } -void ImGuiLayer::processEvent(const SDL_Event& event) { - ImGui_ImplSDL2_ProcessEvent(&event); +void ImGuiLayer::processEvent(const SDL_Event& event) +{ + imguiBackend.processEvent(event); } -void ImGuiLayer::beginFrame() { - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplSDL2_NewFrame(window); +void ImGuiLayer::beginFrame() +{ + imguiBackend.newFrame(); ImGui::NewFrame(); ImGuiIO& io = ImGui::GetIO(); @@ -179,64 +237,89 @@ void ImGuiLayer::beginFrame() { captureMouse = io.WantCaptureMouse; } -void ImGuiLayer::render() { - int drawableW = 0; - int drawableH = 0; - SDL_GL_GetDrawableSize(window, &drawableW, &drawableH); - if (drawableW == 0 || drawableH == 0) { - #ifdef IMGUI_FRONTEND_DEBUG - static bool warnedOnce = false; - if (!warnedOnce) { - warnedOnce = true; - printf("[IMGUI] render: drawable size is %dx%d\n", drawableW, drawableH); - } - #endif - } else { - glViewport(0, 0, drawableW, drawableH); +void ImGuiLayer::render() +{ + int drawable_w = 0; + int drawable_h = 0; + SDL_GL_GetDrawableSize(window, &drawable_w, &drawable_h); + if (drawable_w > 0 && drawable_h > 0) { + glViewport(0, 0, drawable_w, drawable_h); + } + + const auto classic_request = fsuiAdapter.consumeClassicUiRequest(); + if (classic_request != PandaFsuiAdapter::ClassicUiRequest::None) { + fullscreenSelectorMode = false; + showPauseMenu = (emu.romType != ROMType::None); + showSettings = (classic_request == PandaFsuiAdapter::ClassicUiRequest::OpenSettings); } drawDebugPanel(); - drawPausePanel(); - drawSettingsPanel(); + if (useFullscreenUI()) { + fsuiAdapter.setSelectorMode(false); + fsuiAdapter.render(); + fsuiAdapter.consumeCommands(); + } else { + drawClassicPausePanel(); + drawClassicSettingsPanel(); + } ImGui::Render(); glBindFramebuffer(GL_FRAMEBUFFER, 0); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - #ifdef IMGUI_FRONTEND_DEBUG - GLenum err = glGetError(); - if (err != GL_NO_ERROR) { - printf("[IMGUI] render: glGetError=0x%X\n", err); - } - #endif + imguiBackend.renderDrawData(); } -void ImGuiLayer::handleHotkey(const SDL_Event& event) { +void ImGuiLayer::handleHotkey(const SDL_Event& event) +{ if (event.type != SDL_KEYDOWN) { return; } - switch (event.key.keysym.sym) { - case SDLK_F1: - showDebug = !showDebug; - emu.getConfig().frontendSettings.showImGuiDebugPanel = showDebug; - break; - case SDLK_F2: - showSettings = !showSettings; - break; - case SDLK_F3: - case SDLK_ESCAPE: - showPauseMenu = !showPauseMenu; - if (onPauseChange) { - isPaused = showPauseMenu; - onPauseChange(isPaused); - } - break; - default: - break; + if (!useFullscreenUI()) { + switch (event.key.keysym.sym) { + case SDLK_F1: + showDebug = !showDebug; + emu.getConfig().frontendSettings.showImGuiDebugPanel = showDebug; + break; + case SDLK_F2: showSettings = !showSettings; break; + case SDLK_F3: + case SDLK_ESCAPE: + showPauseMenu = !showPauseMenu; + if (onPauseChange) { + isPaused = showPauseMenu; + onPauseChange(isPaused); + } + break; + default: break; + } + return; + } + + if (event.key.keysym.sym == SDLK_F2 && !fullscreenSelectorMode) { + if (!fsuiAdapter.hasActiveWindow()) { + fsuiAdapter.openPauseMenu(); + } + fsuiAdapter.switchToSettings(); + return; + } + + if ((event.key.keysym.sym == SDLK_F3 || event.key.keysym.sym == SDLK_ESCAPE) && !fullscreenSelectorMode) { + if (!fsuiAdapter.hasActiveWindow()) { + fsuiAdapter.openPauseMenu(); + } else { + fsuiAdapter.returnToPreviousWindow(); + } } } -void ImGuiLayer::showPauseMenuFromController() { +void ImGuiLayer::showPauseMenuFromController() +{ + if (useFullscreenUI()) { + if (!fullscreenSelectorMode) { + fsuiAdapter.openPauseMenu(); + } + return; + } + if (!showPauseMenu) { showPauseMenu = true; if (onPauseChange) { @@ -246,406 +329,525 @@ void ImGuiLayer::showPauseMenuFromController() { } } -std::optional ImGuiLayer::runGameSelector() { - std::vector games = scanAllGames(); - bool showNoRom = games.empty(); - bool inSettings = false; - int selected = 0; - bool selectionMade = false; - std::optional selectedPath; +std::optional ImGuiLayer::runGameSelector() +{ + showPauseMenu = false; + showSettings = false; + fullscreenSelectorMode = useFullscreenUI(); + std::optional selected_path; + + if (fullscreenSelectorMode) { + fsuiAdapter.setSelectorMode(true); + } else { + fsuiAdapter.setSelectorMode(false); + } + + std::vector games = scanAllGames(); + int selected = 0; + + while (!selected_path.has_value()) { + if (!fullscreenSelectorMode && !games.empty()) { + selected = std::clamp(selected, 0, static_cast(games.size()) - 1); + } - while (!selectionMade) { SDL_Event event; while (SDL_PollEvent(&event)) { - ImGui_ImplSDL2_ProcessEvent(&event); + processEvent(event); if (event.type == SDL_QUIT) { + fsuiAdapter.setSelectorMode(false); + fullscreenSelectorMode = false; return std::nullopt; } - if (!inSettings && event.type == SDL_CONTROLLERBUTTONDOWN) { - if (event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_UP && selected > 0) selected--; - if (event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN && selected < (int)games.size() - 1) selected++; - if (event.cbutton.button == SDL_CONTROLLER_BUTTON_A && !games.empty()) selectionMade = true; + + if (fullscreenSelectorMode) { + continue; } - if (!inSettings && event.type == SDL_KEYDOWN) { - if (event.key.keysym.sym == SDLK_UP && selected > 0) selected--; - if (event.key.keysym.sym == SDLK_DOWN && selected < (int)games.size() - 1) selected++; - if (event.key.keysym.sym == SDLK_RETURN && !games.empty()) selectionMade = true; + + if (showSettings) { + if (event.type == SDL_KEYDOWN && (event.key.keysym.sym == SDLK_ESCAPE || event.key.keysym.sym == SDLK_F2)) { + showSettings = false; + } + if (event.type == SDL_CONTROLLERBUTTONDOWN && + (event.cbutton.button == SDL_CONTROLLER_BUTTON_B || event.cbutton.button == SDL_CONTROLLER_BUTTON_BACK)) { + showSettings = false; + } + continue; + } + + if (event.type == SDL_CONTROLLERBUTTONDOWN) { + if (event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_UP && selected > 0) { + selected--; + } + if (event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN && selected < static_cast(games.size()) - 1) { + selected++; + } + if (event.cbutton.button == SDL_CONTROLLER_BUTTON_A && !games.empty()) { + selected_path = games[selected].path; + } + if (event.cbutton.button == SDL_CONTROLLER_BUTTON_START) { + showSettings = true; + } + } + + if (event.type == SDL_KEYDOWN) { + if (event.key.keysym.sym == SDLK_UP && selected > 0) { + selected--; + } + if (event.key.keysym.sym == SDLK_DOWN && selected < static_cast(games.size()) - 1) { + selected++; + } + if ((event.key.keysym.sym == SDLK_RETURN || event.key.keysym.sym == SDLK_KP_ENTER) && !games.empty()) { + selected_path = games[selected].path; + } + if (event.key.keysym.sym == SDLK_F2) { + showSettings = true; + } + if (event.key.keysym.sym == SDLK_ESCAPE) { + return std::nullopt; + } + if (event.key.keysym.sym == SDLK_F5) { + games = scanAllGames(); + } } } - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplSDL2_NewFrame(window); + imguiBackend.newFrame(); ImGui::NewFrame(); - int drawableW = 0; - int drawableH = 0; - SDL_GL_GetDrawableSize(window, &drawableW, &drawableH); - #ifdef IMGUI_FRONTEND_DEBUG - if (drawableW == 0 || drawableH == 0) { - static bool warnedOnce = false; - if (!warnedOnce) { - warnedOnce = true; - printf("[IMGUI] selector: drawable size is %dx%d\n", drawableW, drawableH); - } + int drawable_w = 0; + int drawable_h = 0; + SDL_GL_GetDrawableSize(window, &drawable_w, &drawable_h); + if (drawable_w > 0 && drawable_h > 0) { + glViewport(0, 0, drawable_w, drawable_h); + glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); } - #endif - ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; - const float maxW = std::max(200.0f, drawableW * 0.9f); - const float maxH = std::max(150.0f, drawableH * 0.9f); + const auto classic_request = fsuiAdapter.consumeClassicUiRequest(); + if (classic_request != PandaFsuiAdapter::ClassicUiRequest::None) { + fullscreenSelectorMode = false; + fsuiAdapter.setSelectorMode(false); + showSettings = (classic_request == PandaFsuiAdapter::ClassicUiRequest::OpenSettings); + games = scanAllGames(); + } - if (showNoRom) { - std::string rootLabel = primaryScanRootLabel(); - ImGui::SetNextWindowPos(ImVec2(drawableW * 0.5f, drawableH * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(std::min(620.0f, maxW), std::min(260.0f, maxH)), ImGuiCond_Always); - ImGui::Begin("No ROMs Found", nullptr, flags); + drawDebugPanel(); + if (fullscreenSelectorMode) { + fsuiAdapter.render(); + fsuiAdapter.consumeCommands(); + if (auto fsui_selection = fsuiAdapter.consumeLaunchPath(); fsui_selection.has_value()) { + selected_path = fsui_selection; + } + if (fsuiAdapter.consumeCloseSelector() && !selected_path.has_value()) { + fsuiAdapter.setSelectorMode(false); + fullscreenSelectorMode = false; + return std::nullopt; + } + } else if (showSettings) { + drawClassicSettingsPanel(); + } else if (games.empty()) { + std::string root_label = primaryScanRootLabel(); + ImGui::SetNextWindowPos(ImVec2(drawable_w * 0.5f, drawable_h * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(std::min(620.0f, drawable_w * 0.9f), std::min(260.0f, drawable_h * 0.9f)), ImGuiCond_Always); + ImGui::Begin("No ROMs Found", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); ImGui::TextWrapped( "No ROM inserted!\n\n" "Please add ROMs (supported formats: .cci, .3ds, .cxi, .app, .ncch, .elf, .axf, .3dsx) to:\n" "E:/\nE:/PANDA3DS\n%s\n%s/PANDA3DS", - rootLabel.c_str(), rootLabel.c_str() + root_label.c_str(), root_label.c_str() ); - ImGui::Dummy(ImVec2(0, 8)); - if (ImGui::Button("Retry", ImVec2(120, 0))) { + ImGui::Dummy(ImVec2(0.0f, 8.0f)); + if (ImGui::Button("Retry", ImVec2(120.0f, 0.0f))) { games = scanAllGames(); - showNoRom = games.empty(); } ImGui::SameLine(); - if (ImGui::Button("Settings", ImVec2(120, 0))) { - inSettings = true; - showSettings = true; - } - ImGui::End(); - } else if (!inSettings) { - ImGui::SetNextWindowPos(ImVec2(drawableW * 0.5f, drawableH * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(std::min(800.0f, maxW), std::min(600.0f, maxH)), ImGuiCond_Always); - ImGui::Begin("Select Game", nullptr, flags); - - for (int i = 0; i < (int)games.size(); i++) { - char buf[512]; - snprintf(buf, sizeof(buf), "%d: %s (ID: %s)", i + 1, games[i].title.c_str(), games[i].id.c_str()); - if (ImGui::Selectable(buf, selected == i)) selected = i; - } - ImGui::Dummy(ImVec2(0, 8)); - ImGui::Separator(); - ImGui::Dummy(ImVec2(0, 8)); - ImGui::SetCursorPosX((ImGui::GetWindowWidth() - 120.0f) * 0.5f); - if (ImGui::Button("Settings", ImVec2(120, 0))) { - inSettings = true; + if (ImGui::Button("Settings", ImVec2(120.0f, 0.0f))) { showSettings = true; } ImGui::End(); } else { - drawSettingsPanel(); - if (!showSettings) { - inSettings = false; + ImGui::SetNextWindowPos(ImVec2(drawable_w * 0.5f, drawable_h * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(std::min(800.0f, drawable_w * 0.9f), std::min(600.0f, drawable_h * 0.9f)), ImGuiCond_Always); + ImGui::Begin("Select Game", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); + for (int i = 0; i < static_cast(games.size()); i++) { + char buffer[512]; + std::snprintf(buffer, sizeof(buffer), "%d: %s (ID: %s)", i + 1, games[i].title.c_str(), games[i].id.c_str()); + if (ImGui::Selectable(buffer, selected == i)) { + selected = i; + } } - } - - if (selectionMade && !games.empty()) { - selectedPath = games[selected].path; + ImGui::Dummy(ImVec2(0.0f, 8.0f)); + ImGui::Separator(); + ImGui::Dummy(ImVec2(0.0f, 8.0f)); + if (ImGui::Button("Launch", ImVec2(120.0f, 0.0f))) { + selected_path = games[selected].path; + } + ImGui::SameLine(); + if (ImGui::Button("Refresh", ImVec2(120.0f, 0.0f))) { + games = scanAllGames(); + } + ImGui::SameLine(); + if (ImGui::Button("Settings", ImVec2(120.0f, 0.0f))) { + showSettings = true; + } + ImGui::End(); } ImGui::Render(); - SDL_GL_MakeCurrent(window, glContext); - if (drawableW > 0 && drawableH > 0) { - glViewport(0, 0, drawableW, drawableH); - glClearColor(0.1f, 0.1f, 0.1f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - } glBindFramebuffer(GL_FRAMEBUFFER, 0); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - #ifdef IMGUI_FRONTEND_DEBUG - GLenum err = glGetError(); - if (err != GL_NO_ERROR) { - printf("[IMGUI] selector: glGetError=0x%X\n", err); - } - #endif - SDL_GL_SwapWindow(window); + imguiBackend.renderDrawData(); SDL_Delay(16); } - return selectedPath; + fsuiAdapter.setSelectorMode(false); + fullscreenSelectorMode = false; + showPauseMenu = false; + isPaused = false; + return selected_path; } -void ImGuiLayer::drawDebugPanel() { +void ImGuiLayer::drawDebugPanel() +{ if (!showDebug) { return; } - int winW = 0; - int winH = 0; - SDL_GL_GetDrawableSize(window, &winW, &winH); - const float maxW = std::max(220.0f, winW * 0.45f); - const float maxH = std::max(120.0f, winH * 0.45f); - ImGui::SetNextWindowSizeConstraints(ImVec2(200.0f, 0.0f), ImVec2(maxW, maxH)); - - ImGui::SetNextWindowPos(ImVec2(float(kDebugPadding), float(kDebugPadding)), ImGuiCond_Always); + int win_w = 0; + int win_h = 0; + SDL_GL_GetDrawableSize(window, &win_w, &win_h); + const float max_w = std::max(220.0f, win_w * 0.45f); + const float max_h = std::max(120.0f, win_h * 0.45f); + ImGui::SetNextWindowSizeConstraints(ImVec2(200.0f, 0.0f), ImVec2(max_w, max_h)); + ImGui::SetNextWindowPos(ImVec2(10.0f, 10.0f), ImGuiCond_Always); ImGui::SetNextWindowBgAlpha(0.35f); - ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings; - ImGui::Begin("##DebugOverlay", nullptr, flags); - ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + maxW - 20.0f); + ImGui::Begin("##DebugOverlay", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + max_w - 20.0f); int major = 0; int minor = 0; glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); - const VersionInfo versionInfo = splitVersionString(PANDA3DS_VERSION); + const VersionInfo version_info = splitVersionString(PANDA3DS_VERSION); ImGui::Text("Context : GL %d.%d", major, minor); ImGui::Text("Driver : %s", glGetString(GL_RENDERER)); - ImGui::Text("Version : %s", versionInfo.appVersion.c_str()); - ImGui::Text("Revision: %s", versionInfo.gitRevision.c_str()); + ImGui::Text("Version : %s", version_info.app_version.c_str()); + ImGui::Text("Revision: %s", version_info.git_revision.c_str()); ImGui::Text("FPS : %.1f", ImGui::GetIO().Framerate); ImGui::Text("Paused : %s", isPaused ? "Yes" : "No"); ImGui::PopTextWrapPos(); ImGui::End(); } -void ImGuiLayer::drawPausePanel() { +void ImGuiLayer::drawClassicPausePanel() +{ if (!showPauseMenu) { return; } - int winW = 0; - int winH = 0; - SDL_GL_GetDrawableSize(window, &winW, &winH); + int win_w = 0; + int win_h = 0; + SDL_GL_GetDrawableSize(window, &win_w, &win_h); - ImGui::SetNextWindowSize(ImVec2(260, 160), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImVec2(winW * 0.5f, winH * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(260.0f, 160.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2(win_w * 0.5f, win_h * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::Begin("##PauseMenu", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); - ImGui::TextUnformatted("Game Paused"); - ImGui::Dummy(ImVec2(0, 8)); + ImGui::Dummy(ImVec2(0.0f, 8.0f)); - if (ImGui::Button("Resume", ImVec2(-1, 0))) { + if (ImGui::Button("Resume", ImVec2(-1.0f, 0.0f))) { showPauseMenu = false; if (onPauseChange) { isPaused = false; onPauseChange(false); } } - if (ImGui::Button("Settings", ImVec2(-1, 0))) { + if (ImGui::Button("Settings", ImVec2(-1.0f, 0.0f))) { showSettings = true; } - if (ImGui::Button("Quit", ImVec2(-1, 0))) { + if (ImGui::Button("Quit", ImVec2(-1.0f, 0.0f))) { if (onExitToSelector) { onExitToSelector(); } else { - SDL_Event quit{}; + SDL_Event quit {}; quit.type = SDL_QUIT; SDL_PushEvent(&quit); } } - ImGui::End(); } -void ImGuiLayer::drawSettingsPanel() { +void ImGuiLayer::drawSettingsGeneralSection(bool& reloadSettings) +{ + EmulatorConfig& cfg = emu.getConfig(); + const char* current_lang = EmulatorConfig::languageCodeToString(cfg.systemLanguage); + reloadSettings |= ImGui::Checkbox("Enable Discord RPC", &cfg.discordRpcEnabled); + reloadSettings |= ImGui::Checkbox("Print App Version", &cfg.printAppVersion); + reloadSettings |= ImGui::Checkbox("Enable Circle Pad Pro", &cfg.circlePadProEnabled); + reloadSettings |= ImGui::Checkbox("Enable Fastmem", &cfg.fastmemEnabled); + reloadSettings |= ImGui::Checkbox("Use Portable Build", &cfg.usePortableBuild); + + struct LanguageOption { + const char* label; + const char* code; + }; + static const LanguageOption language_options[] = { + {"English (en)", "en"}, {"Japanese (ja)", "ja"}, {"French (fr)", "fr"}, {"German (de)", "de"}, + {"Italian (it)", "it"}, {"Spanish (es)", "es"}, {"Chinese (zh)", "zh"}, {"Korean (ko)", "ko"}, + {"Dutch (nl)", "nl"}, {"Portuguese (pt)", "pt"}, {"Russian (ru)", "ru"}, {"Taiwanese (tw)", "tw"}, + }; + static const char* language_labels[] = { + "English (en)", "Japanese (ja)", "French (fr)", "German (de)", + "Italian (it)", "Spanish (es)", "Chinese (zh)", "Korean (ko)", + "Dutch (nl)", "Portuguese (pt)", "Russian (ru)", "Taiwanese (tw)", + }; + int lang_index = 0; + for (int i = 0; i < static_cast(std::size(language_options)); i++) { + if (std::strcmp(language_options[i].code, current_lang) == 0) { + lang_index = i; + break; + } + } + if (ImGui::Combo("System Language", &lang_index, language_labels, static_cast(std::size(language_labels)))) { + cfg.systemLanguage = EmulatorConfig::languageCodeFromString(language_options[lang_index].code); + reloadSettings = true; + } +} + +void ImGuiLayer::drawSettingsWindowSection(bool& reloadSettings) +{ + EmulatorConfig& cfg = emu.getConfig(); + reloadSettings |= ImGui::Checkbox("Show App Version", &cfg.windowSettings.showAppVersion); + reloadSettings |= ImGui::Checkbox("Remember Position", &cfg.windowSettings.rememberPosition); + ImGui::InputInt("Pos X", &cfg.windowSettings.x); + ImGui::InputInt("Pos Y", &cfg.windowSettings.y); + ImGui::InputInt("Width", &cfg.windowSettings.width); + ImGui::InputInt("Height", &cfg.windowSettings.height); +} + +void ImGuiLayer::drawSettingsUISection(bool& reloadSettings) +{ + EmulatorConfig& cfg = emu.getConfig(); + static const char* theme_labels[] = {"System", "Light", "Dark", "Greetings Cat", "Cream", "OLED"}; + static const FrontendSettings::Theme theme_values[] = { + FrontendSettings::Theme::System, FrontendSettings::Theme::Light, FrontendSettings::Theme::Dark, + FrontendSettings::Theme::GreetingsCat, FrontendSettings::Theme::Cream, FrontendSettings::Theme::Oled, + }; + int theme_index = 0; + for (int i = 0; i < static_cast(std::size(theme_values)); i++) { + if (cfg.frontendSettings.theme == theme_values[i]) { + theme_index = i; + break; + } + } + if (ImGui::Combo("Theme", &theme_index, theme_labels, static_cast(std::size(theme_labels)))) { + cfg.frontendSettings.theme = theme_values[theme_index]; + reloadSettings = true; + } + + int fsui_theme_index = 0; + for (int i = 0; i < static_cast(std::size(s_fsui_theme_values)); i++) { + if (cfg.fsuiTheme == s_fsui_theme_values[i]) { + fsui_theme_index = i; + break; + } + } + if (ImGui::Combo("Fullscreen UI Theme", &fsui_theme_index, s_fsui_theme_labels, static_cast(std::size(s_fsui_theme_labels)))) { + cfg.fsuiTheme = s_fsui_theme_values[fsui_theme_index]; + } + + static const char* icon_labels[] = {"Rpog", "Rsyn", "Rnap", "Rcow", "SkyEmu", "Runpog"}; + static const FrontendSettings::WindowIcon icon_values[] = { + FrontendSettings::WindowIcon::Rpog, FrontendSettings::WindowIcon::Rsyn, FrontendSettings::WindowIcon::Rnap, + FrontendSettings::WindowIcon::Rcow, FrontendSettings::WindowIcon::SkyEmu, FrontendSettings::WindowIcon::Runpog, + }; + int icon_index = 0; + for (int i = 0; i < static_cast(std::size(icon_values)); i++) { + if (cfg.frontendSettings.icon == icon_values[i]) { + icon_index = i; + break; + } + } + if (ImGui::Combo("Window Icon", &icon_index, icon_labels, static_cast(std::size(icon_labels)))) { + cfg.frontendSettings.icon = icon_values[icon_index]; + reloadSettings = true; + } + + bool show_debug_panel = cfg.frontendSettings.showImGuiDebugPanel; + if (ImGui::Checkbox("Show ImGui Debug Panel", &show_debug_panel)) { + cfg.frontendSettings.showImGuiDebugPanel = show_debug_panel; + showDebug = show_debug_panel; + } + ImGui::Checkbox("Stretch Output To Window", &cfg.frontendSettings.stretchImGuiOutputToWindow); + bool enable_fullscreen_ui = cfg.frontendSettings.enableFullscreenUI; + if (ImGui::Checkbox("Switch To Fullscreen UI", &enable_fullscreen_ui)) { + cfg.frontendSettings.enableFullscreenUI = enable_fullscreen_ui; + if (enable_fullscreen_ui) { + showSettings = false; + fullscreenSelectorMode = (emu.romType == ROMType::None); + fsuiAdapter.setSelectorMode(fullscreenSelectorMode); + if (!fullscreenSelectorMode && showPauseMenu) { + fsuiAdapter.openPauseMenu(); + } + fsuiAdapter.switchToSettings(); + } else { + showPauseMenu = (emu.romType != ROMType::None); + showSettings = true; + } + } + ImGui::TextWrapped("When enabled, Alber uses the fullscreen interface instead of the classic centered ImGui menus."); +} + +void ImGuiLayer::drawSettingsGraphicsSection(bool& reloadSettings) +{ + EmulatorConfig& cfg = emu.getConfig(); + ImGui::TextUnformatted("Renderer: OpenGL 4.1"); + if (ImGui::Checkbox("Enable VSync", &cfg.vsyncEnabled)) { + if (onVsyncChange) { + onVsyncChange(cfg.vsyncEnabled); + } + } + reloadSettings |= ImGui::Checkbox("Enable Shader JIT", &cfg.shaderJitEnabled); + reloadSettings |= ImGui::Checkbox("Use Ubershaders", &cfg.useUbershaders); + reloadSettings |= ImGui::Checkbox("Accurate Shader Mul", &cfg.accurateShaderMul); + reloadSettings |= ImGui::Checkbox("Accelerate Shaders", &cfg.accelerateShaders); + reloadSettings |= ImGui::Checkbox("Force Shadergen for Lighting", &cfg.forceShadergenForLights); + reloadSettings |= ImGui::InputInt("Shadergen Light Threshold", &cfg.lightShadergenThreshold); + cfg.lightShadergenThreshold = std::clamp(cfg.lightShadergenThreshold, 0, 8); + reloadSettings |= ImGui::Checkbox("Hash Textures", &cfg.hashTextures); + reloadSettings |= ImGui::Checkbox("Enable Renderdoc", &cfg.enableRenderdoc); + + static const char* layout_labels[] = {"Default", "Default Flipped", "Side By Side", "Side By Side Flipped"}; + static const ScreenLayout::Layout layout_values[] = { + ScreenLayout::Layout::Default, ScreenLayout::Layout::DefaultFlipped, ScreenLayout::Layout::SideBySide, + ScreenLayout::Layout::SideBySideFlipped, + }; + int layout_index = 0; + for (int i = 0; i < static_cast(std::size(layout_values)); i++) { + if (cfg.screenLayout == layout_values[i]) { + layout_index = i; + break; + } + } + if (ImGui::Combo("Screen Layout", &layout_index, layout_labels, static_cast(std::size(layout_labels)))) { + cfg.screenLayout = layout_values[layout_index]; + reloadSettings = true; + } + reloadSettings |= ImGui::SliderFloat("Top Screen Size", &cfg.topScreenSize, 0.0f, 1.0f); +} + +void ImGuiLayer::drawSettingsAudioSection(bool& reloadSettings) +{ + EmulatorConfig& cfg = emu.getConfig(); + reloadSettings |= ImGui::Checkbox("Enable Audio", &cfg.audioEnabled); + reloadSettings |= ImGui::Checkbox("Mute Audio", &cfg.audioDeviceConfig.muteAudio); + reloadSettings |= ImGui::SliderFloat("Volume", &cfg.audioDeviceConfig.volumeRaw, 0.0f, 2.0f); + reloadSettings |= ImGui::Checkbox("Enable AAC Audio", &cfg.aacEnabled); + reloadSettings |= ImGui::Checkbox("Print DSP Firmware", &cfg.printDSPFirmware); + + static const char* dsp_labels[] = {"Null", "Teakra", "HLE"}; + static const Audio::DSPCore::Type dsp_values[] = {Audio::DSPCore::Type::Null, Audio::DSPCore::Type::Teakra, Audio::DSPCore::Type::HLE}; + int dsp_index = 0; + for (int i = 0; i < static_cast(std::size(dsp_values)); i++) { + if (cfg.dspType == dsp_values[i]) { + dsp_index = i; + break; + } + } + if (ImGui::Combo("DSP Emulation", &dsp_index, dsp_labels, static_cast(std::size(dsp_labels)))) { + cfg.dspType = dsp_values[dsp_index]; + reloadSettings = true; + } + + static const char* curve_labels[] = {"Cubic", "Linear"}; + static const AudioDeviceConfig::VolumeCurve curve_values[] = { + AudioDeviceConfig::VolumeCurve::Cubic, AudioDeviceConfig::VolumeCurve::Linear + }; + int curve_index = 0; + for (int i = 0; i < static_cast(std::size(curve_values)); i++) { + if (cfg.audioDeviceConfig.volumeCurve == curve_values[i]) { + curve_index = i; + break; + } + } + if (ImGui::Combo("Volume Curve", &curve_index, curve_labels, static_cast(std::size(curve_labels)))) { + cfg.audioDeviceConfig.volumeCurve = curve_values[curve_index]; + reloadSettings = true; + } +} + +void ImGuiLayer::drawSettingsBatterySection(bool& reloadSettings) +{ + EmulatorConfig& cfg = emu.getConfig(); + reloadSettings |= ImGui::Checkbox("Charger Plugged", &cfg.chargerPlugged); + reloadSettings |= ImGui::SliderInt("Battery %", &cfg.batteryPercentage, 0, 100); +} + +void ImGuiLayer::drawSettingsSDSection(bool& reloadSettings) +{ + EmulatorConfig& cfg = emu.getConfig(); + reloadSettings |= ImGui::Checkbox("Use Virtual SD", &cfg.sdCardInserted); + reloadSettings |= ImGui::Checkbox("Write Protect SD", &cfg.sdWriteProtected); +} + +void ImGuiLayer::drawClassicSettingsPanel() +{ if (!showSettings) { return; } - int winW = 0; - int winH = 0; - SDL_GL_GetDrawableSize(window, &winW, &winH); - const float maxW = std::max(240.0f, winW * 0.9f); - const float maxH = std::max(200.0f, winH * 0.9f); - const float width = std::min(520.0f, maxW); - const float height = std::min(640.0f * 0.65f, maxH); + int win_w = 0; + int win_h = 0; + SDL_GL_GetDrawableSize(window, &win_w, &win_h); + const float max_w = std::max(240.0f, win_w * 0.9f); + const float max_h = std::max(200.0f, win_h * 0.9f); + const float width = std::min(520.0f, max_w); + const float height = std::min(640.0f * 0.65f, max_h); ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImVec2(winW * 0.5f, winH * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowPos(ImVec2(win_w * 0.5f, win_h * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); EmulatorConfig& cfg = emu.getConfig(); - bool reloadSettings = false; - bool saveConfig = false; - const char* currentLang = EmulatorConfig::languageCodeToString(cfg.systemLanguage); + bool reload_settings = false; + bool save_config = false; ImGui::Begin("Settings", &showSettings, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); - if (ImGui::CollapsingHeader("General", ImGuiTreeNodeFlags_DefaultOpen)) { - reloadSettings |= ImGui::Checkbox("Enable Discord RPC", &cfg.discordRpcEnabled); - reloadSettings |= ImGui::Checkbox("Print App Version", &cfg.printAppVersion); - reloadSettings |= ImGui::Checkbox("Enable Circle Pad Pro", &cfg.circlePadProEnabled); - reloadSettings |= ImGui::Checkbox("Enable Fastmem", &cfg.fastmemEnabled); - reloadSettings |= ImGui::Checkbox("Use Portable Build", &cfg.usePortableBuild); - - struct LanguageOption { - const char* label; - const char* code; - }; - static const LanguageOption languageOptions[] = { - {"English (en)", "en"}, {"Japanese (ja)", "ja"}, {"French (fr)", "fr"}, - {"German (de)", "de"}, {"Italian (it)", "it"}, {"Spanish (es)", "es"}, - {"Chinese (zh)", "zh"}, {"Korean (ko)", "ko"}, {"Dutch (nl)", "nl"}, - {"Portuguese (pt)", "pt"}, {"Russian (ru)", "ru"}, {"Taiwanese (tw)", "tw"}, - }; - int langIndex = 0; - for (int i = 0; i < (int)std::size(languageOptions); i++) { - if (std::strcmp(languageOptions[i].code, currentLang) == 0) { - langIndex = i; - break; - } - } - if (ImGui::Combo("System Language", &langIndex, [](void* data, int idx, const char** outText) { - const auto* options = static_cast(data); - *outText = options[idx].label; - return true; - }, (void*)languageOptions, (int)std::size(languageOptions))) { - cfg.systemLanguage = EmulatorConfig::languageCodeFromString(languageOptions[langIndex].code); - reloadSettings = true; - } + drawSettingsGeneralSection(reload_settings); } - if (ImGui::CollapsingHeader("Window")) { - reloadSettings |= ImGui::Checkbox("Show App Version", &cfg.windowSettings.showAppVersion); - reloadSettings |= ImGui::Checkbox("Remember Position", &cfg.windowSettings.rememberPosition); - ImGui::InputInt("Pos X", &cfg.windowSettings.x); - ImGui::InputInt("Pos Y", &cfg.windowSettings.y); - ImGui::InputInt("Width", &cfg.windowSettings.width); - ImGui::InputInt("Height", &cfg.windowSettings.height); + drawSettingsWindowSection(reload_settings); } - if (ImGui::CollapsingHeader("UI")) { - static const char* themeLabels[] = {"System", "Light", "Dark", "Greetings Cat", "Cream", "OLED"}; - static const FrontendSettings::Theme themeValues[] = { - FrontendSettings::Theme::System, FrontendSettings::Theme::Light, FrontendSettings::Theme::Dark, - FrontendSettings::Theme::GreetingsCat, FrontendSettings::Theme::Cream, FrontendSettings::Theme::Oled, - }; - int themeIndex = 0; - for (int i = 0; i < (int)std::size(themeValues); i++) { - if (cfg.frontendSettings.theme == themeValues[i]) { - themeIndex = i; - break; - } - } - if (ImGui::Combo("Theme", &themeIndex, themeLabels, (int)std::size(themeLabels))) { - cfg.frontendSettings.theme = themeValues[themeIndex]; - reloadSettings = true; - } - - static const char* iconLabels[] = {"Rpog", "Rsyn", "Rnap", "Rcow", "SkyEmu", "Runpog"}; - static const FrontendSettings::WindowIcon iconValues[] = { - FrontendSettings::WindowIcon::Rpog, FrontendSettings::WindowIcon::Rsyn, FrontendSettings::WindowIcon::Rnap, - FrontendSettings::WindowIcon::Rcow, FrontendSettings::WindowIcon::SkyEmu, FrontendSettings::WindowIcon::Runpog, - }; - int iconIndex = 0; - for (int i = 0; i < (int)std::size(iconValues); i++) { - if (cfg.frontendSettings.icon == iconValues[i]) { - iconIndex = i; - break; - } - } - if (ImGui::Combo("Window Icon", &iconIndex, iconLabels, (int)std::size(iconLabels))) { - cfg.frontendSettings.icon = iconValues[iconIndex]; - reloadSettings = true; - } - - bool showDebugPanel = cfg.frontendSettings.showImGuiDebugPanel; - if (ImGui::Checkbox("Show ImGui Debug Panel", &showDebugPanel)) { - cfg.frontendSettings.showImGuiDebugPanel = showDebugPanel; - showDebug = showDebugPanel; - } - ImGui::Checkbox("Stretch Output To Window", &cfg.frontendSettings.stretchImGuiOutputToWindow); + drawSettingsUISection(reload_settings); } - if (ImGui::CollapsingHeader("Graphics")) { - ImGui::TextUnformatted("Renderer: OpenGL 4.1"); - if (ImGui::Checkbox("Enable VSync", &cfg.vsyncEnabled)) { - if (onVsyncChange) { - onVsyncChange(cfg.vsyncEnabled); - } - } - reloadSettings |= ImGui::Checkbox("Enable Shader JIT", &cfg.shaderJitEnabled); - reloadSettings |= ImGui::Checkbox("Use Ubershaders", &cfg.useUbershaders); - reloadSettings |= ImGui::Checkbox("Accurate Shader Mul", &cfg.accurateShaderMul); - reloadSettings |= ImGui::Checkbox("Accelerate Shaders", &cfg.accelerateShaders); - reloadSettings |= ImGui::Checkbox("Force Shadergen for Lighting", &cfg.forceShadergenForLights); - reloadSettings |= ImGui::InputInt("Shadergen Light Threshold", &cfg.lightShadergenThreshold); - cfg.lightShadergenThreshold = std::clamp(cfg.lightShadergenThreshold, 0, 8); - reloadSettings |= ImGui::Checkbox("Hash Textures", &cfg.hashTextures); - reloadSettings |= ImGui::Checkbox("Enable Renderdoc", &cfg.enableRenderdoc); - - static const char* layoutLabels[] = {"Default", "Default Flipped", "Side By Side", "Side By Side Flipped"}; - static const ScreenLayout::Layout layoutValues[] = { - ScreenLayout::Layout::Default, ScreenLayout::Layout::DefaultFlipped, ScreenLayout::Layout::SideBySide, - ScreenLayout::Layout::SideBySideFlipped, - }; - int layoutIndex = 0; - for (int i = 0; i < (int)std::size(layoutValues); i++) { - if (cfg.screenLayout == layoutValues[i]) { - layoutIndex = i; - break; - } - } - if (ImGui::Combo("Screen Layout", &layoutIndex, layoutLabels, (int)std::size(layoutLabels))) { - cfg.screenLayout = layoutValues[layoutIndex]; - reloadSettings = true; - } - reloadSettings |= ImGui::SliderFloat("Top Screen Size", &cfg.topScreenSize, 0.0f, 1.0f); + drawSettingsGraphicsSection(reload_settings); } - if (ImGui::CollapsingHeader("Audio")) { - reloadSettings |= ImGui::Checkbox("Enable Audio", &cfg.audioEnabled); - reloadSettings |= ImGui::Checkbox("Mute Audio", &cfg.audioDeviceConfig.muteAudio); - reloadSettings |= ImGui::SliderFloat("Volume", &cfg.audioDeviceConfig.volumeRaw, 0.0f, 2.0f); - reloadSettings |= ImGui::Checkbox("Enable AAC Audio", &cfg.aacEnabled); - reloadSettings |= ImGui::Checkbox("Print DSP Firmware", &cfg.printDSPFirmware); - - static const char* dspLabels[] = {"Null", "Teakra", "HLE"}; - static const Audio::DSPCore::Type dspValues[] = {Audio::DSPCore::Type::Null, Audio::DSPCore::Type::Teakra, Audio::DSPCore::Type::HLE}; - int dspIndex = 0; - for (int i = 0; i < (int)std::size(dspValues); i++) { - if (cfg.dspType == dspValues[i]) { - dspIndex = i; - break; - } - } - if (ImGui::Combo("DSP Emulation", &dspIndex, dspLabels, (int)std::size(dspLabels))) { - cfg.dspType = dspValues[dspIndex]; - reloadSettings = true; - } - - static const char* curveLabels[] = {"Cubic", "Linear"}; - static const AudioDeviceConfig::VolumeCurve curveValues[] = {AudioDeviceConfig::VolumeCurve::Cubic, AudioDeviceConfig::VolumeCurve::Linear}; - int curveIndex = 0; - for (int i = 0; i < (int)std::size(curveValues); i++) { - if (cfg.audioDeviceConfig.volumeCurve == curveValues[i]) { - curveIndex = i; - break; - } - } - if (ImGui::Combo("Volume Curve", &curveIndex, curveLabels, (int)std::size(curveLabels))) { - cfg.audioDeviceConfig.volumeCurve = curveValues[curveIndex]; - reloadSettings = true; - } + drawSettingsAudioSection(reload_settings); } - if (ImGui::CollapsingHeader("Battery")) { - reloadSettings |= ImGui::Checkbox("Charger Plugged", &cfg.chargerPlugged); - reloadSettings |= ImGui::SliderInt("Battery %", &cfg.batteryPercentage, 0, 100); + drawSettingsBatterySection(reload_settings); } - if (ImGui::CollapsingHeader("SD")) { - reloadSettings |= ImGui::Checkbox("Use Virtual SD", &cfg.sdCardInserted); - reloadSettings |= ImGui::Checkbox("Write Protect SD", &cfg.sdWriteProtected); + drawSettingsSDSection(reload_settings); } ImGui::Separator(); if (ImGui::Button("Save")) { - saveConfig = true; + save_config = true; } ImGui::SameLine(); if (ImGui::Button("Close")) { showSettings = false; } - ImGui::End(); - if (reloadSettings) { + if (reload_settings) { emu.reloadSettings(); } - if (saveConfig) { + if (save_config) { cfg.save(); } } diff --git a/src/panda_sdl/panda_fsui.cpp b/src/panda_sdl/panda_fsui.cpp new file mode 100644 index 00000000..66b47c3b --- /dev/null +++ b/src/panda_sdl/panda_fsui.cpp @@ -0,0 +1,1418 @@ +#ifdef IMGUI_FRONTEND + +#include "panda_sdl/panda_fsui.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "IconsFontAwesome5.h" +#include "IconsPromptFont.h" +#include "config.hpp" +#include "emulator.hpp" +#include "frontend_settings.hpp" +#include "fsui/imgui_fullscreen.hpp" +#include "fsui/platform_sdl2.hpp" +#include "io_file.hpp" +#include "services/region_codes.hpp" + +namespace +{ + const std::array s_window_icon_names = { + "Rpog", + "Rsyn", + "Rnap", + "Rcow", + "SkyEmu", + "Runpog", + }; + + const std::array s_window_icon_values = { + FrontendSettings::WindowIcon::Rpog, + FrontendSettings::WindowIcon::Rsyn, + FrontendSettings::WindowIcon::Rnap, + FrontendSettings::WindowIcon::Rcow, + FrontendSettings::WindowIcon::SkyEmu, + FrontendSettings::WindowIcon::Runpog, + }; + + const std::array s_screen_layout_names = { + "Default", + "Default Flipped", + "Side By Side", + "Side By Side Flipped", + }; + + const std::array s_screen_layout_values = { + ScreenLayout::Layout::Default, + ScreenLayout::Layout::DefaultFlipped, + ScreenLayout::Layout::SideBySide, + ScreenLayout::Layout::SideBySideFlipped, + }; + + const std::array s_dsp_names = { + "Null", + "Teakra", + "HLE", + }; + + const std::array s_dsp_values = { + Audio::DSPCore::Type::Null, + Audio::DSPCore::Type::Teakra, + Audio::DSPCore::Type::HLE, + }; + + const std::array s_volume_curve_names = { + "Cubic", + "Linear", + }; + + const std::array s_volume_curve_values = { + AudioDeviceConfig::VolumeCurve::Cubic, + AudioDeviceConfig::VolumeCurve::Linear, + }; + + template + int findArrayIndex(const std::array& values, const T& value, int fallback = 0) + { + for (int i = 0; i < static_cast(N); i++) { + if (values[static_cast(i)] == value) { + return i; + } + } + return fallback; + } + + template + int findStringIndex(const std::array& values, std::string_view value, int fallback = 0) + { + for (int i = 0; i < static_cast(N); i++) { + if (value == values[static_cast(i)]) { + return i; + } + } + return fallback; + } + + std::string toLower(std::string value) + { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return value; + } + + const char* windowIconFilename(FrontendSettings::WindowIcon icon) + { + switch (icon) { + case FrontendSettings::WindowIcon::Rsyn: return "rsyn_icon.png"; + case FrontendSettings::WindowIcon::Rnap: return "rnap_icon.png"; + case FrontendSettings::WindowIcon::Rcow: return "rcow_icon.png"; + case FrontendSettings::WindowIcon::SkyEmu: return "skyemu_icon.png"; + case FrontendSettings::WindowIcon::Runpog: return "runpog_icon.png"; + case FrontendSettings::WindowIcon::Rpog: + default: return "rpog_icon.png"; + } + } + + std::filesystem::path fsuiIconDirectory() + { + #ifdef PANDA3DS_FSUI_ICON_DIR + return std::filesystem::path(PANDA3DS_FSUI_ICON_DIR); + #else + return {}; + #endif + } + + std::string resolveFsuiAppIconPath(FrontendSettings::WindowIcon icon) + { + const std::string filename = windowIconFilename(icon); + std::vector candidates; + + char* base_path = SDL_GetBasePath(); + if (base_path != nullptr) { + const std::filesystem::path root(base_path); + SDL_free(base_path); + candidates.push_back(root / filename); + candidates.push_back(root / "docs" / "img" / filename); + candidates.push_back(root.parent_path() / "Resources" / filename); + candidates.push_back(root.parent_path() / "Resources" / "docs" / "img" / filename); + } + + const std::filesystem::path icon_dir = fsuiIconDirectory(); + if (!icon_dir.empty()) { + candidates.push_back(icon_dir / filename); + } + + for (const auto& candidate : candidates) { + std::error_code ec; + if (!candidate.empty() && std::filesystem::exists(candidate, ec)) { + return candidate.string(); + } + } + + return icon_dir.empty() ? filename : (icon_dir / filename).string(); + } + + void customizeInterfaceRows(std::vector& rows) + { + for (auto& row : rows) { + if (row.id == "fsui_prompt_icons") { + row.summary = "Chooses which controller prompt glyph set the fullscreen UI should use."; + } else if (row.id == "fsui_background_image") { + row.summary = "Sets a fullscreen background image for the application UI."; + } + } + } + + std::time_t fileTimeToTimeT(const std::filesystem::file_time_type& file_time) + { + using namespace std::chrono; + const auto system_now = system_clock::now(); + const auto file_now = std::filesystem::file_time_type::clock::now(); + const auto adjusted = time_point_cast(file_time - file_now + system_now); + return system_clock::to_time_t(adjusted); + } + + std::string regionToString(Regions region) + { + switch (region) { + case Regions::Japan: return "Japan"; + case Regions::USA: return "North America"; + case Regions::Europe: return "Europe"; + case Regions::Australia: return "Australia"; + case Regions::China: return "China"; + case Regions::Korea: return "Korea"; + case Regions::Taiwan: return "Taiwan"; + default: return "Other"; + } + } + + const char* regionToFlagAsset(Regions region) + { + switch (region) { + case Regions::Japan: return "icons/flags/jp.png"; + case Regions::USA: return "icons/flags/us.png"; + case Regions::Europe: return "icons/flags/eu.png"; + case Regions::Australia: return "icons/flags/au.png"; + case Regions::China: return "icons/flags/cn.png"; + case Regions::Korea: return "icons/flags/kr.png"; + case Regions::Taiwan: return "icons/flags/tw.png"; + default: return "icons/flags/Other.png"; + } + } + + bool readBytes(IOFile& file, std::uint64_t offset, void* out, size_t size) + { + if (!file.seek(static_cast(offset), SEEK_SET)) { + return false; + } + auto [ok, read] = file.readBytes(out, size); + return ok && read == size; + } + + std::string readSmdhString(const std::vector& data, size_t offset, size_t size) + { + std::string result; + result.reserve(size / 2); + for (size_t i = offset; i + 1 < offset + size && i + 1 < data.size(); i += 2) { + const std::uint16_t code = static_cast(data[i] | (data[i + 1] << 8)); + if (code == 0) { + break; + } + if (code < 0x80) { + result.push_back(static_cast(code)); + } else if (code < 0x800) { + result.push_back(static_cast(0xC0 | (code >> 6))); + result.push_back(static_cast(0x80 | (code & 0x3F))); + } else { + result.push_back(static_cast(0xE0 | (code >> 12))); + result.push_back(static_cast(0x80 | ((code >> 6) & 0x3F))); + result.push_back(static_cast(0x80 | (code & 0x3F))); + } + } + return result; + } +} // namespace + +struct PandaFsuiAdapter::ParsedMetadata +{ + std::string title; + std::string english_title; + Regions region = Regions::USA; + bool has_region = false; + std::string title_id; +}; + +struct PandaFsuiAdapter::LanguageOption +{ + const char* label; + const char* code; +}; + +PandaFsuiAdapter::PandaFsuiAdapter(SDL_Window* window_, Emulator& emu_) : window(window_), emu(emu_) {} + +void PandaFsuiAdapter::setPauseCallback(std::function callback) +{ + onPauseChange = std::move(callback); +} + +void PandaFsuiAdapter::setVsyncCallback(std::function callback) +{ + onVsyncChange = std::move(callback); +} + +void PandaFsuiAdapter::setExitToSelectorCallback(std::function callback) +{ + onExitToSelector = std::move(callback); +} + +void PandaFsuiAdapter::syncUiStateFromConfig() +{ + const EmulatorConfig& cfg = emu.getConfig(); + uiState.theme = cfg.fsuiTheme; + uiState.prompt_icon_pack = fsui::PromptIconPackFromString(cfg.fsuiPromptIconPack); + uiState.background_image_path = cfg.fsuiBackgroundImagePath; + uiState.default_game_view = cfg.fsuiDefaultGameView; + uiState.game_sort = cfg.fsuiGameSort; + uiState.game_sort_reverse = cfg.fsuiGameSortReverse; + uiState.game_list_paths = cfg.fsuiGameListPaths; + uiState.game_list_recursive_paths = cfg.fsuiGameListRecursivePaths; + uiState.covers_path = cfg.fsuiCoversPath.empty() ? (emu.getAppDataRoot() / "covers") : cfg.fsuiCoversPath; + fsuiContext.app_icon_path = resolveFsuiAppIconPath(cfg.frontendSettings.icon); +} + +void PandaFsuiAdapter::persistUiState(bool reload) +{ + EmulatorConfig& cfg = emu.getConfig(); + cfg.fsuiTheme = uiState.theme; + cfg.fsuiPromptIconPack = std::string(fsui::PromptIconPackToString(uiState.prompt_icon_pack)); + cfg.fsuiBackgroundImagePath = uiState.background_image_path; + cfg.fsuiDefaultGameView = uiState.default_game_view; + cfg.fsuiGameSort = uiState.game_sort; + cfg.fsuiGameSortReverse = uiState.game_sort_reverse; + cfg.fsuiGameListPaths = uiState.game_list_paths; + cfg.fsuiGameListRecursivePaths = uiState.game_list_recursive_paths; + cfg.fsuiCoversPath = uiState.covers_path; + cfg.save(); + if (reload) { + emu.reloadSettings(); + } +} + +std::vector PandaFsuiAdapter::currentScanRoots() const +{ + std::vector paths; + paths.emplace_back("E:/"); + paths.emplace_back(std::filesystem::path("E:/") / "PANDA3DS"); + + std::filesystem::path root_path; + #ifdef __WINRT__ + char* sdl_path = SDL_GetPrefPath(nullptr, nullptr); + #else + char* sdl_path = SDL_GetBasePath(); + #endif + if (sdl_path) { + root_path = std::filesystem::path(sdl_path); + SDL_free(sdl_path); + } + + if (!root_path.empty()) { + paths.push_back(root_path); + paths.push_back(root_path / "PANDA3DS"); + } + + std::vector unique_paths; + for (const auto& path : paths) { + if (std::find(unique_paths.begin(), unique_paths.end(), path) == unique_paths.end()) { + unique_paths.push_back(path); + } + } + return unique_paths; +} + +void PandaFsuiAdapter::seedGameListPathsIfNeeded() +{ + if (!uiState.game_list_paths.empty() || !uiState.game_list_recursive_paths.empty()) { + return; + } + + for (const auto& path : currentScanRoots()) { + uiState.game_list_paths.push_back(path); + } +} + +bool PandaFsuiAdapter::hasSupportedExtension(const std::filesystem::path& path) const +{ + static const std::set extensions = { + ".cci", ".3ds", ".cxi", ".app", ".ncch", ".elf", ".axf", ".3dsx", + }; + return extensions.contains(toLower(path.extension().string())); +} + +std::string PandaFsuiAdapter::formatPercent(float value) const +{ + char buffer[32] = {}; + std::snprintf(buffer, sizeof(buffer), "%.0f%%", value * 100.0f); + return buffer; +} + +std::string PandaFsuiAdapter::formatInteger(int value) const +{ + return std::to_string(value); +} + +std::string PandaFsuiAdapter::formatTitleId(std::uint64_t program_id) const +{ + if (program_id == 0) { + return {}; + } + char buffer[17] = {}; + std::snprintf(buffer, sizeof(buffer), "%016" PRIX64, program_id); + return buffer; +} + +std::filesystem::path PandaFsuiAdapter::defaultCoverDirectory() const +{ + return uiState.covers_path.empty() ? (emu.getAppDataRoot() / "covers") : uiState.covers_path; +} + +std::filesystem::path PandaFsuiAdapter::findCoverPath(const std::filesystem::path& rom_path, const std::string& title_id) const +{ + const std::filesystem::path covers_dir = defaultCoverDirectory(); + static const std::array cover_extensions = {".png", ".jpg", ".jpeg", ".bmp"}; + + auto try_base = [&](const std::string& base) -> std::filesystem::path { + if (base.empty()) { + return {}; + } + for (const char* ext : cover_extensions) { + const std::filesystem::path candidate = covers_dir / (base + ext); + if (std::filesystem::exists(candidate)) { + return candidate; + } + } + return {}; + }; + + if (auto by_title_id = try_base(title_id); !by_title_id.empty()) { + return by_title_id; + } + return try_base(rom_path.stem().string()); +} + +std::optional PandaFsuiAdapter::readMetadataForPath(const std::filesystem::path& path) const +{ + auto parseSmdh = [](const std::vector& smdh) -> ParsedMetadata { + ParsedMetadata metadata; + if (smdh.size() < 0x36C0) { + return metadata; + } + if (smdh[0] != 'S' || smdh[1] != 'M' || smdh[2] != 'D' || smdh[3] != 'H') { + return metadata; + } + + const u32 region_masks = *reinterpret_cast(&smdh[0x2018]); + const bool japan = (region_masks & 0x1) != 0; + const bool north_america = (region_masks & 0x2) != 0; + const bool europe = (region_masks & 0x4) != 0; + const bool australia = (region_masks & 0x8) != 0; + const bool china = (region_masks & 0x10) != 0; + const bool korea = (region_masks & 0x20) != 0; + const bool taiwan = (region_masks & 0x40) != 0; + + int meta_language = 1; + if (north_america) { + metadata.region = Regions::USA; + metadata.has_region = true; + } else if (europe) { + metadata.region = Regions::Europe; + metadata.has_region = true; + } else if (australia) { + metadata.region = Regions::Australia; + metadata.has_region = true; + } else if (japan) { + metadata.region = Regions::Japan; + metadata.has_region = true; + meta_language = 0; + } else if (korea) { + metadata.region = Regions::Korea; + metadata.has_region = true; + meta_language = 7; + } else if (china) { + metadata.region = Regions::China; + metadata.has_region = true; + meta_language = 6; + } else if (taiwan) { + metadata.region = Regions::Taiwan; + metadata.has_region = true; + meta_language = 6; + } + + metadata.english_title = readSmdhString(smdh, 0x8 + (512 * 1) + 0x80, 0x100); + metadata.title = readSmdhString(smdh, 0x8 + (512 * meta_language) + 0x80, 0x100); + if (metadata.title.empty()) { + metadata.title = metadata.english_title; + } + return metadata; + }; + + auto readNcchMetadata = [&](IOFile& file, std::uint64_t base_offset) -> std::optional { + std::array header {}; + if (!readBytes(file, base_offset, header.data(), header.size())) { + return std::nullopt; + } + if (header[0x100] != 'N' || header[0x101] != 'C' || header[0x102] != 'C' || header[0x103] != 'H') { + return std::nullopt; + } + + ParsedMetadata metadata; + metadata.title_id = formatTitleId(*reinterpret_cast(header.data() + 0x118)); + const std::uint64_t exefs_offset = base_offset + static_cast(*reinterpret_cast(header.data() + 0x1A0)) * 0x200ull; + const std::uint64_t exefs_size = static_cast(*reinterpret_cast(header.data() + 0x1A4)) * 0x200ull; + if (exefs_offset == base_offset || exefs_size < 0x200) { + return metadata; + } + + std::array exefs_header {}; + if (!readBytes(file, exefs_offset, exefs_header.data(), exefs_header.size())) { + return metadata; + } + + for (int i = 0; i < 10; i++) { + const u8* entry = exefs_header.data() + (i * 16); + char name[9] = {}; + std::memcpy(name, entry, 8); + const u32 file_offset = *reinterpret_cast(entry + 8); + const u32 file_size = *reinterpret_cast(entry + 12); + if (file_size == 0) { + continue; + } + if (std::string_view(name) == "icon") { + std::vector smdh(file_size); + if (readBytes(file, exefs_offset + 0x200 + file_offset, smdh.data(), smdh.size())) { + ParsedMetadata smdh_metadata = parseSmdh(smdh); + if (!smdh_metadata.title.empty()) { + metadata.title = std::move(smdh_metadata.title); + } + if (!smdh_metadata.english_title.empty()) { + metadata.english_title = std::move(smdh_metadata.english_title); + } + if (smdh_metadata.has_region) { + metadata.region = smdh_metadata.region; + metadata.has_region = true; + } + } + break; + } + } + + return metadata; + }; + + IOFile file; + if (!file.open(path, "rb")) { + return std::nullopt; + } + + const std::string ext = toLower(path.extension().string()); + if (ext == ".cxi" || ext == ".app" || ext == ".ncch") { + return readNcchMetadata(file, 0); + } + + if (ext == ".3ds" || ext == ".cci") { + std::array header {}; + if (!readBytes(file, 0, header.data(), header.size())) { + return std::nullopt; + } + if (header[0x100] != 'N' || header[0x101] != 'C' || header[0x102] != 'S' || header[0x103] != 'D') { + return std::nullopt; + } + for (int i = 0; i < 8; i++) { + const size_t entry_offset = 0x120 + i * 8; + const std::uint64_t partition_offset = static_cast(*reinterpret_cast(header.data() + entry_offset)) * 0x200ull; + const std::uint64_t partition_size = static_cast(*reinterpret_cast(header.data() + entry_offset + 4)) * 0x200ull; + if (partition_offset != 0 && partition_size != 0) { + return readNcchMetadata(file, partition_offset); + } + } + } + + return std::nullopt; +} + +fsui::GameEntry PandaFsuiAdapter::buildGameListEntry(const std::filesystem::directory_entry& entry) const +{ + fsui::GameEntry item; + item.path = entry.path(); + item.file_size = entry.file_size(); + item.modified_time = fileTimeToTimeT(entry.last_write_time()); + item.title = entry.path().stem().string(); + item.english_title = item.title; + item.title_sort = toLower(item.title); + + const std::string extension = toLower(entry.path().extension().string()); + if (extension == ".elf" || extension == ".axf" || extension == ".3dsx") { + item.type = fsui::GameEntryType::Homebrew; + } else { + item.type = fsui::GameEntryType::Cartridge; + } + + if (auto metadata = readMetadataForPath(entry.path()); metadata.has_value()) { + if (!metadata->title.empty()) { + item.title = metadata->title; + item.title_sort = toLower(metadata->title); + } + if (!metadata->english_title.empty()) { + item.english_title = metadata->english_title; + } + item.title_id = metadata->title_id; + if (metadata->has_region) { + item.region = regionToString(metadata->region); + item.region_flag_path = regionToFlagAsset(metadata->region); + } + } + + item.cover_path = findCoverPath(item.path, item.title_id); + return item; +} + +std::vector PandaFsuiAdapter::buildGameList() +{ + seedGameListPathsIfNeeded(); + std::vector items; + std::set seen; + + auto scan_directory = [&](const std::filesystem::path& root, bool recursive) { + std::error_code ec; + if (!std::filesystem::exists(root, ec) || !std::filesystem::is_directory(root, ec)) { + return; + } + + if (recursive) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(root, ec)) { + if (ec || !entry.is_regular_file()) { + continue; + } + if (!hasSupportedExtension(entry.path()) || !seen.insert(entry.path()).second) { + continue; + } + items.push_back(buildGameListEntry(entry)); + } + } else { + for (const auto& entry : std::filesystem::directory_iterator(root, ec)) { + if (ec || !entry.is_regular_file()) { + continue; + } + if (!hasSupportedExtension(entry.path()) || !seen.insert(entry.path()).second) { + continue; + } + items.push_back(buildGameListEntry(entry)); + } + } + }; + + for (const auto& path : uiState.game_list_paths) { + scan_directory(path, false); + } + for (const auto& path : uiState.game_list_recursive_paths) { + scan_directory(path, true); + } + return items; +} + +fsui::CurrentGameInfo PandaFsuiAdapter::buildCurrentGameInfo() +{ + fsui::CurrentGameInfo info; + const auto& rom_path = emu.getROMPath(); + if (!rom_path.has_value()) { + info.title = "No Game Loaded"; + info.subtitle = "No title is currently running."; + return info; + } + + info.has_game = true; + info.path = *rom_path; + info.title = rom_path->stem().string(); + info.subtitle = rom_path->filename().string(); + auto it = std::find_if(cachedGameList.begin(), cachedGameList.end(), [&](const fsui::GameEntry& entry) { return entry.path == *rom_path; }); + if (it != cachedGameList.end()) { + info.title = it->title; + info.title_id = it->title_id; + } + return info; +} + +std::string PandaFsuiAdapter::currentGameTitle() const +{ + const auto& rom_path = emu.getROMPath(); + return rom_path.has_value() ? rom_path->stem().string() : "No Game Loaded"; +} + +std::string PandaFsuiAdapter::currentGameSubtitle() const +{ + const auto& rom_path = emu.getROMPath(); + return rom_path.has_value() ? rom_path->filename().string() : "No title is currently running."; +} + +void PandaFsuiAdapter::requestLaunchPath(const std::filesystem::path& path) +{ + pendingLaunchPath = path; + closeSelectorRequested = true; +} + +void PandaFsuiAdapter::requestClassicUi(bool open_settings) +{ + emu.getConfig().frontendSettings.enableFullscreenUI = false; + emu.getConfig().save(); + pendingClassicUiRequest = open_settings ? ClassicUiRequest::OpenSettings : ClassicUiRequest::Return; +} + +void PandaFsuiAdapter::openUnsupportedPrompt(std::string title, std::string message) const +{ + ImGuiFullscreen::OpenInfoMessageDialog(std::move(title), std::move(message)); +} + +void PandaFsuiAdapter::openFileAndLaunch() +{ + ImGuiFullscreen::OpenFileSelector( + "Start File", + false, + [this](const std::string& path) { + if (!path.empty()) { + requestLaunchPath(path); + closeSelectorRequested = true; + } + ImGuiFullscreen::CloseFileSelector(); + }, + {".cci", ".3ds", ".cxi", ".app", ".ncch", ".elf", ".axf", ".3dsx"} + ); +} + +std::vector PandaFsuiAdapter::buildLandingItems() +{ + return { + { + .id = "game_list", + .icon_path = fsui::ResolveStartupIconPath(fsuiContext, fsui::BuiltInStartupIcon::GameList), + .title = "Game List", + .summary = "Launch a game from images scanned from your game directories.", + .on_activate = []() { fsui::ShowGameListWindow(); }, + }, + { + .id = "start_game", + .icon_path = fsui::ResolveStartupIconPath(fsuiContext, fsui::BuiltInStartupIcon::StartGame), + .title = "Start Game", + .summary = "Launch a game by selecting a file image.", + .on_activate = []() { fsui::ShowStartGameWindow(); }, + }, + { + .id = "settings", + .icon_path = fsui::ResolveStartupIconPath(fsuiContext, fsui::BuiltInStartupIcon::Settings), + .title = "Settings", + .summary = "Changes settings for the application.", + .on_activate = []() { fsui::SwitchToSettings(); }, + }, + { + .id = "exit", + .icon_path = fsui::ResolveStartupIconPath(fsuiContext, fsui::BuiltInStartupIcon::Exit), + .title = "Exit", + .summary = "Exit the application.", + .on_activate = []() { fsui::ShowExitWindow(); }, + }, + }; +} + +std::vector PandaFsuiAdapter::buildStartItems() +{ + return { + { + .id = "start_file", + .icon_path = fsui::ResolveStartupIconPath(fsuiContext, fsui::BuiltInStartupIcon::StartFile), + .title = "Start File", + .summary = "Launch a game by selecting a file image.", + .on_activate = [this]() { openFileAndLaunch(); }, + }, + { + .id = "back", + .icon_path = fsui::ResolveStartupIconPath(fsuiContext, fsui::BuiltInStartupIcon::Back), + .title = "Back", + .summary = "Return to the previous menu.", + .on_activate = []() { fsui::ShowLandingWindow(); }, + }, + }; +} + +std::vector PandaFsuiAdapter::buildExitItems() +{ + return { + { + .id = "back", + .icon_path = fsui::ResolveStartupIconPath(fsuiContext, fsui::BuiltInStartupIcon::Back), + .title = "Back", + .summary = "Return to the previous menu.", + .on_activate = []() { fsui::ShowLandingWindow(); }, + }, + { + .id = "exit_app", + .icon_path = fsui::ResolveStartupIconPath(fsuiContext, fsui::BuiltInStartupIcon::Exit), + .title = "Exit Alber", + .summary = "Completely exits the application.", + .on_activate = []() { + SDL_Event quit {}; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + }, + }, + { + .id = "desktop_mode", + .icon_path = "fullscreenui/desktop-mode.png", + .title = "Desktop Mode", + .summary = "Return to a desktop-style interface.", + .on_activate = [this]() { requestClassicUi(false); }, + }, + }; +} + +std::vector PandaFsuiAdapter::buildPauseItems() +{ + return { + { + .id = "resume", + .title = ICON_FA_PLAY " Resume Game", + .summary = "Return to the running game.", + .on_activate = []() { fsui::ReturnToMainWindow(); }, + }, + { + .id = "settings", + .title = ICON_FA_SLIDERS_H " Settings", + .summary = "Open per-game fullscreen settings.", + .on_activate = []() { fsui::SwitchToSettings(); }, + }, + { + .id = "close_game", + .title = ICON_FA_POWER_OFF " Close Game", + .summary = "Reset or close the running game.", + .children = { + { + .id = "reset_system", + .title = ICON_FA_SYNC " Reset System", + .summary = "Reset the currently running title.", + .on_activate = [this]() { + emu.reset(Emulator::ReloadOption::Reload); + fsui::ReturnToMainWindow(); + }, + }, + { + .id = "exit_without_saving", + .title = ICON_FA_POWER_OFF " Exit Without Saving", + .summary = "Close the game immediately.", + .on_activate = [this]() { + if (onExitToSelector) { + onExitToSelector(); + } else { + SDL_Event quit {}; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + } + }, + }, + }, + }, + }; +} + +std::vector PandaFsuiAdapter::buildGameLaunchOptions(const fsui::GameEntry& entry) +{ + return { + { + .id = "resume_game", + .title = "Resume Game", + .summary = "Launch the selected title.", + .on_activate = [this, path = entry.path]() { requestLaunchPath(path); }, + }, + { + .id = "default_boot", + .title = "Default Boot", + .summary = "Launch the selected title with default boot behavior.", + .on_activate = [this, path = entry.path]() { requestLaunchPath(path); }, + }, + }; +} + +std::vector PandaFsuiAdapter::buildSettingsPages(fsui::SettingsScope scope) +{ + const std::array language_options = {{ + {"English (en)", "en"}, + {"Japanese (ja)", "ja"}, + {"French (fr)", "fr"}, + {"German (de)", "de"}, + {"Italian (it)", "it"}, + {"Spanish (es)", "es"}, + {"Chinese (zh)", "zh"}, + {"Korean (ko)", "ko"}, + {"Dutch (nl)", "nl"}, + {"Portuguese (pt)", "pt"}, + {"Russian (ru)", "ru"}, + {"Taiwanese (tw)", "tw"}, + }}; + + std::vector pages; + if (scope == fsui::SettingsScope::PerGame) { + pages.push_back({ + .id = "summary", + .scope = scope, + .built_in_page = fsui::BuiltInSettingsPage::Summary, + .build_rows = [this]() { + std::vector rows; + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Current Game"}); + const fsui::CurrentGameInfo info = buildCurrentGameInfo(); + rows.push_back({.kind = fsui::SettingsRowKind::Value, .title = ICON_FA_INFO " Title", .summary = "The title currently loaded in Panda3DS.", .value = info.title, .enabled = false}); + rows.push_back({.kind = fsui::SettingsRowKind::Value, .title = ICON_FA_INFO_CIRCLE " Subtitle", .summary = "The current ROM filename.", .value = info.subtitle, .enabled = false}); + if (!info.title_id.empty()) { + rows.push_back({.kind = fsui::SettingsRowKind::Value, .title = ICON_FA_INFO_CIRCLE " Title ID", .summary = "The detected title ID for the current game.", .value = info.title_id, .enabled = false}); + } + if (!info.path.empty()) { + rows.push_back({.kind = fsui::SettingsRowKind::Value, .title = ICON_FA_FOLDER_OPEN " Path", .summary = "The full filesystem path to the current game.", .value = info.path.string(), .enabled = false}); + } + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Related"}); + rows.push_back({ + .kind = fsui::SettingsRowKind::Action, + .title = ICON_FA_LIST_ALT " Open Game List Settings", + .summary = "Configure directories, sort, and cover settings.", + .on_activate = []() { fsui::ShowGameListSettingsWindow(); }, + }); + return rows; + }, + }); + } + + pages.push_back({ + .id = "interface", + .scope = fsui::SettingsScope::Global, + .built_in_page = fsui::BuiltInSettingsPage::Interface, + .build_rows = [this, language_options]() { + EmulatorConfig& cfg = emu.getConfig(); + std::vector rows = fsui::BuildStandardInterfaceRows(); + customizeInterfaceRows(rows); + + const int icon_index = findArrayIndex(s_window_icon_values, cfg.frontendSettings.icon); + std::vector icon_choices; + for (int i = 0; i < static_cast(s_window_icon_names.size()); i++) { + icon_choices.push_back({s_window_icon_names[static_cast(i)], i == icon_index}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_FA_INFO_CIRCLE " Window Icon", + .summary = "Selects the application icon used by the desktop window.", + .value = s_window_icon_names[static_cast(icon_index)], + .dialog_title = ICON_FA_INFO_CIRCLE " Window Icon", + .choices = icon_choices, + .on_choice = [this](int index) { + emu.getConfig().frontendSettings.icon = s_window_icon_values[static_cast(index)]; + emu.getConfig().save(); + }, + }); + + rows.push_back({ + .kind = fsui::SettingsRowKind::Toggle, + .title = ICON_FA_LIST_ALT " Enable Fullscreen UI", + .summary = "Uses the fullscreen interface instead of the classic centered ImGui UI.", + .toggle_value = cfg.frontendSettings.enableFullscreenUI, + .on_toggle = [this](bool value) { + emu.getConfig().frontendSettings.enableFullscreenUI = value; + emu.getConfig().save(); + if (!value) { + requestClassicUi(true); + } + }, + }); + rows.push_back({ + .kind = fsui::SettingsRowKind::Toggle, + .title = ICON_FA_INFO_CIRCLE " Stretch Output To Window", + .summary = "Stretches the emulator output to match the current window size.", + .toggle_value = cfg.frontendSettings.stretchImGuiOutputToWindow, + .on_toggle = [this](bool value) { + emu.getConfig().frontendSettings.stretchImGuiOutputToWindow = value; + emu.getConfig().save(); + }, + }); + rows.push_back({ + .kind = fsui::SettingsRowKind::Toggle, + .title = ICON_FA_INFO_CIRCLE " Show ImGui Debug Panel", + .summary = "Displays the classic debugging and diagnostics panel.", + .toggle_value = cfg.frontendSettings.showImGuiDebugPanel, + .on_toggle = [this](bool value) { + emu.getConfig().frontendSettings.showImGuiDebugPanel = value; + emu.getConfig().save(); + }, + }); + rows.push_back({ + .kind = fsui::SettingsRowKind::Toggle, + .title = ICON_FA_INFO_CIRCLE " Show App Version On Window", + .summary = "Adds the Panda3DS version string to the desktop window title.", + .toggle_value = cfg.windowSettings.showAppVersion, + .on_toggle = [this](bool value) { + emu.getConfig().windowSettings.showAppVersion = value; + emu.getConfig().save(); + }, + }); + rows.push_back({ + .kind = fsui::SettingsRowKind::Toggle, + .title = ICON_FA_INFO_CIRCLE " Remember Window Position", + .summary = "Restores the previous desktop window size and position on launch.", + .toggle_value = cfg.windowSettings.rememberPosition, + .on_toggle = [this](bool value) { + emu.getConfig().windowSettings.rememberPosition = value; + emu.getConfig().save(); + }, + }); + + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Behaviour"}); + const char* current_language = EmulatorConfig::languageCodeToString(cfg.systemLanguage); + int language_index = 0; + for (int i = 0; i < static_cast(language_options.size()); i++) { + if (std::strcmp(language_options[static_cast(i)].code, current_language) == 0) { + language_index = i; + break; + } + } + std::vector language_choices; + for (int i = 0; i < static_cast(language_options.size()); i++) { + language_choices.push_back({language_options[static_cast(i)].label, i == language_index}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_PF_KEYBOARD_ALT " System Language", + .summary = "Selects the emulated system language used by software.", + .value = language_options[static_cast(language_index)].label, + .dialog_title = ICON_PF_KEYBOARD_ALT " System Language", + .choices = language_choices, + .on_choice = [this, language_options](int index) { + emu.getConfig().systemLanguage = EmulatorConfig::languageCodeFromString(language_options[static_cast(index)].code); + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + rows.push_back({ + .kind = fsui::SettingsRowKind::Toggle, + .title = ICON_FA_INFO_CIRCLE " Print App Version", + .summary = "Prints the application version information on startup.", + .toggle_value = cfg.printAppVersion, + .on_toggle = [this](bool value) { + emu.getConfig().printAppVersion = value; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + rows.push_back({ + .kind = fsui::SettingsRowKind::Toggle, + .title = ICON_FA_INFO_CIRCLE " Enable Discord RPC", + .summary = "Enables Discord rich presence support when available.", + .toggle_value = cfg.discordRpcEnabled, + .on_toggle = [this](bool value) { + emu.getConfig().discordRpcEnabled = value; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + return rows; + }, + }); + + pages.push_back({ + .id = "emulation", + .scope = scope, + .built_in_page = fsui::BuiltInSettingsPage::Emulation, + .build_rows = [this]() { + EmulatorConfig& cfg = emu.getConfig(); + std::vector rows; + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Core"}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_PF_GAMEPAD_ALT " Enable Circle Pad Pro", .summary = "Enables Circle Pad Pro emulation for games that support it.", .toggle_value = cfg.circlePadProEnabled, .on_toggle = [this](bool value) { emu.getConfig().circlePadProEnabled = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_PF_MICROCHIP " Enable Fastmem", .summary = "Uses host fast memory mappings for improved performance where supported.", .toggle_value = cfg.fastmemEnabled, .on_toggle = [this](bool value) { emu.getConfig().fastmemEnabled = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_ARCHIVE " Use Portable Build", .summary = "Stores emulator data relative to the application instead of the user profile directory.", .toggle_value = cfg.usePortableBuild, .on_toggle = [this](bool value) { emu.getConfig().usePortableBuild = value; emu.getConfig().save(); emu.reloadSettings(); }}); + + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Power"}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_POWER_OFF " Charger Plugged", .summary = "Controls the emulated charging state reported to software.", .toggle_value = cfg.chargerPlugged, .on_toggle = [this](bool value) { emu.getConfig().chargerPlugged = value; emu.getConfig().save(); emu.reloadSettings(); }}); + std::vector battery_choices; + for (int value = 0; value <= 100; value += 5) { + battery_choices.push_back({std::to_string(value), value == cfg.batteryPercentage}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_FA_INFO_CIRCLE " Battery Percentage", + .summary = "Sets the emulated battery level reported to software.", + .value = formatInteger(cfg.batteryPercentage), + .dialog_title = ICON_FA_INFO_CIRCLE " Battery Percentage", + .choices = battery_choices, + .on_choice = [this](int index) { + emu.getConfig().batteryPercentage = index * 5; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Storage"}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_SAVE " Use Virtual SD", .summary = "Controls whether the emulated SD card is inserted.", .toggle_value = cfg.sdCardInserted, .on_toggle = [this](bool value) { emu.getConfig().sdCardInserted = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_ARCHIVE " Write Protect Virtual SD", .summary = "Marks the emulated SD card as write-protected.", .toggle_value = cfg.sdWriteProtected, .on_toggle = [this](bool value) { emu.getConfig().sdWriteProtected = value; emu.getConfig().save(); emu.reloadSettings(); }}); + return rows; + }, + }); + + pages.push_back({ + .id = "graphics", + .scope = scope, + .built_in_page = fsui::BuiltInSettingsPage::Graphics, + .build_rows = [this]() { + EmulatorConfig& cfg = emu.getConfig(); + std::vector rows; + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Renderer"}); + rows.push_back({.kind = fsui::SettingsRowKind::Value, .title = ICON_PF_PICTURE " Renderer", .summary = "The active renderer backend for the SDL frontend.", .value = "OpenGL 4.1", .enabled = false}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_INFO_CIRCLE " Enable VSync", .summary = "Synchronizes presentation to the display refresh rate.", .toggle_value = cfg.vsyncEnabled, .on_toggle = [this](bool value) { emu.getConfig().vsyncEnabled = value; emu.getConfig().save(); if (onVsyncChange) onVsyncChange(value); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_PF_MICROCHIP " Enable Shader JIT", .summary = "Uses the shader JIT to improve GPU performance.", .toggle_value = cfg.shaderJitEnabled, .on_toggle = [this](bool value) { emu.getConfig().shaderJitEnabled = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_PF_PICTURE " Use Ubershaders", .summary = "Uses ubershaders for improved compatibility at a performance cost.", .toggle_value = cfg.useUbershaders, .on_toggle = [this](bool value) { emu.getConfig().useUbershaders = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_PF_PICTURE " Accurate Shader Multiplication", .summary = "Uses a slower but more accurate multiplication mode in shaders.", .toggle_value = cfg.accurateShaderMul, .on_toggle = [this](bool value) { emu.getConfig().accurateShaderMul = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_PF_PICTURE " Accelerate Shaders", .summary = "Uses accelerated shader compilation paths where available.", .toggle_value = cfg.accelerateShaders, .on_toggle = [this](bool value) { emu.getConfig().accelerateShaders = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_EXCLAMATION_TRIANGLE " Force Shadergen For Lighting", .summary = "Forces the shader generator path when enough lights are active.", .toggle_value = cfg.forceShadergenForLights, .on_toggle = [this](bool value) { emu.getConfig().forceShadergenForLights = value; emu.getConfig().save(); emu.reloadSettings(); }}); + + std::vector threshold_choices; + for (int value = 0; value <= 8; value++) { + threshold_choices.push_back({std::to_string(value), value == cfg.lightShadergenThreshold}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_FA_INFO_CIRCLE " Lighting Threshold", + .summary = "Number of active lights before shadergen is forced.", + .value = formatInteger(cfg.lightShadergenThreshold), + .dialog_title = ICON_FA_INFO_CIRCLE " Lighting Threshold", + .choices = threshold_choices, + .on_choice = [this](int index) { + emu.getConfig().lightShadergenThreshold = index; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_ARCHIVE " Hash Textures", .summary = "Hashes textures to improve texture replacement matching.", .toggle_value = cfg.hashTextures, .on_toggle = [this](bool value) { emu.getConfig().hashTextures = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_EXCLAMATION_TRIANGLE " Enable RenderDoc", .summary = "Enables RenderDoc capture support for graphics debugging.", .toggle_value = cfg.enableRenderdoc, .on_toggle = [this](bool value) { emu.getConfig().enableRenderdoc = value; emu.getConfig().save(); emu.reloadSettings(); }}); + + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Layout"}); + const int layout_index = findArrayIndex(s_screen_layout_values, cfg.screenLayout); + std::vector layout_choices; + for (int i = 0; i < static_cast(s_screen_layout_names.size()); i++) { + layout_choices.push_back({s_screen_layout_names[static_cast(i)], i == layout_index}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_FA_TH " Screen Layout", + .summary = "Chooses how the 3DS screens are arranged in the SDL window.", + .value = s_screen_layout_names[static_cast(layout_index)], + .dialog_title = ICON_FA_TH " Screen Layout", + .choices = layout_choices, + .on_choice = [this](int index) { + emu.getConfig().screenLayout = s_screen_layout_values[static_cast(index)]; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + std::vector top_screen_choices; + for (int value = 0; value <= 100; value += 5) { + top_screen_choices.push_back({std::to_string(value) + "%", value == static_cast(std::lround(cfg.topScreenSize * 100.0f))}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_FA_INFO_CIRCLE " Top Screen Size", + .summary = "Adjusts the size ratio of the top screen in split layouts.", + .value = formatPercent(cfg.topScreenSize), + .dialog_title = ICON_FA_INFO_CIRCLE " Top Screen Size", + .choices = top_screen_choices, + .on_choice = [this](int index) { + emu.getConfig().topScreenSize = static_cast(index) / 20.0f; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + return rows; + }, + }); + + pages.push_back({ + .id = "audio", + .scope = scope, + .built_in_page = fsui::BuiltInSettingsPage::Audio, + .build_rows = [this]() { + EmulatorConfig& cfg = emu.getConfig(); + std::vector rows; + rows.push_back({.kind = fsui::SettingsRowKind::Heading, .title = "Output"}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_PF_SOUND " Enable Audio", .summary = "Enables or disables audio output entirely.", .toggle_value = cfg.audioEnabled, .on_toggle = [this](bool value) { emu.getConfig().audioEnabled = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_VOLUME_MUTE " Mute Audio", .summary = "Mutes audio output without changing the configured volume.", .toggle_value = cfg.audioDeviceConfig.muteAudio, .on_toggle = [this](bool value) { emu.getConfig().audioDeviceConfig.muteAudio = value; emu.getConfig().save(); emu.reloadSettings(); }}); + + std::vector volume_choices; + for (int value = 0; value <= 200; value += 10) { + volume_choices.push_back({std::to_string(value) + "%", value == static_cast(std::lround(cfg.audioDeviceConfig.volumeRaw * 100.0f))}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_FA_VOLUME_UP " Audio Volume", + .summary = "Sets the master audio volume for the SDL frontend.", + .value = formatPercent(cfg.audioDeviceConfig.volumeRaw * 0.5f), + .dialog_title = ICON_FA_VOLUME_UP " Audio Volume", + .choices = volume_choices, + .on_choice = [this](int index) { + emu.getConfig().audioDeviceConfig.volumeRaw = static_cast(index) / 10.0f; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_PF_SOUND " Enable AAC Audio", .summary = "Enables AAC decoding for titles which use AAC streams.", .toggle_value = cfg.aacEnabled, .on_toggle = [this](bool value) { emu.getConfig().aacEnabled = value; emu.getConfig().save(); emu.reloadSettings(); }}); + rows.push_back({.kind = fsui::SettingsRowKind::Toggle, .title = ICON_FA_INFO_CIRCLE " Print DSP Firmware", .summary = "Prints the DSP firmware information during initialization.", .toggle_value = cfg.printDSPFirmware, .on_toggle = [this](bool value) { emu.getConfig().printDSPFirmware = value; emu.getConfig().save(); emu.reloadSettings(); }}); + + const int dsp_index = findArrayIndex(s_dsp_values, cfg.dspType); + std::vector dsp_choices; + for (int i = 0; i < static_cast(s_dsp_names.size()); i++) { + dsp_choices.push_back({s_dsp_names[static_cast(i)], i == dsp_index}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_PF_MICROCHIP " DSP Emulation", + .summary = "Selects the DSP emulation backend used by Panda3DS.", + .value = s_dsp_names[static_cast(dsp_index)], + .dialog_title = ICON_PF_MICROCHIP " DSP Emulation", + .choices = dsp_choices, + .on_choice = [this](int index) { + emu.getConfig().dspType = s_dsp_values[static_cast(index)]; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + + const int curve_index = findArrayIndex(s_volume_curve_values, cfg.audioDeviceConfig.volumeCurve); + std::vector curve_choices; + for (int i = 0; i < static_cast(s_volume_curve_names.size()); i++) { + curve_choices.push_back({s_volume_curve_names[static_cast(i)], i == curve_index}); + } + rows.push_back({ + .kind = fsui::SettingsRowKind::Choice, + .title = ICON_FA_INFO_CIRCLE " Volume Curve", + .summary = "Chooses how the user-facing volume value maps to audio gain.", + .value = s_volume_curve_names[static_cast(curve_index)], + .dialog_title = ICON_FA_INFO_CIRCLE " Volume Curve", + .choices = curve_choices, + .on_choice = [this](int index) { + emu.getConfig().audioDeviceConfig.volumeCurve = s_volume_curve_values[static_cast(index)]; + emu.getConfig().save(); + emu.reloadSettings(); + }, + }); + return rows; + }, + }); + + pages.push_back({ + .id = "folders", + .scope = fsui::SettingsScope::Global, + .built_in_page = fsui::BuiltInSettingsPage::Folders, + .build_rows = [this]() { + return std::vector{ + {.kind = fsui::SettingsRowKind::Heading, .title = "Game List"}, + {.kind = fsui::SettingsRowKind::Value, .title = ICON_FA_FOLDER_OPEN " Covers Directory", .summary = "Folder used to store external cover art for the game list.", .value = defaultCoverDirectory().string(), .enabled = false}, + {.kind = fsui::SettingsRowKind::Action, .title = ICON_FA_FOLDER_OPEN " Open Game List Settings", .summary = "Configure search directories, sort order, and covers.", .on_activate = []() { fsui::ShowGameListSettingsWindow(); }}, + }; + }, + }); + + pages.erase( + std::remove_if(pages.begin(), pages.end(), [scope](const fsui::SettingsPageDescriptor& page) { return page.scope != scope; }), + pages.end() + ); + return pages; +} + +bool PandaFsuiAdapter::initialize(const fsui::FontStack& fonts) +{ + syncUiStateFromConfig(); + cachedGameList = buildGameList(); + closeSelectorRequested = false; + pendingClassicUiRequest = ClassicUiRequest::None; + pendingLaunchPath.reset(); + + fsuiContext = {}; + fsuiContext.window = window; + fsuiContext.fonts = fonts; + fsuiContext.app_title = "Alber"; + fsuiContext.app_icon_path = resolveFsuiAppIconPath(emu.getConfig().frontendSettings.icon); + fsuiContext.renderer_backend = fsui::RendererBackend::OpenGL; + fsuiContext.appearance_settings.expose_theme_selector = true; + fsuiContext.appearance_settings.expose_prompt_icon_selector = true; + fsuiContext.appearance_settings.expose_background_image_selector = true; + fsuiContext.host.ui_state = &uiState; + fsuiContext.host.has_running_game = [this]() { return emu.romType != ROMType::None; }; + fsuiContext.host.get_current_game = [this]() { return buildCurrentGameInfo(); }; + fsuiContext.host.get_game_list = [this]() { return cachedGameList; }; + fsuiContext.host.refresh_game_list = [this](bool) { cachedGameList = buildGameList(); }; + fsuiContext.host.persist_ui_state = [this](bool reload) { persistUiState(reload); }; + fsuiContext.host.set_paused = [this](bool paused) { + if (onPauseChange) { + onPauseChange(paused); + } + }; + fsuiContext.host.resume_game = [this]() { + if (onPauseChange) { + onPauseChange(false); + } + }; + fsuiContext.host.detect_prompt_icon_pack = []() { return fsui::DetectPromptIconPackFromSDL(); }; + fsuiContext.host.request_classic_ui = [this]() { requestClassicUi(true); }; + fsuiContext.host.launch_path = [this](const std::filesystem::path& path) { requestLaunchPath(path); }; + fsuiContext.host.close_selector = [this]() { closeSelectorRequested = true; }; + fsuiContext.host.get_landing_items = [this]() { return buildLandingItems(); }; + fsuiContext.host.get_start_items = [this]() { return buildStartItems(); }; + fsuiContext.host.get_exit_items = [this]() { return buildExitItems(); }; + fsuiContext.host.get_pause_items = [this]() { return buildPauseItems(); }; + fsuiContext.host.get_game_launch_options = [this](const fsui::GameEntry& entry) { return buildGameLaunchOptions(entry); }; + fsuiContext.host.get_settings_pages = [this](fsui::SettingsScope scope) { return buildSettingsPages(scope); }; + return fsui::Initialize(fsuiContext); +} + +void PandaFsuiAdapter::shutdown(bool clear_state) +{ + fsui::Shutdown(clear_state); +} + +void PandaFsuiAdapter::render() +{ + syncUiStateFromConfig(); + fsui::Render(); +} + +void PandaFsuiAdapter::consumeCommands() +{ + for (const fsui::Command& command : fsui::ConsumeCommands()) { + switch (command.type) { + case fsui::CommandType::LaunchPath: + if (!command.path.empty()) { + pendingLaunchPath = command.path; + closeSelectorRequested = true; + } + break; + + case fsui::CommandType::CloseSelector: + closeSelectorRequested = true; + break; + + case fsui::CommandType::Resume: + if (onPauseChange) { + onPauseChange(false); + } + break; + + case fsui::CommandType::Reset: + emu.reset(Emulator::ReloadOption::Reload); + if (onPauseChange) { + onPauseChange(false); + } + break; + + case fsui::CommandType::ExitToLibrary: + if (onExitToSelector) { + onExitToSelector(); + } else { + SDL_Event quit {}; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + } + break; + + case fsui::CommandType::RequestQuit: + { + SDL_Event quit {}; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + } + break; + + case fsui::CommandType::ExitFullscreenUI: + requestClassicUi(false); + break; + + case fsui::CommandType::SwitchShell: + if (command.id == "desktop" || command.id == "classic") { + requestClassicUi(false); + } + break; + + case fsui::CommandType::Custom: + default: + break; + } + } +} + +void PandaFsuiAdapter::setSelectorMode(bool selector_mode) +{ + if (selector_mode) { + closeSelectorRequested = false; + } + fsui::SetSelectorMode(selector_mode); +} + +bool PandaFsuiAdapter::isSelectorMode() const +{ + return fsui::IsSelectorMode(); +} + +bool PandaFsuiAdapter::hasActiveWindow() const +{ + return fsui::HasActiveWindow(); +} + +void PandaFsuiAdapter::openPauseMenu() +{ + fsui::OpenPauseMenu(); +} + +void PandaFsuiAdapter::returnToPreviousWindow() +{ + fsui::ReturnToPreviousWindow(); +} + +void PandaFsuiAdapter::returnToMainWindow() +{ + fsui::ReturnToMainWindow(); +} + +void PandaFsuiAdapter::switchToSettings() +{ + fsui::SwitchToSettings(); +} + +std::optional PandaFsuiAdapter::consumeLaunchPath() +{ + std::optional value = pendingLaunchPath; + pendingLaunchPath.reset(); + return value; +} + +bool PandaFsuiAdapter::consumeCloseSelector() +{ + const bool value = closeSelectorRequested; + closeSelectorRequested = false; + return value; +} + +PandaFsuiAdapter::ClassicUiRequest PandaFsuiAdapter::consumeClassicUiRequest() +{ + const ClassicUiRequest value = pendingClassicUiRequest; + pendingClassicUiRequest = ClassicUiRequest::None; + return value; +} + +#endif diff --git a/third_party/discord-rpc b/third_party/discord-rpc index 75d09298..1628f8b3 160000 --- a/third_party/discord-rpc +++ b/third_party/discord-rpc @@ -1 +1 @@ -Subproject commit 75d092983caf6503194098b5f6adaeb09e395fdf +Subproject commit 1628f8b31533812ac1e5526afb0d5508932c7dbd diff --git a/third_party/fsui b/third_party/fsui new file mode 160000 index 00000000..ddbe3355 --- /dev/null +++ b/third_party/fsui @@ -0,0 +1 @@ +Subproject commit ddbe335536bd7ccc51c674824936e5c12fb2e7ad